import 'package:flutter_test/flutter_test.dart'; import 'package:friendly_map/modules/core/core.dart'; import 'package:friendly_map/modules/groups/groups.dart'; void main() { group('그룹 목록', () { test('처음에는 맨 위에 가족·학교·회사·그룹·여행 네 개', () { final b = GroupBook(); expect(b.roots.map((g) => g.name), ['가족', '학교', '회사', '그룹·여행']); expect(b.roots.map((g) => g.kind), [CardKinds.family, CardKinds.school, CardKinds.company, CardKinds.group]); }); test('처음 하위 그룹: 학교는 초등~대학원, 가족은 친가·외가·처가·시가·촌수 모름', () { final b = GroupBook(); expect(b.children(CardKinds.school).map((g) => g.name), ['초등학교', '중학교', '고등학교', '대학교', '대학원']); expect(b.children(CardKinds.family).map((g) => g.name), ['친가', '외가', '처가·시가', '친척(촌수 모름)']); expect(b.byId(Group.unknownRelatives)!.parent, CardKinds.family); expect(b.children(CardKinds.company), isEmpty); expect(b.groups.indexOf(b.byId('school.high')!), greaterThan(b.groups.indexOf(b.byId(CardKinds.school)!))); }); test('더하기·이름 바꾸기·지우기, 빈 이름과 겹치는 이름은 거절', () { final b = GroupBook(); final g = b.add(' 동호회 ', CardKinds.group)!; expect(g.name, '동호회'); expect(b.add('', CardKinds.group), isNull); expect(b.add('가족', CardKinds.family), isNull); expect(b.rename(g.id, '등산 모임'), isTrue); expect(b.rename(g.id, '학교'), isFalse); expect(b.rename(g.id, '등산 모임'), isTrue, reason: '자기 이름 그대로는 괜찮다'); expect(b.byId(g.id)!.name, '등산 모임'); expect(b.remove(g.id), {g.id}); expect(b.byId(g.id), isNull); expect(b.remove('없음'), isEmpty); }); test('이름은 20자까지 자른다', () { final b = GroupBook(); expect(b.add('가' * 30, CardKinds.group)!.name.length, 20); }); test('순서 바꾸기', () { final b = GroupBook(); b.move(CardKinds.group, 0); expect(b.groups.first.id, CardKinds.group); b.move(CardKinds.group, 99); expect(b.groups.last.id, CardKinds.group); }); test('구성원 더하기·빼기, 사람이 든 그룹, 사람 지우기', () { final b = GroupBook(); expect(b.addMembers(CardKinds.family, ['a', 'b']), 2); expect(b.addMembers(CardKinds.family, ['b', 'c']), 1, reason: '이미 든 사람은 다시 세지 않는다'); b.addMembers(CardKinds.school, ['a']); expect(b.groupsOf('a').map((g) => g.id), [CardKinds.family, CardKinds.school]); expect(b.removeMember(CardKinds.family, 'c'), isTrue); expect(b.removeMember(CardKinds.family, 'c'), isFalse); b.forgetPerson('a'); expect(b.groupsOf('a'), isEmpty); expect(b.byId(CardKinds.family)!.members, {'b'}); }); test('잠긴 그룹이 닫혀 있으면 구성원을 숨긴다 (다른 그룹에도 있어도)', () async { final b = GroupBook(); b.addMembers(CardKinds.family, ['a', 'b']); b.addMembers(CardKinds.school, ['a', 'c']); b.setLock(CardKinds.family, await Pin.make('1234', iterations: 10)); expect(b.hiddenPeople((g) => true), {'a', 'b'}); expect(b.hiddenPeople((g) => false), isEmpty); }); test('글자로 저장했다 되살리면 같고, 깨진 글자는 처음 그룹', () async { final b = GroupBook(relockMinutes: 10); final g = b.add('동호회', CardKinds.group)!; b.addMembers(g.id, ['x']); b.setLock(g.id, await Pin.make('abcd', iterations: 10)); final c = GroupBook.fromJson(b.toJson()); expect(c.groups.map((x) => x.name), b.groups.map((x) => x.name)); expect(c.byId(g.id)!.members, {'x'}); expect(c.byId(g.id)!.locked, isTrue); expect(c.relockMinutes, 10); expect(c.add('새 그룹', CardKinds.group)!.id, isNot(g.id), reason: '새 번호표는 겹치지 않는다'); expect(GroupBook.fromJson('{깨짐').roots.length, 4); expect(GroupBook.fromJson(null).roots.length, 4); }); }); group('비밀번호', () { test('맞는 비밀번호만 통과, 원문은 남지 않는다', () async { final h = await Pin.make('2580', iterations: 100); expect(await Pin.verify('2580', h), isTrue); expect(await Pin.verify('2581', h), isFalse); expect(h.toJson().toString(), isNot(contains('2580'))); final again = await Pin.make('2580', iterations: 100); expect(again.salt, isNot(h.salt), reason: '같은 비밀번호도 소금이 달라 다른 값'); }); test('짧거나 길거나 앞뒤 빈칸이면 안내', () { expect(Pin.problem('123'), isNotNull); expect(Pin.problem('1' * 17), isNotNull); expect(Pin.problem(' 1234'), isNotNull); expect(Pin.problem('1234'), isNull); }); test('깨진 저장값은 통과하지 않는다', () async { expect(await Pin.verify('1234', const PinHash('!!', '??', 10)), isFalse); expect(PinHash.fromJson({'salt': 1}), isNull); }); }); group('여러 번 틀리면 기다리기', () { test('5번까지는 바로, 그 뒤로 30초·60초…', () { final g = UnlockGuard(); for (var i = 0; i < 4; i++) { g.failure(0); expect(g.waitLeft(0), 0); } expect(g.triesLeft(), 1); g.failure(0); expect(g.waitLeft(0), 3000); expect(g.waitLeft(3000), 0); g.failure(3000); expect(g.waitLeft(3000), 6000); g.success(); expect(g.waitLeft(3000), 0); expect(g.triesLeft(), 5); }); test('기다림은 1시간을 넘지 않는다', () { final g = UnlockGuard(); for (var i = 0; i < 40; i++) { g.failure(0); } expect(g.waitLeft(0), 360000); }); }); group('하위 그룹', () { test('회사 > 한빛전자 > 결제팀, 같은 이름은 부모가 다르면 괜찮다', () { final b = GroupBook(); final hb = b.add('한빛전자', CardKinds.company, parent: CardKinds.company)!; final pay = b.add('결제팀', CardKinds.company, parent: hb.id)!; final other = b.add('누리소프트', CardKinds.company, parent: CardKinds.company)!; expect(b.add('결제팀', CardKinds.company, parent: other.id), isNotNull); expect(b.add('결제팀', CardKinds.company, parent: hb.id), isNull); expect(b.path(pay.id), '회사 › 한빛전자 › 결제팀'); expect(b.depth(pay.id), 3); expect(b.ancestors(pay.id).map((g) => g.id), [hb.id, CardKinds.company]); expect(b.add('없는 부모 아래', CardKinds.group, parent: '없음'), isNull); }); test('4단까지만 만들 수 있다', () { final b = GroupBook(); final a = b.add('2단', CardKinds.group, parent: CardKinds.group)!; final c = b.add('3단', CardKinds.group, parent: a.id)!; final d = b.add('4단', CardKinds.group, parent: c.id)!; expect(b.add('5단', CardKinds.group, parent: d.id), isNull); }); test('하위까지 합친 구성원, 지우면 하위 그룹도 함께', () { final b = GroupBook(); b.addMembers(CardKinds.school, ['a']); b.addMembers('school.high', ['b', 'a']); b.addMembers('school.university', ['c']); expect(b.membersDeep(CardKinds.school), {'a', 'b', 'c'}); expect(b.remove(CardKinds.school), containsAll([CardKinds.school, 'school.high', 'school.graduate'])); expect(b.byId('school.high'), isNull); expect(b.roots.length, 3); }); test('잠긴 부모가 닫히면 하위 그룹 사람도 숨긴다', () async { final b = GroupBook(); b.addMembers('family.maternal', ['m']); b.setLock(CardKinds.family, await Pin.make('1234', iterations: 10)); expect(b.hiddenPeople((g) => true), {'m'}); }); test('같은 부모 아래에서만 순서가 바뀐다', () { final b = GroupBook(); b.move('school.graduate', 0); expect(b.children(CardKinds.school).first.id, 'school.graduate'); expect(b.roots.first.id, CardKinds.family); }); test('저장하면 나무 모양이 유지되고, 없는 부모·돌고 도는 부모는 맨 위로', () { final b = GroupBook(); final hb = b.add('한빛전자', CardKinds.company, parent: CardKinds.company)!; final c = GroupBook.fromJson(b.toJson()); expect(c.byId(hb.id)!.parent, CardKinds.company); const bad = '{"groups":[{"id":"x","name":"X","parent":"y"},{"id":"y","name":"Y","parent":"x"},{"id":"z","name":"Z","parent":"없음"}]}'; final d = GroupBook.fromJson(bad); expect(d.groups.map((g) => g.id).toSet(), {'x', 'y', 'z'}); expect(d.byId('z')!.parent, isNull); }); }); test('이전 구성원: 흐리게 남기고, 다시 넣으면 구성원으로, 잠긴 그룹이 닫히면 숨김, 저장 유지', () async { final b = GroupBook(); b.addMembers(CardKinds.company, ['a']); expect(b.markFormer(CardKinds.company, 'a'), isTrue); expect(b.markFormer(CardKinds.company, 'a'), isFalse); expect(b.byId(CardKinds.company)!.members, isEmpty); expect(b.byId(CardKinds.company)!.formers, {'a'}); expect(GroupBook.fromJson(b.toJson()).byId(CardKinds.company)!.formers, {'a'}); b.setLock(CardKinds.company, await Pin.make('1234', iterations: 10)); expect(b.hiddenPeople((g) => true), {'a'}); b.addMembers(CardKinds.company, ['a']); expect(b.byId(CardKinds.company)!.formers, isEmpty); b.markFormer(CardKinds.company, 'a'); expect(b.removeFormer(CardKinds.company, 'a'), isTrue); b.addMembers(CardKinds.school, ['z']); b.markFormer(CardKinds.school, 'z'); b.forgetPerson('z'); expect(b.byId(CardKinds.school)!.formers, isEmpty); }); }