diff --git a/test/ftp_test.dart b/test/ftp_test.dart index c8a6348..a36395b 100644 --- a/test/ftp_test.dart +++ b/test/ftp_test.dart @@ -10,6 +10,9 @@ import 'package:jj_mkvmaker/services/secret_store.dart'; import 'package:path/path.dart' as p; import 'support/sftp_server.dart' show sftpTestPython; +import 'package:jj_mkvmaker/core/file_ops.dart'; +import 'package:jj_mkvmaker/core/rsync_filter.dart'; +import 'package:jj_mkvmaker/core/sync_preview.dart'; /// l (관리자 지시 10/17): FTP · FTPS (dart:io 로 직접). 실제 서버 시험은 tool/ftp_test_server.py (pyftpdlib) - /// 시험용 파이썬 (sftp 시험과 같은 venv) 에 pyftpdlib 가 없으면 건너뛴다 @@ -161,6 +164,76 @@ void main() { expect(bad, isA().having((x) => x.changed, 'changed', isTrue)); }, skip: hasPy ? false : '시험용 파이썬 없음'); + test('관리자 지시 (10/19): FTP 맞추기도 거르기 5가지를 따르고 미리 보기 = 실제', () async { + await start('plain'); + FtpRegistry.configure([srv(security: 'none')]); + final src = Directory(p.join(tmp.path, 'fsrc')); + void fresh() { + for (final d in [src, Directory(p.join(root.path, 'dst'))]) { + if (d.existsSync()) d.deleteSync(recursive: true); + } + void lf(String rel, String s) => File(p.join(src.path, rel)) + ..parent.createSync(recursive: true) + ..writeAsStringSync(s); + void rf(String rel, String s) => File(p.join(root.path, 'dst', rel)) + ..parent.createSync(recursive: true) + ..writeAsStringSync(s); + lf('a.txt', 'a'); + lf('skip.tmp', 's'); + lf('cache/c.bin', 'c'); + lf('keep/x.txt', 'x'); + lf('keep/y.log', 'y'); + rf('only.tmp', 'o'); + rf('old.txt', 'old'); + rf('cache/remote.bin', 'r'); + rf('gone/g.tmp', 'g'); + rf('gone/g.txt', 'g'); + } + + String real(String rel) => p.joinAll([root.path, 'dst', ...rel.split('/').where((s) => s.isNotEmpty)]); + for (final (opts, delete) in [ + ('-av --exclude=*.tmp --exclude=cache/', true), + ('-av --exclude=*.tmp --exclude=cache/ --delete-excluded', true), + ('-av --include=keep/ --include=keep/*.txt --exclude=keep/* --exclude=*.tmp', true), + ('-av --filter="- *.log" --filter="- /cache"', true), + ('-av --exclude=*.tmp', false), + ]) { + fresh(); + final filter = RsyncFilter.parse(opts); + final before = {}; + void scan(Directory d, String rel) { + for (final e in d.listSync()) { + final r = rel.isEmpty ? p.basename(e.path) : '$rel/${p.basename(e.path)}'; + before.add(r); + if (e is Directory) scan(e, r); + } + } + + scan(Directory(real('')), ''); + final pv = await previewSync(src.path, 'ftp://f1/dst', toRight: true, delete: delete, filter: filter); + await FileOps().mirror(src.path, 'ftp://f1/dst', delete: delete, filter: filter); + for (final x in pv) { + final there = FileSystemEntity.typeSync(real(x.rel)) != FileSystemEntityType.notFound; + if (x.action == PreviewAction.add || x.action == PreviewAction.update) expect(there, isTrue, reason: '+/~ ${x.rel} ($opts)'); + if (x.action == PreviewAction.delete) expect(there, isFalse, reason: '− ${x.rel} ($opts)'); + } + final shown = {for (final x in pv) if (x.action == PreviewAction.delete) x.rel}; + for (final r in before) { + if (FileSystemEntity.typeSync(real(r)) == FileSystemEntityType.notFound) { + expect(shown.any((d) => r == d || r.startsWith('$d/')), isTrue, reason: '실제로 지워진 $r 는 미리 보기에 ($opts)'); + } + } + } + // 하나 자세히: 받는 쪽의 거른 것 지키기 · 거른 것이 든 폴더는 남기고 안의 것만 + fresh(); + await FileOps().mirror(src.path, 'ftp://f1/dst', delete: true, filter: RsyncFilter.parse('--exclude=*.tmp --exclude=cache/')); + expect(File(real('skip.tmp')).existsSync(), isFalse); + expect(File(real('only.tmp')).existsSync(), isTrue); + expect(File(real('cache/remote.bin')).existsSync(), isTrue); + expect(File(real('old.txt')).existsSync(), isFalse); + expect((File(real('gone/g.tmp')).existsSync(), File(real('gone/g.txt')).existsSync()), (true, false)); + }, skip: hasPy ? false : '시험용 파이썬 없음'); + test('동영상 스트리밍 다리: ftp:// 경로의 부분 읽기 (FtpRemote.openReadFrom - REST 로 그 자리부터)', () async { await start('plain'); FtpRegistry.configure([srv(security: 'none')]); diff --git a/test/smb_test.dart b/test/smb_test.dart index 9effde5..2765a5f 100644 --- a/test/smb_test.dart +++ b/test/smb_test.dart @@ -9,6 +9,9 @@ import 'package:jj_mkvmaker/core/vfs.dart'; import 'package:jj_mkvmaker/services/secret_store.dart'; import 'package:jj_mkvmaker/ui/smb_settings.dart'; import 'package:path/path.dart' as p; +import 'package:jj_mkvmaker/core/file_ops.dart'; +import 'package:jj_mkvmaker/core/rsync_filter.dart'; +import 'package:jj_mkvmaker/core/sync_preview.dart'; /// 88: Android SMB (smbj) 의 Dart 쪽 - 플랫폼 채널을 가짜 서버로 (실제 smbj · 서버 연결은 Android 기기 확인 몫) class _FakeSmb { @@ -227,4 +230,37 @@ void main() { expect(fake.lastArgs['requireSecure'], isTrue); expect(e, isA().having((x) => x.kind, 'kind', SmbFailure.insecure).having((x) => x.message, 'm', contains('SMB 서명 · 암호화 필수'))); }); + test('관리자 지시 (10/19): SMB 맞추기 (_mirrorV) 도 거르기를 따르고 미리 보기 = 실제 (가짜 채널)', () async { + SmbRegistry.configure(const [server]); + final tmp = Directory.systemTemp.createTempSync('jj_smbfilter_'); + addTearDown(() => tmp.deleteSync(recursive: true)); + void lf(String rel, String s) => File(p.join(tmp.path, rel)) + ..parent.createSync(recursive: true) + ..writeAsStringSync(s); + lf('a.txt', 'a'); + lf('skip.tmp', 's'); + lf('cache/c.bin', 'c'); + fake.files.addAll({ + 'dst': null, + r'dst\only.tmp': utf8.encode('o'), + r'dst\old.txt': utf8.encode('old'), + r'dst\cache': null, + r'dst\cache\remote.bin': utf8.encode('r'), + r'dst\gone': null, + r'dst\gone\g.tmp': utf8.encode('g'), + r'dst\gone\g.txt': utf8.encode('g'), + }); + final filter = RsyncFilter.parse('-av --exclude=*.tmp --exclude=cache/'); + final pv = await previewSync(tmp.path, 'smb://m1/dst', toRight: true, delete: true, filter: filter); + expect({for (final x in pv) if (x.action == PreviewAction.delete) x.rel}, {'old.txt', 'gone/g.txt'}); + expect({for (final x in pv) if (x.action == PreviewAction.add) x.rel}, {'a.txt'}); + await FileOps().mirror(tmp.path, 'smb://m1/dst', delete: true, filter: filter); + expect(fake.files.containsKey(r'dst\a.txt'), isTrue); + expect(fake.files.containsKey(r'dst\skip.tmp'), isFalse, reason: '거른 것은 보내지 않음'); + expect(fake.files.containsKey(r'dst\cache\c.bin'), isFalse); + expect(fake.files.containsKey(r'dst\only.tmp'), isTrue, reason: '받는 쪽의 거른 것은 지킴'); + expect(fake.files.containsKey(r'dst\cache\remote.bin'), isTrue); + expect(fake.files.containsKey(r'dst\old.txt'), isFalse); + expect((fake.files.containsKey(r'dst\gone\g.tmp'), fake.files.containsKey(r'dst\gone\g.txt')), (true, false)); + }); }