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 referenceStamp() async => stamp; }