269 lines
11 KiB
Dart
269 lines
11 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:friendly_map/modules/core/core.dart';
|
|
import 'package:friendly_map/modules/storage/storage.dart';
|
|
import 'package:friendly_map/modules/sync/sync.dart';
|
|
import 'package:friendly_map/modules/transport/transport.dart';
|
|
|
|
import 'support/device.dart';
|
|
|
|
const pa = '+821011112222';
|
|
const pb = '+821022223333';
|
|
|
|
Message message(String id, String room, {String body = '안녕'}) =>
|
|
Message(id: id, roomId: room, senderId: 'me', body: body, sentAt: 100, updatedAt: 100);
|
|
|
|
void main() {
|
|
group('인사', () {
|
|
test('인사를 보내면 상대에게 이름·공개 키가 알려지고, 답 인사로 내 쪽도 키를 알게 됨', () async {
|
|
final hub = MemoryHub();
|
|
final a = await Device.create(hub, pa, '가');
|
|
final b = await Device.create(hub, pb, '나');
|
|
expect(await a.engine.sendHello(pb), SendResult.sent);
|
|
await Device.settleAll();
|
|
final hello = b.events.single;
|
|
expect(hello.type, SyncEventType.hello);
|
|
expect(hello.phone, pa);
|
|
expect(hello.name, '가');
|
|
expect(hello.publicKey, a.key);
|
|
expect(hello.isReply, false);
|
|
await b.engine.sendHello(pa, reply: true);
|
|
await Device.settleAll();
|
|
expect(a.events.single.isReply, true);
|
|
expect(a.events.single.publicKey, b.key);
|
|
});
|
|
|
|
test('받는 사람이 아닌 봉투는 무시', () async {
|
|
final hub = MemoryHub();
|
|
final b = await Device.create(hub, pb, '나');
|
|
final e = Envelope(id: 'x', from: pa, to: '+821099999999', kind: 'hello', unitId: '',
|
|
body: '{"name":"가","key":"K","reply":false}', stamp: 1);
|
|
expect(await b.engine.receive(e), Outcome.ignored);
|
|
expect(b.events, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('명함 공유', () {
|
|
test('공유한 명함이 상대 받은 명함에 들어가고, 고쳐서 다시 보내면 갱신', () async {
|
|
final hub = MemoryHub();
|
|
final a = await Device.create(hub, pa, '가');
|
|
final b = await Device.create(hub, pb, '나');
|
|
final pBofA = await a.know(b);
|
|
final pAofB = await b.know(a);
|
|
final card = await a.addCard('company');
|
|
final share = await a.shareCard(card, pBofA);
|
|
expect(await a.engine.sendCard(share), SendResult.sent);
|
|
await Device.settleAll();
|
|
final got = (await b.storage.receivedCards.get(card.id))!;
|
|
expect(got.personId, pAofB.id);
|
|
expect(got.kind, 'company');
|
|
expect(got.fieldsJson, '{"company":"한빛"}');
|
|
final edited = share.resent(snapshot: '{"company":"새빛"}', sentVersion: 9, updatedAt: 20);
|
|
await a.storage.shares.put(edited);
|
|
await a.engine.sendCard(edited);
|
|
await Device.settleAll();
|
|
expect((await b.storage.receivedCards.get(card.id))!.fieldsJson, '{"company":"새빛"}');
|
|
expect((await b.storage.receivedCards.all()).length, 1);
|
|
});
|
|
|
|
test('상대가 꺼져 있어도 보관함을 거쳐 나중에 받음', () async {
|
|
final hub = MemoryHub();
|
|
final a = await Device.create(hub, pa, '가');
|
|
final b = await Device.create(hub, pb, '나', start: false);
|
|
final pBofA = await a.know(b);
|
|
await b.know(a);
|
|
final card = await a.addCard('school');
|
|
expect(await a.engine.sendCard(await a.shareCard(card, pBofA)), SendResult.sent);
|
|
await Device.settleAll();
|
|
expect(await b.storage.receivedCards.all(), isEmpty);
|
|
await b.engine.start(); // 켜면서 보관함에서 가져옴
|
|
expect((await b.storage.receivedCards.all()).single.kind, 'school');
|
|
});
|
|
});
|
|
|
|
group('보낼함', () {
|
|
test('아무 길도 없으면 보낼함에 쌓이고, 길이 생기면 다시 보냄', () async {
|
|
final hub = MemoryHub()..mailboxEnabled = false;
|
|
final a = await Device.create(hub, pa, '가');
|
|
final b = await Device.create(hub, pb, '나', start: false);
|
|
final pBofA = await a.know(b);
|
|
await b.know(a);
|
|
final card = await a.addCard('group');
|
|
expect(await a.engine.sendCard(await a.shareCard(card, pBofA)), SendResult.queued);
|
|
await Device.settleAll();
|
|
expect((await a.storage.outbox.pending()).length, 1);
|
|
expect(await a.engine.flushOutbox(), 0);
|
|
await Device.settleAll();
|
|
expect((await a.storage.outbox.pending()).single.attempts, 1);
|
|
hub.mailboxEnabled = true;
|
|
await b.engine.start();
|
|
await Device.settleAll();
|
|
expect(await a.engine.flushOutbox(), 1);
|
|
await Device.settleAll();
|
|
expect(await a.storage.outbox.pending(), isEmpty);
|
|
await b.engine.refresh();
|
|
await Device.settleAll();
|
|
expect((await b.storage.receivedCards.all()).length, 1);
|
|
});
|
|
|
|
test('상대 키를 모르면 보내지 않고 알려 줌', () async {
|
|
final hub = MemoryHub();
|
|
final a = await Device.create(hub, pa, '가');
|
|
final p = (await a.storage.people.register(pb, '나', 2))!;
|
|
final card = await a.addCard('company');
|
|
expect(await a.engine.sendCard(await a.shareCard(card, p)), SendResult.noKey);
|
|
await Device.settleAll();
|
|
expect(await a.storage.outbox.pending(), isEmpty);
|
|
});
|
|
});
|
|
|
|
group('대화', () {
|
|
Future<(Device, Device, Person, Person)> pair() async {
|
|
final hub = MemoryHub();
|
|
final a = await Device.create(hub, pa, '가');
|
|
final b = await Device.create(hub, pb, '나');
|
|
return (a, b, await a.know(b), await b.know(a));
|
|
}
|
|
|
|
test('개인 대화방은 명함 없이 열리고 대화가 전달됨', () async {
|
|
final (a, b, pBofA, pAofB) = await pair();
|
|
final room = await a.addRoom('r1', 'personal', [pBofA]);
|
|
await a.engine.sendRoom(await a.shareRoom(room, pBofA));
|
|
await Device.settleAll();
|
|
final m = message('m1', 'r1');
|
|
await a.storage.messages.put(m);
|
|
await a.engine.sendMessage(m);
|
|
await Device.settleAll();
|
|
final r = (await b.storage.rooms.get('r1'))!;
|
|
expect(r.contextKind, 'personal');
|
|
expect(r.membersJson, '["${pAofB.id}"]');
|
|
final got = (await b.storage.messages.get('m1'))!;
|
|
expect(got.body, '안녕');
|
|
expect(got.senderId, pAofB.id);
|
|
});
|
|
|
|
test('받은 방에서 내가 쓴 대화도 상대에게 감 (받은 방도 공유 기록이 생김)', () async {
|
|
final (a, b, pBofA, pAofB) = await pair();
|
|
final room = await a.addRoom('r6', 'personal', [pBofA]);
|
|
await a.engine.sendRoom(await a.shareRoom(room, pBofA));
|
|
await Device.settleAll();
|
|
expect((await b.storage.shares.activeFor('r6')).single.peerId, pAofB.id);
|
|
final m = message('m6', 'r6', body: '답장');
|
|
await b.storage.messages.put(m);
|
|
await b.engine.sendMessage(m);
|
|
await Device.settleAll();
|
|
expect((await a.storage.messages.get('m6'))!.body, '답장');
|
|
});
|
|
|
|
test('회사 맥락 방은 상대의 회사 명함을 받은 뒤에야 열림', () async {
|
|
final (a, b, pBofA, _) = await pair();
|
|
final room = await a.addRoom('r2', 'company', [pBofA]);
|
|
await a.engine.sendRoom(await a.shareRoom(room, pBofA));
|
|
await Device.settleAll();
|
|
expect(await b.storage.rooms.get('r2'), null);
|
|
final card = await a.addCard('company');
|
|
await a.engine.sendCard(await a.shareCard(card, pBofA));
|
|
await Device.settleAll();
|
|
expect((await b.storage.rooms.get('r2'))!.contextKind, 'company');
|
|
});
|
|
|
|
test('방 정보보다 대화가 먼저 와도 보류했다가 반영', () async {
|
|
final (a, b, pBofA, _) = await pair();
|
|
final room = await a.addRoom('r3', 'personal', [pBofA]);
|
|
final share = await a.shareRoom(room, pBofA);
|
|
final m = message('m3', 'r3');
|
|
await a.engine.sendMessage(m);
|
|
await Device.settleAll();
|
|
expect(await b.storage.messages.get('m3'), null);
|
|
await a.engine.sendRoom(share);
|
|
await Device.settleAll();
|
|
expect((await b.storage.messages.get('m3'))!.body, '안녕');
|
|
});
|
|
|
|
test('같은 봉투가 두 번 와도 한 번만 반영', () async {
|
|
final (a, b, pBofA, _) = await pair();
|
|
final room = await a.addRoom('r4', 'personal', [pBofA]);
|
|
await a.engine.sendRoom(await a.shareRoom(room, pBofA));
|
|
await Device.settleAll();
|
|
final env = Envelope(
|
|
id: 'dup', from: pa, to: pb, kind: 'chat.message', unitId: 'r4',
|
|
body: await a.engineCipherSeal(b.key, 'r4', '{"id":"m4","body":"한번","sentAt":5}'), stamp: 7);
|
|
expect(await b.engine.receive(env), Outcome.applied);
|
|
expect(await b.engine.receive(env), Outcome.ignored);
|
|
expect((await b.storage.messages.page('r4')).length, 1);
|
|
});
|
|
|
|
test('모르는 사람이 보낸 대화는 보류, 사람을 등록하고 키를 알면 반영', () async {
|
|
final hub = MemoryHub();
|
|
final a = await Device.create(hub, pa, '가');
|
|
final b = await Device.create(hub, pb, '나');
|
|
final pBofA = await a.know(b);
|
|
final room = await a.addRoom('r5', 'personal', [pBofA]);
|
|
await a.engine.sendRoom(await a.shareRoom(room, pBofA));
|
|
await Device.settleAll();
|
|
expect(await b.storage.rooms.get('r5'), null); // 아직 가를 모름
|
|
await b.know(a);
|
|
expect(await b.engine.retryHeld(), 1);
|
|
expect((await b.storage.rooms.get('r5')), isNotNull);
|
|
});
|
|
|
|
test('키가 다른 사람이 보낸 봉투는 무시', () async {
|
|
final hub = MemoryHub();
|
|
final b = await Device.create(hub, pb, '나');
|
|
await b.storage.people.register(pa, '가', 2, publicKey: 'KEY-가');
|
|
final env = Envelope(id: 'bad', from: pa, to: pb, kind: 'card.share', unitId: 'c9',
|
|
body: 'forged|c9|e30=', stamp: 5);
|
|
expect(await b.engine.receive(env), Outcome.ignored);
|
|
expect(await b.storage.receivedCards.all(), isEmpty);
|
|
});
|
|
});
|
|
|
|
group('충돌 규칙', () {
|
|
test('같은 사람은 늦은 변경이 이기고, 다른 사람끼리는 먼저 쓴 변경이 이김', () async {
|
|
final rule = ConflictRule(Storage.memory().meta);
|
|
expect(await rule.accept('k', 'A', 100), true);
|
|
expect(await rule.accept('k', 'A', 150), true);
|
|
expect(await rule.accept('k', 'A', 120), false);
|
|
expect(await rule.accept('k', 'B', 200), false);
|
|
expect(await rule.accept('k', 'B', 100), true);
|
|
expect(await rule.accept('k', 'A', 300), false);
|
|
});
|
|
});
|
|
|
|
group('시계 보정', () {
|
|
test('서버 시각으로 맞추고, 그 뒤에는 상대 시각으로 바꾸지 않음', () async {
|
|
final clock = Clock();
|
|
final sync = ClockSync(clock);
|
|
final raw = DateTime.now().millisecondsSinceEpoch ~/ 10;
|
|
expect(await sync.syncWith(_Server(raw + 500)), true);
|
|
expect((clock.now() - raw - 500).abs() < 50, true);
|
|
expect(sync.syncWithPeer(raw + 9000), false);
|
|
expect(sync.source, 'server');
|
|
});
|
|
|
|
test('상대 시각과 1초 미만 차이는 맞추지 않음 (전송 지연일 수 있음)', () async {
|
|
final clock = Clock();
|
|
final sync = ClockSync(clock);
|
|
final raw = DateTime.now().millisecondsSinceEpoch ~/ 10;
|
|
expect(sync.syncWithPeer(raw + 30), false);
|
|
expect(clock.offset, 0);
|
|
});
|
|
|
|
test('서버가 없으면 같은 네트워크 상대 시각을 따름', () async {
|
|
final clock = Clock();
|
|
final sync = ClockSync(clock);
|
|
expect(await sync.syncWith(_Server(null)), false);
|
|
final raw = DateTime.now().millisecondsSinceEpoch ~/ 10;
|
|
expect(sync.syncWithPeer(raw - 300), true);
|
|
expect((clock.now() - raw + 300).abs() < 50, true);
|
|
expect(sync.source, 'peer');
|
|
});
|
|
});
|
|
}
|
|
|
|
class _Server implements TimeSource {
|
|
_Server(this.stamp);
|
|
final int? stamp;
|
|
@override
|
|
Future<int?> referenceStamp() async => stamp;
|
|
}
|