140 lines
6.9 KiB
Dart
140 lines
6.9 KiB
Dart
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));
|
||
// 관리자 지시 (10/24): 폴더 이름 · 포트 · 서버 키 지문을 실행마다 같게 (캡처가 흔들리지 않게 - 시험 쪽에서만)
|
||
final tmp = testFixedTempDir('jj_sshkeyshot_fixed');
|
||
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', fixedPort: 42022, fixedHostKey: true);
|
||
});
|
||
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)));
|
||
}
|
||
}
|