JJ_MKVMaker/test/vault_page_test.dart

211 lines
11 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/master_lock.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/services/secret_store.dart';
import 'package:jj_mkvmaker/ui/explorer_page.dart';
import 'package:jj_mkvmaker/ui/vault_page.dart';
import 'package:path/path.dart' as p;
/// X-plore 비교 5-2 암호 보관함 화면 (관리자 결정 10/21): 만들기 창의 알림 · 넣기 (복사 / 원본 지우기) · 잠금 · 틀린 비밀번호 ·
/// 꺼내기 · 지우기 · [마스터로도 열기] (마스터를 초기화하면 그 길만 없어짐)
void main() {
late Directory tmp;
final was = vaultDirOf;
final wasPick = vaultPickFolder;
setUp(() {
tmp = Directory.systemTemp.createTempSync('jj_vaultui_');
vaultDirOf = () async => Directory(p.join(tmp.path, 'vault'));
vaultPickFolder = () async => p.join(tmp.path, 'out');
Directory(p.join(tmp.path, 'out')).createSync();
vaultIterationsForTest = 1000;
});
tearDown(() {
vaultDirOf = was;
vaultPickFolder = wasPick;
MasterLock.instance = null;
tmp.deleteSync(recursive: true);
});
AppController ctl() {
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings.vaultEnabled = true;
return c;
}
Future<void> until(WidgetTester t, bool Function() ok, {int rounds = 200}) async {
for (var i = 0; i < rounds && !ok(); i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
await t.pump();
}
// 닫히는 창 · 메뉴의 막 (barrier) 이 사라질 때까지
for (var i = 0; i < 8; i++) {
await t.pump(const Duration(milliseconds: 50));
}
}
/// 진행 막대 · 글자 칸 깜빡임이 있으면 pumpAndSettle 이 끝나지 않는다 - 정한 만큼만
Future<void> settle(WidgetTester t) async {
for (var i = 0; i < 12; i++) {
await t.pump(const Duration(milliseconds: 50));
}
}
Future<void> menu(WidgetTester t, String row, String item) async {
// 꺼내기 · 넣기 중에는 메뉴 단추가 꺼져 있다
await until(t, () => find.byType(LinearProgressIndicator).evaluate().isEmpty);
await t.tap(find.descendant(of: find.widgetWithText(ListTile, row), matching: find.byType(PopupMenuButton<String>)));
await settle(t);
await t.tap(find.text(item).last);
await t.pump();
}
testWidgets('만들기 (알림 · 체크해야 켜짐) → 넣기 (원본 지우기 체크 → 탐색기 지우기 부름) → 잠금 → 틀린 비밀번호 → 열기 → 꺼내기 → 지우기', (t) async {
t.view.physicalSize = const Size(900, 1400);
t.view.devicePixelRatio = 1;
addTearDown(t.view.reset);
final src = File(p.join(tmp.path, '비밀 문서.pdf'))..writeAsBytesSync(List.generate(3000, (i) => i % 256));
final deleted = <List<String>>[];
final c = ctl();
await t.pumpWidget(MaterialApp(
home: VaultPage(c: c, add: [src.path], deleteOriginals: (paths) async => deleted.add(paths))));
await until(t, () => find.text('새 보관함 만들기').evaluate().isNotEmpty);
expect(find.textContaining('다른 앱도 이 폴더의 암호화된 파일을 볼 수 있습니다'), findsOneWidget);
expect(find.textContaining('이 폴더를 지우면 보관함도 사라집니다'), findsOneWidget);
expect(find.textContaining('되살릴 수 없습니다'), findsWidgets);
final fields = find.byType(TextField);
await t.enterText(fields.at(0), 'pw1');
await t.enterText(fields.at(1), 'pw1');
await t.pump();
expect(t.widget<FilledButton>(find.widgetWithText(FilledButton, '만들기')).onPressed, isNull, reason: '체크해야 켜짐');
await t.tap(find.text('비밀번호를 잊으면 되살릴 수 없음을 압니다'));
await t.pump();
await t.tap(find.widgetWithText(FilledButton, '만들기'));
await until(t, () => find.textContaining('보관함에 넣기 ·').evaluate().isNotEmpty);
// 넣기 창: 원본 지우기 (처음 끔) 를 켜고 넣기
expect(t.widget<CheckboxListTile>(find.byType(CheckboxListTile)).value, isFalse, reason: '처음은 복사');
await t.tap(find.text('원본 지우기 (이동)'));
await t.pump();
await t.tap(find.widgetWithText(FilledButton, '넣기'));
await until(t, () => deleted.isNotEmpty);
expect(deleted.single, [src.path], reason: '다 넣은 뒤 탐색기의 지우기로');
expect(find.text('비밀 문서.pdf'), findsOneWidget);
expect(Directory(p.join(tmp.path, 'vault')).listSync().map((f) => p.basename(f.path)).join(), isNot(contains('비밀')));
// 잠금 → 틀린 비밀번호 → 맞는 비밀번호
await t.tap(find.byTooltip('잠그기'));
await t.pump();
await t.enterText(find.byType(TextField), 'wrong');
await t.tap(find.widgetWithText(FilledButton, '열기'));
await until(t, () => find.text('비밀번호가 틀렸습니다').evaluate().isNotEmpty);
expect(find.text('비밀번호가 틀렸습니다'), findsOneWidget);
await t.enterText(find.byType(TextField), 'pw1');
await t.tap(find.widgetWithText(FilledButton, '열기'));
await until(t, () => find.text('비밀 문서.pdf').evaluate().isNotEmpty);
// 꺼내기 (원본과 같은 내용)
await menu(t, '비밀 문서.pdf', '꺼내기');
await until(t, () => File(p.join(tmp.path, 'out', '비밀 문서.pdf')).existsSync());
expect(File(p.join(tmp.path, 'out', '비밀 문서.pdf')).readAsBytesSync(), src.readAsBytesSync());
// 지우기 (확인)
await menu(t, '비밀 문서.pdf', '보관함에서 지우기');
await settle(t);
await t.tap(find.widgetWithText(FilledButton, '지우기'));
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
expect(find.text('보관함이 비어 있습니다'), findsOneWidget);
await t.pumpWidget(const SizedBox());
});
testWidgets('틀린 비밀번호 5번이면 30초 기다림 · [마스터로도 열기] 켜기 → 마스터로 열기 · 마스터를 초기화하면 그 길만 없어짐', (t) async {
t.view.physicalSize = const Size(900, 1400);
t.view.devicePixelRatio = 1;
addTearDown(t.view.reset);
final secrets = MemorySecretStore();
final lock = MasterLock(secrets);
MasterLock.instance = lock;
await t.runAsync(() => lock.setMaster('master'));
final c = ctl();
// 먼저 만들어 둔다
await t.pumpWidget(MaterialApp(home: VaultPage(c: c)));
await until(t, () => find.text('새 보관함 만들기').evaluate().isNotEmpty);
await t.enterText(find.byType(TextField).at(0), 'pw');
await t.enterText(find.byType(TextField).at(1), 'pw');
await t.tap(find.text('비밀번호를 잊으면 되살릴 수 없음을 압니다'));
await t.pump();
await t.tap(find.widgetWithText(FilledButton, '만들기'));
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
// 마스터로도 열기 켜기
await t.tap(find.descendant(of: find.byType(AppBar), matching: find.byType(PopupMenuButton<String>)));
await settle(t);
await t.tap(find.text('마스터로도 열기'));
await settle(t);
expect(find.textContaining('마스터를 초기화하면 이 길만 없어지고'), findsOneWidget);
await t.tap(find.widgetWithText(FilledButton, '켜기'));
await until(t, () => c.settings.vaultMasterCopy);
expect((await t.runAsync(secrets.readAll))!.containsKey(MasterLock.vaultCopyKey), isTrue);
// 잠그고 마스터로 열기
await t.tap(find.byTooltip('잠그기'));
await t.pump();
await t.tap(find.text('마스터 비밀번호로 열기'));
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
expect(find.text('보관함이 비어 있습니다'), findsOneWidget);
// 마스터 초기화 → 그 길만 없어짐 (보관함 비밀번호로는 열림)
await t.runAsync(lock.clearMaster);
expect((await t.runAsync(secrets.readAll))!.containsKey(MasterLock.vaultCopyKey), isFalse);
await t.tap(find.byTooltip('잠그기'));
await t.pump();
await t.tap(find.text('마스터 비밀번호로 열기'));
await until(t, () => find.textContaining('마스터로 여는 열쇠가 없습니다').evaluate().isNotEmpty);
expect(find.textContaining('마스터로 여는 열쇠가 없습니다'), findsOneWidget);
// 틀린 비밀번호 5번
for (var i = 0; i < 5; i++) {
await t.enterText(find.byType(TextField), 'nope$i');
await t.tap(find.widgetWithText(FilledButton, '열기'));
await t.pump();
// 여는 중 (비밀번호 확인) 이 끝나 다시 잠금 화면이 될 때까지
await until(t, () => find.byType(TextField).evaluate().isNotEmpty);
}
expect(find.textContaining('30초 뒤에 다시'), findsOneWidget);
expect(t.widget<FilledButton>(find.widgetWithText(FilledButton, '열기')).onPressed, isNull);
await t.pump(const Duration(seconds: 31));
await t.enterText(find.byType(TextField), 'pw');
await t.tap(find.widgetWithText(FilledButton, '열기'));
await until(t, () => find.text('보관함이 비어 있습니다').evaluate().isNotEmpty);
expect(find.text('보관함이 비어 있습니다'), findsOneWidget);
await t.pumpWidget(const SizedBox());
});
testWidgets('파일 탐색기: [보관함에 넣기] 는 보관함을 켰을 때 · 이 기기의 파일에만 (폴더는 2단계)', (t) async {
t.view.physicalSize = const Size(1400, 900);
t.view.devicePixelRatio = 1;
addTearDown(t.view.reset);
final dir = Directory(p.join(tmp.path, 'files'))..createSync();
File(p.join(dir.path, 'a.txt')).writeAsStringSync('a');
Directory(p.join(dir.path, 'sub')).createSync();
for (final on in [false, true]) {
final c = ctl()
..settings.vaultEnabled = on
..settings.explorerPaths = [dir.path, dir.path]
..settings.explorerLayout = 'split';
await t.runAsync(() => t.pumpWidget(MaterialApp(home: ExplorerPage(key: UniqueKey(), c: c))));
await until(t, () => find.text('a.txt').evaluate().isNotEmpty);
await t.longPress(find.text('a.txt').last);
await settle(t);
expect(find.text('보관함에 넣기'), on ? findsOneWidget : findsNothing, reason: '켬 = $on');
await t.tapAt(const Offset(5, 5));
await settle(t);
if (on) {
await t.longPress(find.text('sub').last);
await settle(t);
expect(find.text('보관함에 넣기'), findsNothing, reason: '폴더는 아직');
await t.tapAt(const Offset(5, 5));
await settle(t);
}
await t.pumpWidget(const SizedBox());
await t.pump(const Duration(seconds: 1));
}
});
}