import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/core/ftp/ftp_remote.dart'; import 'package:jj_mkvmaker/core/ftp/ftp_server.dart'; import 'package:jj_mkvmaker/core/ftp/ftp_session.dart'; import 'package:path/path.dart' as p; import 'support/sftp_server.dart' show sftpTestPython; /// 나누기 ftp 약속 시험: lib/core/ftp/ 모듈마다 맨 위 약속 - 실제 서버 (tool/ftp_test_server.py, pyftpdlib) · 실제 시험 폴더. /// 시험용 파이썬이 없으면 서버 시험은 건너뛴다 (ftp_test 와 같음) const _pw = 'ftp-Pw-7Rk2xQ9t'; void main() { final hasPy = File(sftpTestPython).existsSync(); late Directory tmp; Process? proc; late int port; late String fp; Future start(String mode) async { final pr = await Process.start(sftpTestPython, ['tool/ftp_test_server.py', '--root', tmp.path, '--mode', mode, '--password', _pw]); proc = pr; final lines = pr.stdout.transform(utf8.decoder).transform(const LineSplitter()).asBroadcastStream(); pr.stderr.drain(); port = int.parse((await lines.firstWhere((l) => l.startsWith('PORT ')).timeout(const Duration(seconds: 20))).split(' ')[1]); final f = (await lines.firstWhere((l) => l.startsWith('FINGERPRINT')).timeout(const Duration(seconds: 5))).split(' '); fp = f.length > 1 ? f[1] : ''; } setUp(() => tmp = Directory.systemTemp.createTempSync('jj_ftpm_')); tearDown(() async { final pr = proc; proc = null; if (pr != null) { pr.kill(); await pr.exitCode.timeout(const Duration(seconds: 5), onTimeout: () => -1); } for (var i = 0; tmp.existsSync(); i++) { try { tmp.deleteSync(recursive: true); } on FileSystemException { if (i > 30) rethrow; await Future.delayed(const Duration(milliseconds: 100)); } } }); FtpServer srv(String security, {String cert = '', String password = _pw}) => FtpServer(id: 'm1', name: '', host: '127.0.0.1', port: port, user: 'jj', password: password, security: security, certFingerprint: cert); Future resolve(String h) async => InternetAddress.tryParse(h); test('ftp_server: JSON 에 비밀 값 없음 · 모르는 값은 FTPS · 이름', () { const s = FtpServer(id: 'a', name: ' ', host: 'nas', password: 'secret', security: 'none'); expect(jsonEncode(s.toJson()), isNot(contains('secret'))); expect(FtpServer.fromJson(s.toJson()).security, 'none'); expect(FtpServer.fromJson({'id': 'x', 'security': '?'}).security, 'explicit'); expect((s.label, s.tls, s.copyWith(security: 'implicit').tls), ('nas', false, true)); }); test('ftp_session: 처음 보는 인증서는 지문과 함께 막음 → 믿으면 로그인 · 명령 · 줄바꿈 명령은 보내지 않음', () async { await start('explicit'); final first = await FtpSession.open(srv('explicit'), const Duration(seconds: 10), resolve); final e = await first.login().then((_) => null, onError: (Object x) => x); first.close(); expect(e, isA().having((x) => x.fingerprint, 'fingerprint', fp).having((x) => x.kind, 'kind', FtpFailure.certUnknown)); final s = await FtpSession.open(srv('explicit', cert: fp), const Duration(seconds: 10), resolve); addTearDown(s.close); await s.login(); expect(s.secure, isTrue); expect(s.home, '/'); expect((await s.cmd('NOOP')).$1, 200); expect(() => s.send('DELE a\r\nDELE b'), throwsA(isA())); }, skip: hasPy ? false : '시험용 파이썬 없음'); test('ftp_remote: 쓰기 · 목록 · 정보 · 부분 읽기 · 옮기기 · 지우기 (평문) · 틀린 비밀번호는 auth', () async { await start('plain'); final c = FtpRemote(srv('none')); addTearDown(c.close); await c.mkdirs('/가/나'); await c.write('/가/나/a.txt', Stream.value(utf8.encode('0123456789'))); expect(File(p.join(tmp.path, '가', '나', 'a.txt')).readAsStringSync(), '0123456789'); expect((await c.list('/가/나')).map((x) => (x.rel, x.size)), [('/가/나/a.txt', 10)]); expect(await c.stat('/없음'), isNull); final part = await (await c.openReadFrom('/가/나/a.txt', offset: 4, length: 3)).expand((x) => x).toList(); expect(utf8.decode(part), '456'); await c.move('/가/나/a.txt', '/가/b.txt'); expect(File(p.join(tmp.path, '가', 'b.txt')).existsSync(), isTrue); await c.delete('/가'); expect(Directory(p.join(tmp.path, '가')).existsSync(), isFalse); final bad = FtpRemote(srv('none', password: 'wrong-pw')); addTearDown(bad.close); await expectLater(bad.list('/'), throwsA(isA().having((x) => x.kind, 'kind', FtpFailure.auth))); }, skip: hasPy ? false : '시험용 파이썬 없음'); }