import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/core/sftp/sftp_remote.dart'; import 'package:jj_mkvmaker/core/sftp/sftp_server.dart'; import 'package:path/path.dart' as p; import 'support/sftp_server.dart'; /// 나누기 sftp 약속 시험: lib/core/sftp/ 모듈마다 맨 위 약속 - 실제 서버 (tool/sftp_test_server.py, paramiko) · 실제 시험 폴더. /// 시험용 파이썬이 없으면 서버 시험은 건너뛴다 (sftp_test 와 같음) void main() { test('sftp_server: JSON 에 비밀 값 없음 · 지문 꼴 · 키 형식 · 암호 오류 종류', () { const s = SftpServer(id: 'a', name: '', host: 'nas', password: 'secret', privateKey: 'k'); final j = s.toJson(); expect(jsonEncode(j), isNot(contains('secret'))); expect((j['hasPassword'], j['hasKey'], s.label), (true, true, 'nas')); expect(SftpServer.fromJson({'id': 'x', 'port': 99999}).port, 22); expect(sshFingerprint(Uint8List.fromList(utf8.encode('SHA256:abc'))), 'SHA256:abc'); expect(sshFingerprint(Uint8List.fromList([1, 2, 3])), 'SHA256:AQID'); expect(() => checkPrivateKey('PuTTY-User-Key-File-3: ssh-ed25519'), throwsA(isA().having((e) => e.kind, 'kind', SftpFailure.keyFormat))); expect(() => parsePrivateKey('not a key', ''), throwsA(isA().having((e) => e.kind, 'kind', SftpFailure.keyFormat))); }); test('sftp_remote: 처음 보는 호스트 키는 지문과 함께 막음 → 믿으면 쓰기 · 목록 · 부분 읽기 · 옮기기 · 지우기 · 틀린 비밀번호는 auth', () async { final tmp = Directory.systemTemp.createTempSync('jj_sftpm_'); final root = Directory(p.join(tmp.path, 'root'))..createSync(); final srv = await SftpTestServer.start(root: root.path, hostKeyFile: p.join(tmp.path, 'host.key')); addTearDown(() async { await srv.stop(); tmp.deleteSync(recursive: true); }); SftpServer conf({String fp = '', String pw = 'pw'}) => SftpServer(id: 'm1', name: '', host: '127.0.0.1', port: srv.port, user: 'jj', password: pw, hostKeyFingerprint: fp, root: root.path); final first = SftpRemote(conf()); final e = await first.list('/').then((_) => null, onError: (Object x) => x); first.close(); expect(e, isA().having((x) => x.fingerprint, 'fp', srv.fingerprint).having((x) => x.changed, 'changed', isFalse)); final c = SftpRemote(conf(fp: srv.fingerprint)); addTearDown(c.close); await c.mkdirs('/가/나'); await c.write('/가/나/a.txt', Stream.value(utf8.encode('0123456789'))); expect(File(p.join(root.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(root.path, '가', 'b.txt')).existsSync(), isTrue); await c.delete('/가'); expect(Directory(p.join(root.path, '가')).existsSync(), isFalse); final bad = SftpRemote(conf(fp: srv.fingerprint, pw: 'wrong')); addTearDown(bad.close); await expectLater(bad.list('/'), throwsA(isA().having((x) => x.kind, 'kind', SftpFailure.auth))); }, skip: sftpTestServerAvailable ? false : '시험용 파이썬 없음', timeout: const Timeout(Duration(minutes: 3))); }