frindlymap/test/sync_test.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;
}