frindlymap/test/support/storage_contract.dart

238 lines
12 KiB
Dart

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<int> 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<Storage> 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);
});
});
}