What to do?
ReceiptService, TypingEventService를 만들고, Test 코드 작성하기
채팅서비스에서 다음의 두가지 기능을 구현하기 위한 서비스 코드를 작성
ⓐ 상대방이 내가 보낸 메세지를 읽었는지 여부
ⓑ 상대방이 현재 타이핑을 하고 있는지 여부
이를 위해 Receipt, TypingEvent 관련된 모델과 서비스코드, 테스트코드까지 작성하였다.
이전에 작성한 Message service코드와 거의 대부분이 유사하다
Refence
Receipt Model
메세지를 상대방이 읽었는지를 나타내기 위한 Entity
- recipeint : 메세지를 받은 유저의 id
- messageId : 메세지 아이디
- status : 메세지 상태를 enum으로 정의
- delivered
- read : 상대방이 메세지를 읽음
/model/receipt_model.dart
enum ReceiptStatus { delivered, read }
extension EnumParsing on ReceiptStatus {
String value() {
return toString().split(".").last;
}
static ReceiptStatus fromString(String text) {
return ReceiptStatus.values.firstWhere((element) => element.value() == text);
}
}
class Receipt {
String _id;
String get id => _id;
final String recipient;
final String messageId;
final ReceiptStatus status;
final DateTime timestamp;
Receipt(
{@required this.recipient,
@required this.messageId,
@required this.status,
@required this.timestamp});
Map<String, dynamic> toJson() => {
'recipient': recipient,
'message_id': messageId,
'status': status.value(),
'timestamp': timestamp
};
factory Receipt.fromJson(Map<String, dynamic> json) {
var receipt = Receipt(
recipient: json['recipient'],
messageId: json['message_id'],
status: EnumParsing.fromString(json['status']),
timestamp: json['timestamp']);
receipt._id = json['id'];
return receipt;
}
}
Typing Event Model
유저가 현재 채팅을 입력하고 있는지 여부를 나타나는 Entity
- from : 타이핑을 하고 있는 유저의 id
- to : 타이핑 여부를 전달받을 유저의 id
- evnet : 타이핑 여부(start, stop)를 enum으로 정의
/model/typing_event_model.dart
enum Typing { start, stop }
extension TypingParser on Typing {
String value() {
return toString().split(".").last;
}
static Typing fromString(String event) {
return Typing.values.firstWhere((element) => element.value() == event);
}
}
class TypingEvent {
String get id => _id;
String _id;
final String from;
final String to;
final Typing event;
TypingEvent({@required this.from, @required this.to, @required this.event});
// class → json
Map<String, dynamic> toJson() => {
'from': from,
'to': to,
'event': event,
};
// json → class
factory TypingEvent.fromJson(Map<String, dynamic> json) {
final event = TypingEvent(
from: json['from'],
to: json['to'],
event: json['event'],
);
event._id = json['id'];
return event;
}
}
ReceiptService
기존에 작성한 message service 코드와 거의 유사
/service/receipt/receipient_service_interface.dart
abstract class IReceiptService {
Future<bool> send(Receipt receipt);
Stream<Receipt> receipts(User user);
void dispose();
}
/service/receipt/receipient_service.dart
class ReceiptService implements IReceiptService {
final Rethinkdb _db;
final Connection _connection;
final _logger = Logger();
final _controller = StreamController<Receipt>.broadcast();
StreamSubscription _changeFeed;
ReceiptService(this._db, this._connection);
@override
dispose() {
_changeFeed?.cancel();
_controller?.close();
}
@override
Stream<Receipt> receipts(User user) {
_startReceivingReceipts(user);
return _controller.stream;
}
@override
Future<bool> send(Receipt receipt) async {
Map record =
await _db.table('receipts').insert(receipt.toJson()).run(_connection);
return record['inserted'] == 1;
}
_startReceivingReceipts(User user) {
_changeFeed = _db
.table('receipts')
.filter({'recipient': user.id})
.changes({'include_initial': true})
.run(_connection)
.asStream()
.cast<Feed>()
.listen((event) {
event
.forEach((feedData) {
if (feedData['new_val'] == null) return;
final receipt = _receiptFromFeed(feedData);
_controller.sink.add(receipt);
_removeDeliveredReceipt(receipt);
})
.catchError((err) => _logger.e(err))
.onError((err, stackTrace) => _logger.e(err));
});
}
Receipt _receiptFromFeed(feedData) => Receipt.fromJson(feedData['new_val']);
_removeDeliveredReceipt(Receipt receipt) {
_db
.table('receipts')
.get(receipt.id)
.delete({'return_changes': false}).run(_connection);
}
}
TypingNotificationService
기존에 작성한 message service 코드와 거의 유사
/service/typing_notification/typing_notification_service_interface.dart
abstract class ITypingNotificationService {
Future<bool> send({@required TypingEvent event});
Stream<TypingEvent> subscribe(User user, List<String> userIds);
void dispose();
}
/service/typing_notification/typing_notification_service.dart
class TypingNotificationService implements ITypingNotificationService {
final _logger = Logger();
final Rethinkdb _db;
final Connection _connection;
final _controller = StreamController<TypingEvent>.broadcast();
StreamSubscription _changeFeed;
TypingNotificationService(this._db, this._connection);
@override
Future<bool> send(
{@required TypingEvent event, @required User to}) async {
if (to.active) return false;
Map record = await _db
.table('typing_events')
.insert(event.toJson(), {'conflict': 'update'}).run(_connection);
return record['inserted'] == 1;
}
@override
Stream<TypingEvent> subscribe(User user, List<String> userIds) {
_startReceivingTypingEvent(user, userIds);
return _controller.stream;
}
@override
void dispose() {
_changeFeed?.cancel();
_controller?.close();
}
_startReceivingTypingEvent(User user, List<String> userIds) {
_changeFeed = _db
.table('typing_events')
.filter((event) {
return event('to')
.eq(user.id)
.and(_db.expr(userIds).contains(event('from')));
})
.changes({'include_initial': true})
.run(_connection)
.asStream()
.cast<Feed>()
.listen((event) {
event
.forEach((feedData) {
if (feedData['new_val'] == null) return;
final typingEvent = _typingEventFromFeed(feedData);
_controller.sink.add(typingEvent);
_removeDeliveredTypingEvent(typingEvent);
})
// logging error
.catchError((err) => _logger.e(err))
.onError((err, stackTrace) => _logger.e(err));
});
}
TypingEvent _typingEventFromFeed(feedData) =>
TypingEvent.fromJson(feedData['new_val']);
_removeDeliveredTypingEvent(TypingEvent event) {
_db
.table('typing_events')
.get(event.id)
.delete({'return_changes': false}).run(_connection);
}
}
테스트 코드
util_for_test.dart
(기작성했던 코드를 수정함)
String databaseName = 'test';
List<String> tablesNames = ["users", "messages", "receipts", "typing_events"];
Future<void> createDatabase(Rethinkdb db, Connection connection) async {
await db.dbCreate(databaseName).run(connection).catchError(print);
for (String tn in tablesNames) {
await db.tableCreate(tn).run(connection).catchError(print);
}
}
Future<void> cleanDatabase(Rethinkdb db, Connection connection) async {
for (String tn in tablesNames) {
await db.table(tn).delete().run(connection);
}
}
test/receipt_service_test.dart
void main() {
Rethinkdb db = Rethinkdb();
Connection connection;
ReceiptService sut;
setUp(() async {
connection = await db.connect(host: '127.0.0.1', port: 28015);
await createDatabase(db, connection);
sut = ReceiptService(db, connection);
});
tearDown(() async {
sut.dispose();
await cleanDatabase(db, connection);
});
final user =
User.fromJson({'id': '2', 'active': true, 'lastSeen': DateTime.now()});
test('if receipt sent successfully, then return true', () async {
Receipt receipt = Receipt(
recipient: '444',
messageId: '12',
status: ReceiptStatus.delivered,
timestamp: DateTime.now());
final result = await sut.send(receipt);
expect(result, true);
});
test('sending receipt works successfully', () async {
sut.receipts(user).listen(expectAsync1((receipt) {
expect(receipt.recipient, user.id);
}, count: 2));
Receipt receipt1 = Receipt(
recipient: user.id,
messageId: "1234",
status: ReceiptStatus.delivered,
timestamp: DateTime.now());
Receipt receipt2 = Receipt(
recipient: user.id,
messageId: "12345",
status: ReceiptStatus.delivered,
timestamp: DateTime.now());
await sut.send(receipt1);
await sut.send(receipt2);
});
}
도커 컨테이너 실행
docker run -d -p 8080:8080 -p 28015:28015 rethinkdb
테스트 코드 실행 결과, 잘 수행됨
'Flutter' 카테고리의 다른 글
[Flutter] 채팅앱 만들기 #6 (0) | 2023.03.05 |
---|---|
[Flutter] 채팅앱 만들기 #5 (0) | 2023.03.04 |
[Flutter] 채팅앱 만들기 #3 (0) | 2023.03.01 |
[Flutter] 채팅앱 만들기 #2 (0) | 2023.03.01 |
[Flutter] 채팅앱 만들기 #1 (0) | 2023.02.27 |