import 'package:flutter_test/flutter_test.dart'; import 'package:friendly_map/modules/platform/platform.dart'; import 'package:friendly_map/modules/storage/storage.dart'; class FakeProbe implements DeviceStorageProbe { FakeProbe(this.percent); int percent; @override Future usedPercent() async => percent; } MyCard card(String id, String kind, {int at = 1}) => MyCard(id: id, kind: kind, title: '$kind 명함', fieldsJson: '{}', updatedAt: at); Message msg(String id, int at, {String body = 'hi', String room = 'r1'}) => Message(id: id, roomId: room, senderId: 's', body: body, sentAt: at, updatedAt: at); /// 저장 방식(메모리, SQLite)이 같은 약속을 지키는지 확인하는 공통 시험. /// 새 저장 방식을 만들면 이 시험에 넣어 보면 된다. void storageContract(String label, Future Function() make) { group('$label 저장소', () { late Storage s; setUp(() async => s = await make()); test('내 명함: 저장, 읽기, 종류별 찾기, 같은 id 는 덮어씀', () async { await s.myCards.put(card('a', 'company')); await s.myCards.put(card('b', 'school')); await s.myCards.put(card('a', 'company', at: 5)); expect((await s.myCards.get('a'))!.updatedAt, 5); expect((await s.myCards.all()).length, 2); expect((await s.myCards.byKind('school')).single.id, 'b'); expect(await s.myCards.updatedAtOf('a'), 5); expect(await s.myCards.updatedAtOf('none'), null); expect(await s.myCards.get('none'), null); }); test('지우기: 명함·받은 명함·연결 고리, 없는 것을 지워도 문제 없음', () async { await s.myCards.put(card('a', 'company')); await s.myCards.remove('a'); await s.myCards.remove('none'); expect(await s.myCards.get('a'), null); await s.links.put(const Link(id: 'l', personId: 'p', relation: '동창', detail: '', updatedAt: 1)); await s.links.remove('l'); expect(await s.links.byPerson('p'), isEmpty); await s.receivedCards.put(ReceivedCard( id: 'r', personId: 'p', kind: 'school', fieldsJson: '{}', receivedAt: 1, updatedAt: 1)); await s.receivedCards.remove('r'); expect(await s.receivedCards.byPerson('p'), isEmpty); }); test('받은 명함은 사람별로 최근 것부터', () async { ReceivedCard rc(String id, String p, int at) => ReceivedCard( id: id, personId: p, kind: 'school', fieldsJson: '{}', receivedAt: at, updatedAt: at); await s.receivedCards.put(rc('1', 'p', 10)); await s.receivedCards.put(rc('2', 'p', 30)); await s.receivedCards.put(rc('3', 'q', 20)); expect((await s.receivedCards.byPerson('p')).map((c) => c.id).toList(), ['2', '1']); }); test('사람: 번호가 같으면 같은 사람, 이름만 다르면 이름 변경 후보', () async { final p = await s.people.register('+821011112222', '정민호', 100); expect(p, isNotNull); expect((await s.people.match('+821011112222', '정민호')).kind, MatchKind.same); final m = await s.people.match('+821011112222', '정민우'); expect(m.kind, MatchKind.nameDiffers); expect(m.candidates.single.id, p!.id); }); test('사람: 이름이 같고 번호가 처음이면 번호 변경 후보, 둘 다 처음이면 새 사람', () async { await s.people.register('+821011112222', '정민호', 100); final m = await s.people.match('+821033334444', '정민호'); expect(m.kind, MatchKind.phoneDiffers); expect(m.candidates, hasLength(1)); expect((await s.people.match('+821055556666', '김하늘')).kind, MatchKind.none); }); test('사람: 같은 번호 중복 등록은 막힘', () async { await s.people.register('+821011112222', '정민호', 100); expect(await s.people.register('+821011112222', '다른이름', 101), null); }); test('사람: 번호를 바꿔도 id 는 그대로, 이전 번호로도 찾아짐', () async { final p = (await s.people.register('+821011112222', '정민호', 100))!; expect(await s.people.changePhone(p.id, '+821099998888', 200), true); final now = (await s.people.get(p.id))!; expect(now.phone, '+821099998888'); expect((await s.people.byPhone('+821011112222'))!.id, p.id); expect((await s.people.byPhone('+821099998888'))!.id, p.id); expect((await s.people.match('+821011112222', '정민호')).kind, MatchKind.same); }); test('사람: 새 번호가 다른 사람 것이면 번호 변경 거절', () async { final a = (await s.people.register('+821011112222', '가', 100))!; await s.people.register('+821033334444', '나', 100); expect(await s.people.changePhone(a.id, '+821033334444', 200), false); expect((await s.people.get(a.id))!.phone, '+821011112222'); }); test('사람: 이름을 바꿔도 이전 이름으로 찾아지고 같은 사람으로 봄', () async { final p = (await s.people.register('+821011112222', '정민호', 100))!; expect(await s.people.changeName(p.id, '정민우', 200), true); expect((await s.people.get(p.id))!.name, '정민우'); expect((await s.people.byName('정민호')).single.id, p.id); expect((await s.people.byName('정민우')).single.id, p.id); expect((await s.people.match('+821011112222', '정민호')).kind, MatchKind.same); }); test('사람: 공개 키를 붙인다', () async { final p = (await s.people.register('+821011112222', '정민호', 100))!; await s.people.setPublicKey(p.id, 'KEY', 300); expect((await s.people.get(p.id))!.publicKey, 'KEY'); }); test('대화: 시간순, before, limit, 별도 저장 제외', () async { for (var i = 1; i <= 5; i++) { await s.messages.put(msg('m$i', i * 10)); } await s.messages.put(msg('x', 5, room: 'r2')); expect((await s.messages.page('r1')).map((m) => m.id).toList(), ['m1', 'm2', 'm3', 'm4', 'm5']); expect((await s.messages.page('r1', limit: 2)).map((m) => m.id).toList(), ['m4', 'm5']); expect((await s.messages.page('r1', before: 30, limit: 10)).map((m) => m.id).toList(), ['m1', 'm2']); }); test('대화: 텍스트 크기는 UTF-8 바이트, 범위는 처음~끝', () async { await s.messages.put(msg('a', 10, body: 'abc')); await s.messages.put(msg('b', 20, body: '가나')); expect(await s.messages.textBytes('r1'), 3 + 6); expect(await s.messages.span('r1'), (10, 20)); expect(await s.messages.span('빈방'), null); expect(await s.messages.textBytes('빈방'), 0); }); test('대화: 범위를 별도 저장으로 나누고 되돌림 (지우지 않음)', () async { for (var i = 1; i <= 4; i++) { await s.messages.put(msg('m$i', i * 10, body: 'xx')); } expect(await s.messages.setArchivedBetween('r1', 10, 20, 99), 2); expect((await s.messages.page('r1')).map((m) => m.id).toList(), ['m3', 'm4']); expect(await s.messages.textBytes('r1'), 4); expect(await s.messages.span('r1'), (30, 40)); expect((await s.messages.get('m1'))!.archived, true); expect(await s.messages.setArchivedBetween('r1', 10, 20, 100, archived: false), 2); expect((await s.messages.page('r1')).length, 4); }); test('대화방: 최근 순서, 별도 저장된 방은 기본에서 빠짐', () async { ChatRoom room(String id, int at, {bool archived = false}) => ChatRoom( id: id, title: id, kind: 'direct', contextKind: 'personal', membersJson: '[]', archived: archived, updatedAt: at); await s.rooms.put(room('a', 10)); await s.rooms.put(room('b', 30)); await s.rooms.put(room('c', 20, archived: true)); expect((await s.rooms.recent()).map((r) => r.id).toList(), ['b', 'a']); expect((await s.rooms.recent(includeArchived: true)).map((r) => r.id).toList(), ['b', 'c', 'a']); }); test('공유: 지금 공유 중인 것만 찾고, 끝내면 빠짐', () async { ShareRecord sr(String id, String unit, String target) => ShareRecord( id: id, unit: unit, targetId: target, peerId: 'p', startedAt: 1, updatedAt: 1); await s.shares.put(sr('1', 'card', 'c1')); await s.shares.put(sr('2', 'room', 'r1')); expect((await s.shares.activeFor('c1')).single.id, '1'); expect((await s.shares.activeOf('room')).single.id, '2'); await s.shares.end('1', 50); expect(await s.shares.activeFor('c1'), isEmpty); final ended = (await s.shares.get('1'))!; expect(ended.endedAt, 50); expect(ended.active, false); }); test('보낼함: 오래된 것부터, 갱신, 삭제', () async { OutboxItem o(String id, int at) => OutboxItem(id: id, peerPhone: '+82', envelopeJson: '{}', createdAt: at); await s.outbox.add(o('b', 20)); await s.outbox.add(o('a', 10)); expect((await s.outbox.pending()).map((x) => x.id).toList(), ['a', 'b']); await s.outbox.update(o('a', 10).tried()); expect((await s.outbox.pending()).first.attempts, 1); await s.outbox.remove('a'); expect((await s.outbox.pending()).single.id, 'b'); }); test('친구 요청: 대기 중인 것만, 상태 바꾸면 빠짐', () async { final r = FriendRequest( id: '+821011112222', name: '정민호', publicKey: 'K', receivedAt: 5, updatedAt: 5); await s.friendRequests.put(r); expect((await s.friendRequests.pending()).single.phone, '+821011112222'); await s.friendRequests.put(r.withStatus('accepted', 9)); expect(await s.friendRequests.pending(), isEmpty); }); test('연결 고리: 사람별', () async { await s.links.put(const Link(id: '1', personId: 'p', relation: '동창', detail: '가람초', updatedAt: 1)); await s.links.put(const Link(id: '2', personId: 'q', relation: '동문', detail: '한국대', updatedAt: 1)); expect((await s.links.byPerson('p')).single.relation, '동창'); }); test('설정: 기본값, 저장 후 읽기, 내 정보', () async { expect((await s.settings.load()).separateTextBytes, 1024 * 1024); expect(await s.settings.loadProfile(), null); await s.settings.save(const AppSettings(storageUsagePercent: 90, exchangeMode: 'auto'), 1); final back = await s.settings.load(); expect(back.storageUsagePercent, 90); expect(back.exchangeMode, 'auto'); await s.settings.saveProfile(const MyProfile(phone: '+82101', name: '나'), 2); expect((await s.settings.loadProfile())!.name, '나'); await s.settings.putText('home_layout', '{"a":1}', 3); expect(await s.settings.getText('home_layout'), '{"a":1}'); }); test('이름-값: 봉투 처리 기록과 충돌 기록', () async { expect(await s.seen.get('e1'), null); await s.seen.put('e1', '1', 1); expect(await s.seen.get('e1'), '1'); await s.meta.put('row1', 'author|5', 1); expect(await s.meta.get('row1'), 'author|5'); }); test('별도 저장 권고와 나누기', () async { for (var i = 1; i <= 3; i++) { await s.messages.put(msg('m$i', i * 10, body: 'x' * 400000)); } final probe = FakeProbe(10); final svc = ArchiveService( messages: s.messages, settings: s.settings, clock: Clock(), probe: probe); final advice = await svc.adviseRoom('r1'); expect(advice.overText, true); expect(advice.overDisk, false); expect(await svc.selectableSpan('r1'), (10, 30)); expect(await svc.archiveRange('r1', 10, 20), 2); expect((await svc.adviseRoom('r1')).overText, false); probe.percent = 85; expect((await svc.adviseRoom('r1')).overDisk, true); expect(await svc.restoreRange('r1', 10, 20), 2); }); }); }