JJ_MKVMaker/integration_test/windows_sshkey_shot_test.dart

139 lines
6.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<List<String>> pickFiles({required String title, required List<String> 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<String> 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<NavigatorState>();
final shotKey = GlobalKey();
await t.pumpWidget(RepaintBoundary(
key: shotKey,
child: MaterialApp(
theme: buildTheme(), debugShowCheckedModeBanner: false, navigatorKey: navKey, home: const Scaffold(body: SizedBox.expand())),
));
Future<void> settle(bool Function() done, {int rounds = 150}) async {
for (var i = 0; i < rounds && !done(); i++) {
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 40)));
await t.pump();
}
await t.pump(const Duration(milliseconds: 200));
}
Future<void> 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<void> pick(String file) async {
storage.next = file;
final b = find.byIcon(Icons.key_outlined);
await t.ensureVisible(b);
await t.tap(b);
}
Future<void> 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)));
}
}