import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/app/i18n_controller.dart'; import 'package:jj_mkvmaker/core/sftp.dart'; import 'package:jj_mkvmaker/l10n/tr.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/ui/sftp_settings.dart'; import 'package:jj_mkvmaker/ui/theme.dart'; import 'package:path/path.dart' as p; import '../test/support/sftp_server.dart'; import 'support/temp_dir.dart'; /// [개인 키 파일 고르기] 가 고르는 파일을 시험이 정한다 (실제 파일 창은 열지 않음) class _PickStorage extends DesktopStorageService { String? next; @override Future> pickFiles({required String title, required List extensions, String? initialDirectory}) async => next == null ? const [] : [next!]; } /// 관리자 지시 (10/21): SFTP 키 로그인의 사용자 글 - 키 고른 뒤 안내 · 암호 필요 · 암호 틀림 · 서버 거부 · .ppk 안내 를 /// ko · en · ja × 412 로 찍는다. 실제 시험 서버 (paramiko) · ssh-keygen 으로 만든 시험 키만 쓴다 (시험 폴더 안, 설정은 메모리만) void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final out = Platform.environment['JJ_SHOT_DIR'] ?? ''; final keygen = r'C:\Windows\System32\OpenSSH\ssh-keygen.exe'; final skip = !sftpTestServerAvailable || !File(keygen).existsSync(); for (final lang in ['ko', 'en', 'ja']) { testWidgets('SSH 키 글 $lang 412x900', (t) async { t.view.physicalSize = const Size(412, 900); t.view.devicePixelRatio = 1; addTearDown(t.view.reset); await t.runAsync(() => i18n.apply(lang, save: false)); addTearDown(() => i18n.apply('ko', save: false)); final tmp = testTempDir('jj_sshkeyshot_'); final root = Directory(p.join(tmp.path, 'root'))..createSync(); Future gen(String name, String pass) async { final f = p.join(tmp.path, name); final r = await Process.run(keygen, ['-q', '-t', 'ed25519', '-N', pass, '-f', f, '-C', 'jj-test']); expect(r.exitCode, 0, reason: '${r.stderr}'); return f; } late SftpTestServer server; late String keyA, keyB; await t.runAsync(() async { keyA = await gen('a', 'kp'); keyB = await gen('b', ''); server = await SftpTestServer.start(root: root.path, hostKeyFile: p.join(tmp.path, 'hostkey'), password: null, pubkey: '$keyA.pub'); }); addTearDown(() => t.runAsync(server.stop)); final ppk = File(p.join(tmp.path, 'key.ppk')) ..writeAsStringSync('PuTTY-User-Key-File-3: ssh-ed25519\nEncryption: none\nComment: x\nPublic-Lines: 1\nAAAA\n'); final storage = _PickStorage(); final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: storage)); final old = SftpServer(id: 's1', name: 'NAS', host: '127.0.0.1', port: server.port, user: 'jj', hostKeyFingerprint: server.fingerprint); final navKey = GlobalKey(); final shotKey = GlobalKey(); await t.pumpWidget(RepaintBoundary( key: shotKey, child: MaterialApp( theme: buildTheme(), debugShowCheckedModeBanner: false, navigatorKey: navKey, home: const Scaffold(body: SizedBox.expand())), )); Future settle(bool Function() done, {int rounds = 150}) async { for (var i = 0; i < rounds && !done(); i++) { await t.runAsync(() => Future.delayed(const Duration(milliseconds: 40))); await t.pump(); } await t.pump(const Duration(milliseconds: 200)); } Future shot(String name, String expectText) async { final f = find.textContaining(expectText); await settle(() => f.evaluate().isNotEmpty); expect(f, findsOneWidget, reason: name); await t.ensureVisible(f); await settle(() => false, rounds: 5); expect(t.takeException(), isNull, reason: '$name 넘침'); if (out.isEmpty) return; final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary; final img = await t.runAsync(() => b.toImage()); final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png)); Directory(out).createSync(recursive: true); File(p.join(out, 'sshkey_${name}_${lang}_412.png')).writeAsBytesSync(bytes!.buffer.asUint8List()); } Future pick(String file) async { storage.next = file; final b = find.byIcon(Icons.key_outlined); await t.ensureVisible(b); await t.tap(b); } Future testConnection() async { final b = find.text(tr('연결 확인')); await t.ensureVisible(b); await t.tap(b); await settle(() => find.byType(LinearProgressIndicator).evaluate().isNotEmpty, rounds: 20); await settle(() => find.byType(LinearProgressIndicator).evaluate().isEmpty, rounds: 400); } unawaited(editSftpServer(navKey.currentContext!, c, old: old)); await settle(() => find.text(tr('연결 확인')).evaluate().isNotEmpty); // ① PuTTY 키를 고르면 바꾸는 법 await pick(ppk.path); await shot('1_ppk', 'Export OpenSSH key'); // ② 암호가 걸린 키를 고르면 "아래에 키 암호를" await pick(keyA); await shot('2_picked', trf('개인 키를 넣었습니다. 암호가 걸린 키라 위의 [{0}] 칸에 키 암호를 넣으세요 (원래 파일 경로는 저장하지 않습니다)', [tr('개인 키 암호')])); // ③ 키 암호 없이 [연결 확인] await testConnection(); await shot('3_needpass', tr('개인 키에 암호가 걸려 있습니다. 키 암호를 넣으세요')); // ④ 틀린 키 암호 await t.enterText(find.widgetWithText(TextField, tr('개인 키 암호')), 'nope'); await testConnection(); await shot('4_badpass', tr('개인 키의 암호가 맞지 않습니다')); // ⑤ 서버가 모르는 키 (짝 공개 키가 서버에 없음) await pick(keyB); await settle(() => false, rounds: 5); await testConnection(); await shot('5_rejected', 'authorized_keys'); navKey.currentState!.popUntil((r) => r.isFirst); await settle(() => false, rounds: 10); expect(c.settings.remote.sftpServers, isEmpty, reason: '[저장] 은 누르지 않음'); }, skip: skip, timeout: const Timeout(Duration(minutes: 3))); } }