JJ_MKVMaker/test/ftp_modules_contract_test.dart

99 lines
4.7 KiB
Dart

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<void> 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<void>();
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<void>.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<InternetAddress?> 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<Object?>((_) => null, onError: (Object x) => x);
first.close();
expect(e, isA<FtpCertException>().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<FtpException>()));
}, 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<FtpException>().having((x) => x.kind, 'kind', FtpFailure.auth)));
}, skip: hasPy ? false : '시험용 파이썬 없음');
}