import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/services/secret_store.dart'; import 'package:path/path.dart' as p; /// 54 · 130 (관리자 지시 10/17): Windows 안전 저장소 (DPAPI) 를 실제로 쓰고 읽고 지운다 - 보통 회귀 시험에 들어가게. /// 임시 폴더의 secrets.dat 만 쓴다 (사용자의 앱 데이터 폴더 secrets.dat 는 건드리지 않음 - integration_test 의 예전 시험은 그것을 썼다). void main() { late Directory tmp; final userFile = Platform.isWindows ? File(p.join(Platform.environment['APPDATA'] ?? '', 'com.jj', 'jj_mkvmaker', 'secrets.dat')) : null; (DateTime, int)? userBefore; setUp(() { tmp = Directory.systemTemp.createTempSync('jj_secret_'); final f = userFile; userBefore = f != null && f.existsSync() ? (f.lastModifiedSync(), f.lengthSync()) : null; }); tearDown(() { // 사용자의 secrets.dat 는 그대로여야 한다 (있으면 바뀐 때 · 크기가 같음) final f = userFile; if (f != null && userBefore != null) { expect((f.lastModifiedSync(), f.lengthSync()), userBefore, reason: '사용자 secrets.dat 를 건드리면 안 됨'); } try { tmp.deleteSync(recursive: true); } catch (_) {} }); test('쓰기 · 읽기 · 바꾸기 · 지우기 - 파일은 암호화되어 평문이 없다', () async { final s = SecretStore.dpapiAt(tmp.path); expect(await s.readAll(), isEmpty); await s.write('jj.selftest', '비밀-1'); expect((await s.readAll())['jj.selftest'], '비밀-1'); final file = File(p.join(tmp.path, 'secrets.dat')); expect(file.existsSync(), isTrue); final raw = file.readAsBytesSync(); expect(String.fromCharCodes(raw).contains('jj.selftest'), isFalse, reason: 'DPAPI 로 암호화 - 열쇠 이름도 보이지 않음'); await s.write('jj.selftest', '비밀-2'); expect((await SecretStore.dpapiAt(tmp.path).readAll())['jj.selftest'], '비밀-2', reason: '새로 연 저장소도 같은 값'); await s.delete('jj.selftest'); expect((await s.readAll()).containsKey('jj.selftest'), isFalse); expect(File('${file.path}.tmp').existsSync(), isFalse, reason: '임시 파일이 남지 않음'); }, skip: Platform.isWindows ? false : 'Windows 만 (DPAPI)'); test('130: 두 창 (저장소 두 개) 이 번갈아 써도 서로의 값을 지우지 않는다', () async { const a = 'jj.selftest.a', b = 'jj.selftest.b'; final first = SecretStore.dpapiAt(tmp.path), second = SecretStore.dpapiAt(tmp.path); await first.readAll(); // 첫 창이 읽어 둠 await second.write(b, '두 번째 창'); await first.write(a, '첫 창'); // 첫 창이 쓸 때 두 번째 창의 값을 지우면 안 된다 final now = await SecretStore.dpapiAt(tmp.path).readAll(); expect(now[a], '첫 창'); expect(now[b], '두 번째 창'); await second.delete(b); final after = await first.readAll(); expect(after[a], '첫 창'); expect(after.containsKey(b), isFalse); }, skip: Platform.isWindows ? false : 'Windows 만 (DPAPI)'); test('풀 수 없는 파일 (다른 PC · 사용자에서 옮겨 옴) 은 지우지 않고 보관한 뒤 새로 시작', () async { final file = File(p.join(tmp.path, 'secrets.dat'))..writeAsBytesSync([1, 2, 3, 4, 5]); final s = SecretStore.dpapiAt(tmp.path); expect(await s.readAll(), isEmpty); expect(s.lostCopy, isNotNull); expect(File(s.lostCopy!).readAsBytesSync(), [1, 2, 3, 4, 5]); expect(p.basename(s.lostCopy!), startsWith('secrets.broken-')); expect(file.readAsBytesSync(), [1, 2, 3, 4, 5], reason: '새로 쓰기 전에는 원래 파일 그대로'); }, skip: Platform.isWindows ? false : 'Windows 만 (DPAPI)'); }