From c27eb9b59e0b98891066fee85c4f0ffa71cbe722 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:59:15 +0900 Subject: [PATCH] =?UTF-8?q?modular=5Fsync=20#6=20F11=20=EB=82=98=EB=A8=B8?= =?UTF-8?q?=EC=A7=80:=20=EC=9B=90=EA=B2=A9=20=EB=93=B1=EB=A1=9D=204?= =?UTF-8?q?=EB=B2=8C=20=C2=B7=20=EB=A7=88=EC=8A=A4=ED=84=B0=20=EB=AC=B8=20?= =?UTF-8?q?4=EB=B2=8C=EC=9D=84=20RemoteRegistry=20=C2=B7=20requireSecretGa?= =?UTF-8?q?te=20=ED=95=98=EB=82=98=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib/core/remote_registry.dart: RemoteRegistry (configure · servers · server · client · closeAll, onDrop) - 서버마다 다른 것은 비밀 값 목록 · "설정 없음" 오류 · 만들기뿐. DavRegistry · SftpRegistry · SmbRegistry · FtpRegistry 는 같은 이름 그대로 이것을 부름 - 다시 접속하는 바뀜은 예전과 같게 (관리자 조건 ①): 지움 · 비밀이 아닌 칸 (toJson: 주소 · 포트 · 사용자 · 루트 · 보안 · 지문 · 공유 · 도메인 · insecure) · 비밀 값 (WebDAV · SMB · FTP = 비밀번호, SFTP = 비밀번호 · 개인 키 · 키 암호). 접속에 쓰는 칸은 모두 이 둘 안 - 주소 · 사용자가 바뀌었는데 예전 클라이언트를 쓰는 경우는 예전에도 없음. SMB: 버린 연결의 서명 기록 지우기 · 서명 필수가 바뀌면 모두 닫기 그대로 - 문 (관리자 조건 ②, 보안): SecretGate.pass 를 부르는 곳은 requireSecretGate 하나 - 비밀 값이 있으면 연결 전에 마스터 확인 - 약속 시험 remote_registry_contract (7): 4종 등록 규칙 · SMB 서명 필수 · 4종 문 (거절이면 연결 전 DavLockedException, SFTP 는 키만 있어도) · 비밀 값이 없으면 묻지 않음. 바꾸기 전 판 (fe8e5b7) 에서도 7개 모두 같게 통과 (동작 그대로임을 확인) 사용자가 보게 될 동작: 바뀌는 것 없음 (구조만) 확인: analyze 0 · 단위 1497 ×2 · 통합 6/6 ×2 · base10 214장 0px · ftp_shot · sshkey_shot · remote_idle_shot ×2 Co-Authored-By: Claude Opus 5.5 --- lib/core/ftp/ftp_remote.dart | 38 ++++------ lib/core/remote_registry.dart | 79 ++++++++++++++++++++ lib/core/sftp/sftp_remote.dart | 46 ++++-------- lib/core/smb.dart | 46 +++++------- lib/core/webdav.dart | 42 ++++------- test/remote_registry_contract_test.dart | 99 +++++++++++++++++++++++++ 6 files changed, 235 insertions(+), 115 deletions(-) create mode 100644 lib/core/remote_registry.dart create mode 100644 test/remote_registry_contract_test.dart diff --git a/lib/core/ftp/ftp_remote.dart b/lib/core/ftp/ftp_remote.dart index 928fb72..4fbd2aa 100644 --- a/lib/core/ftp/ftp_remote.dart +++ b/lib/core/ftp/ftp_remote.dart @@ -5,11 +5,11 @@ import 'dart:io'; import '../../l10n/tr.dart'; import '../dns_resolver.dart'; import '../remote_fs.dart'; -import '../secret_gate.dart'; import '../webdav.dart' show DavItem, DavLockedException; import '../remote_idle.dart'; import 'ftp_server.dart'; import 'ftp_session.dart'; +import '../remote_registry.dart'; // 약속 (나누기 ftp): FTP · FTPS 원격 저장소 ([RemoteClient]) 와 서버마다 하나인 클라이언트 목록 ([FtpRegistry]). // 입력: 서버 기준 경로 ("/" 로 시작). 출력: 목록 · 정보 ([DavItem]) · 읽기 흐름 · 쓰기 · 남은 공간 (FTP 는 모름). @@ -29,7 +29,7 @@ class FtpRemote implements RemoteClient { Future _connect() async { final s = server; if (s.host.trim().isEmpty) throw FtpException(tr('FTP 서버 주소가 없습니다'), FtpFailure.other); - if (s.password.isNotEmpty && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage)); + await requireSecretGate(s.password.isNotEmpty); if (s.user.isNotEmpty && s.password.isEmpty) { throw FtpException(tr('저장된 비밀번호가 없습니다. 서버 설정에서 비밀번호를 넣어 주세요'), FtpFailure.noSecret); } @@ -307,8 +307,13 @@ class FtpRemote implements RemoteClient { /// 설정의 FTP 서버 목록과 클라이언트 (서버마다 하나) class FtpRegistry { - static final _clients = {}; - static final _servers = {}; + // F11: 몸통은 RemoteRegistry 하나 - 비밀번호는 toJson 에 없으므로 따로 비교 + static final _r = RemoteRegistry( + idOf: (s) => s.id, + toJson: (s) => s.toJson(), + secretsOf: (s) => [s.password], + create: FtpRemote.new, + missing: (id) => FtpException(trf('FTP 서버 설정이 없습니다 ({0})', [id]), FtpFailure.other)); /// 이름 풀기 (main 에서 DNS 강제를 따르게 바꾼다). 처음 값: 기기 DNS (null = InternetAddress.lookup) static FtpResolve resolve = (host) async => InternetAddress.tryParse(host); @@ -322,25 +327,8 @@ class FtpRegistry { return addrs.where((a) => a.type == InternetAddressType.IPv4).firstOrNull ?? addrs.firstOrNull; }; - static void configure(List servers) { - final keep = {for (final s in servers) s.id: s}; - for (final id in _clients.keys.toList()) { - final old = _servers[id], now = keep[id]; - if (now == null || old == null || old.password != now.password || jsonEncode(old.toJson()) != jsonEncode(now.toJson())) { - _clients.remove(id)?.close(); - } - } - _servers - ..clear() - ..addAll(keep); - } - - static List get servers => _servers.values.toList(); - static FtpServer? server(String id) => _servers[id]; - - static FtpRemote client(String id) { - final s = _servers[id]; - if (s == null) throw FtpException(trf('FTP 서버 설정이 없습니다 ({0})', [id]), FtpFailure.other); - return _clients.putIfAbsent(id, () => FtpRemote(s)); - } + static void configure(List servers) => _r.configure(servers); + static List get servers => _r.servers; + static FtpServer? server(String id) => _r.server(id); + static FtpRemote client(String id) => _r.client(id); } diff --git a/lib/core/remote_registry.dart b/lib/core/remote_registry.dart new file mode 100644 index 0000000..9df9091 --- /dev/null +++ b/lib/core/remote_registry.dart @@ -0,0 +1,79 @@ +/// 약속 (modular_sync #6 · F11): 원격 서버 등록 하나 - 설정의 서버 목록 ([RemoteRegistry.configure]) → 서버 id 마다 클라이언트 하나 +/// ([RemoteRegistry.client]). WebDAV · SFTP · SMB · FTP 가 같은 몸통을 쓰고, 서버마다 다른 것은 셋뿐: 비밀 값 목록 · "설정 없음" 오류 · 만들기. +/// 다시 접속 (예전 클라이언트를 닫고 다음에 새로) 하는 바뀜 - 예전 4벌과 똑같이 (관리자 10/24 조건 ①): +/// 서버를 지움 · 새로 들어옴 · toJson 이 다름 (비밀이 아닌 모든 칸: 주소 · 포트 · 사용자 · 루트 · 보안 · 지문 · 공유 · 도메인 · insecure …) · +/// 비밀 값이 다름 (WebDAV · SMB · FTP = password, SFTP = password · privateKey · keyPassphrase) +/// 문 ([requireSecretGate]): 비밀 값이 있으면 마스터 확인 (SecretGate) 을 지나야 연다 - 못 지나면 [DavLockedException] (연결 전에) +library; + +import 'dart:convert'; + +import '../l10n/tr.dart'; +import 'remote_fs.dart'; +import 'secret_gate.dart'; +import 'webdav.dart' show DavLockedException; + +class RemoteRegistry { + RemoteRegistry({required this.idOf, required this.toJson, required this.secretsOf, required this.create, required this.missing, this.onDrop}); + + final String Function(S server) idOf; + final Map Function(S server) toJson; + + /// 비밀 값들 (toJson 에 없는 것 - 바뀌면 다시 접속) + final List Function(S server) secretsOf; + final C Function(S server) create; + + /// 등록에 없는 id 로 [client] 를 부를 때의 오류 + final Exception Function(String id) missing; + + /// 클라이언트를 버릴 때 (다시 접속 · 지움) - SMB 의 지난 연결 서명 기록 지우기 등 + final void Function(String id)? onDrop; + + final _clients = {}; + final _servers = {}; + + bool _same(S old, S now) { + final a = secretsOf(old), b = secretsOf(now); + if (a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return jsonEncode(toJson(old)) == jsonEncode(toJson(now)); + } + + void configure(List servers) { + final keep = {for (final s in servers) idOf(s): s}; + for (final id in _clients.keys.toList()) { + final old = _servers[id], now = keep[id]; + if (now == null || old == null || !_same(old, now)) { + _clients.remove(id)?.close(); + onDrop?.call(id); + } + } + _servers + ..clear() + ..addAll(keep); + } + + List get servers => _servers.values.toList(); + S? server(String id) => _servers[id]; + + /// 열린 클라이언트를 모두 닫는다 (다음 [client] 에서 새로 - 연결 규칙이 바뀐 때) + void closeAll() { + for (final c in _clients.values) { + c.close(); + } + _clients.clear(); + } + + C client(String id) { + final s = _servers[id]; + if (s == null) throw missing(id); + return _clients.putIfAbsent(id, () => create(s)); + } +} + +/// 문: 비밀 값이 있으면 마스터 확인을 지나야 한다 (못 지나면 연결 전에 [DavLockedException]). 비밀 값이 없으면 묻지 않는다 +Future requireSecretGate(bool hasSecret) async { + if (hasSecret && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage)); +} diff --git a/lib/core/sftp/sftp_remote.dart b/lib/core/sftp/sftp_remote.dart index 65955c9..0083bd7 100644 --- a/lib/core/sftp/sftp_remote.dart +++ b/lib/core/sftp/sftp_remote.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -8,10 +7,10 @@ import 'package:dartssh2/dartssh2.dart'; import '../../l10n/tr.dart'; import '../dns_resolver.dart'; import '../remote_fs.dart'; -import '../secret_gate.dart'; -import '../webdav.dart' show DavItem, DavLockedException; +import '../webdav.dart' show DavItem; import '../remote_idle.dart'; import 'sftp_server.dart'; +import '../remote_registry.dart'; // 약속 (나누기 sftp): SFTP 원격 저장소 ([RemoteClient], dartssh2) 와 서버마다 하나인 클라이언트 목록 ([SftpRegistry]). // 입력: 서버 기준 경로 ("/" 로 시작). 출력: 목록 · 정보 ([DavItem]) · 부분 읽기 흐름 · 쓰기 · 남은 공간 (모르면 null). @@ -48,7 +47,7 @@ class SftpRemote implements RemoteClient { final s = server; if (s.host.trim().isEmpty) throw SftpException(tr('SFTP 서버 주소가 없습니다'), SftpFailure.other); final hasSecret = s.password.isNotEmpty || s.privateKey.isNotEmpty; - if (hasSecret && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage)); + await requireSecretGate(hasSecret); // 키도 비밀번호도 없으면 (저장하지 않음) 시도하지 않고 정확히 알린다 (40 과 같게) if (!hasSecret) throw SftpException(tr('저장된 비밀번호가 없습니다'), SftpFailure.noSecret); Socket sock; @@ -404,8 +403,6 @@ class _Socket implements SSHSocket { /// 설정의 SFTP 서버 목록과 클라이언트 (서버마다 하나) class SftpRegistry { - static final _clients = {}; - static final _servers = {}; /// 연결 방법 (main 에서 DNS 강제를 따르게 바꾼다). 처음 값: 기기 DNS static SftpConnect connect = (host, port, timeout) => Socket.connect(host, port, timeout: timeout); @@ -427,31 +424,16 @@ class SftpRegistry { throw last ?? SocketException(trf('{0} 에 연결할 수 없습니다', [host])); }; - static void configure(List servers) { - final keep = {for (final s in servers) s.id: s}; - for (final id in _clients.keys.toList()) { - final old = _servers[id], now = keep[id]; - // 비밀 값은 toJson 에 없으므로 따로 비교 (바꾸면 바로 새 값으로 접속) - if (now == null || - old == null || - old.password != now.password || - old.privateKey != now.privateKey || - old.keyPassphrase != now.keyPassphrase || - jsonEncode(old.toJson()) != jsonEncode(now.toJson())) { - _clients.remove(id)?.close(); - } - } - _servers - ..clear() - ..addAll(keep); - } + // F11: 몸통은 RemoteRegistry 하나 - 비밀 값 (비밀번호 · 개인 키 · 키 암호) 은 toJson 에 없으므로 따로 비교 + static final _r = RemoteRegistry( + idOf: (s) => s.id, + toJson: (s) => s.toJson(), + secretsOf: (s) => [s.password, s.privateKey, s.keyPassphrase], + create: SftpRemote.new, + missing: (id) => SftpException(trf('SFTP 서버 설정이 없습니다 ({0})', [id]), SftpFailure.other)); - static List get servers => _servers.values.toList(); - static SftpServer? server(String id) => _servers[id]; - - static SftpRemote client(String id) { - final s = _servers[id]; - if (s == null) throw SftpException(trf('SFTP 서버 설정이 없습니다 ({0})', [id]), SftpFailure.other); - return _clients.putIfAbsent(id, () => SftpRemote(s)); - } + static void configure(List servers) => _r.configure(servers); + static List get servers => _r.servers; + static SftpServer? server(String id) => _r.server(id); + static SftpRemote client(String id) => _r.client(id); } diff --git a/lib/core/smb.dart b/lib/core/smb.dart index 43c1d76..2760889 100644 --- a/lib/core/smb.dart +++ b/lib/core/smb.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io' show InternetAddress; import 'dart:math' show min; import 'dart:typed_data'; @@ -8,9 +7,9 @@ import 'package:flutter/services.dart'; import '../l10n/tr.dart'; import 'remote_fs.dart'; -import 'secret_gate.dart'; -import 'webdav.dart' show DavException, DavItem, DavLockedException; +import 'webdav.dart' show DavException, DavItem; import 'remote_idle.dart'; +import 'remote_registry.dart'; /// 88: SMB 서버 설정 (Android 만 - Windows 는 운영체제가 \\서버\공유 를 연다, 사용자 결정 88-1). /// 비밀번호는 안전 저장소에 (설정 파일 · 보관본에는 넣지 않음). @@ -129,7 +128,7 @@ class SmbRemote implements RemoteClient { if (server.host.trim().isEmpty || server.share.trim().isEmpty) { throw SmbException(tr('SMB 서버 주소 · 공유 이름이 없습니다'), SmbFailure.other); } - if (server.password.isNotEmpty && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage)); + await requireSecretGate(server.password.isNotEmpty); if (server.user.isNotEmpty && server.password.isEmpty) throw SmbException(tr('저장된 비밀번호가 없습니다'), SmbFailure.noPassword); // 보안 검토 5: 이름은 여기서 풀어 주소로 넘긴다 (Android 의 SMB 다리가 시스템 DNS 로 풀어 강제 DNS 를 비켜 가지 않게) String host; @@ -306,8 +305,14 @@ class SmbRemote implements RemoteClient { /// 설정의 SMB 서버 목록과 클라이언트 (서버마다 하나) class SmbRegistry { - static final _clients = {}; - static final _servers = {}; + // F11: 몸통은 RemoteRegistry 하나 - 비밀번호는 toJson 에 없으므로 따로 비교, 버린 연결의 서명 기록도 지운다 + static final _r = RemoteRegistry( + idOf: (s) => s.id, + toJson: (s) => s.toJson(), + secretsOf: (s) => [s.password], + create: SmbRemote.new, + missing: (id) => SmbException(trf('SMB 서버 설정이 없습니다 ({0})', [id]), SmbFailure.other), + onDrop: (id) => _secure.remove(id)); /// 보안 검토 6: 서버 id → 지난 연결이 서명이나 암호화되었는지 (모르면 없음) static final _secure = {}; @@ -321,33 +326,16 @@ class SmbRegistry { static set requireSecure(bool v) { if (v == _requireSecure) return; _requireSecure = v; - for (final c in _clients.values) { - c.close(); - } - _clients.clear(); + _r.closeAll(); } static void configure(List servers) { - final keep = {for (final s in servers) s.id: s}; - for (final id in _clients.keys.toList()) { - final old = _servers[id], now = keep[id]; - if (now == null || old == null || old.password != now.password || jsonEncode(old.toJson()) != jsonEncode(now.toJson())) { - _clients.remove(id)?.close(); - _secure.remove(id); - } - } - _secure.removeWhere((id, _) => !keep.containsKey(id)); - _servers - ..clear() - ..addAll(keep); + _r.configure(servers); + _secure.removeWhere((id, _) => _r.server(id) == null); } - static List get servers => _servers.values.toList(); - static SmbServer? server(String id) => _servers[id]; - static SmbRemote client(String id) { - final s = _servers[id]; - if (s == null) throw SmbException(trf('SMB 서버 설정이 없습니다 ({0})', [id]), SmbFailure.other); - return _clients.putIfAbsent(id, () => SmbRemote(s)); - } + static List get servers => _r.servers; + static SmbServer? server(String id) => _r.server(id); + static SmbRemote client(String id) => _r.client(id); } diff --git a/lib/core/webdav.dart b/lib/core/webdav.dart index 59c0582..06a5073 100644 --- a/lib/core/webdav.dart +++ b/lib/core/webdav.dart @@ -6,8 +6,8 @@ import 'package:xml/xml.dart'; import '../l10n/tr.dart'; import 'remote_fs.dart'; -import 'secret_gate.dart'; import 'remote_idle.dart'; +import 'remote_registry.dart'; /// WebDAV 서버 설정 (환경 설정 > 파일 탐색기 > WebDAV). 비밀번호는 안전 저장소에 (설정 파일 · 보관본에는 넣지 않음). class DavServer { @@ -145,7 +145,7 @@ class DavClient implements RemoteClient { Future _open(String method, Uri uri) async { // 124: 저장된 비밀번호로 접속하기 전에 (마스터 비밀번호를 정해 두었으면 묻는다) - if (server.password.isNotEmpty && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage)); + await requireSecretGate(server.password.isNotEmpty); final req = await _http.openUrl(method, uri); final a = _auth; if (a != null) req.headers.set(HttpHeaders.authorizationHeader, a); @@ -380,32 +380,16 @@ class DavClient implements RemoteClient { /// 설정의 WebDAV 서버들 (id → 클라이언트). 설정이 바뀌면 [configure]. class DavRegistry { - static final _clients = {}; - static final _servers = {}; + // F11: 몸통은 RemoteRegistry 하나 - 비밀번호는 toJson 에 없으므로 따로 비교 (바꾸면 바로 새 비밀번호로 접속 - 40-2) + static final _r = RemoteRegistry( + idOf: (s) => s.id, + toJson: (s) => s.toJson(), + secretsOf: (s) => [s.password], + create: DavClient.new, + missing: (id) => DavException(trf('WebDAV 서버 설정이 없습니다 ({0})', [id]))); - static void configure(List servers) { - final keep = {for (final s in servers) s.id: s}; - for (final id in _clients.keys.toList()) { - final old = _servers[id], now = keep[id]; - // 비밀번호는 toJson 에 없으므로 따로 비교 (바꾸면 바로 새 비밀번호로 접속 - 40-2) - if (now == null || - old == null || - old.password != now.password || - jsonEncode(old.toJson()) != jsonEncode(now.toJson())) { - _clients.remove(id)?.close(); - } - } - _servers - ..clear() - ..addAll(keep); - } - - static List get servers => _servers.values.toList(); - static DavServer? server(String id) => _servers[id]; - - static DavClient client(String id) { - final s = _servers[id]; - if (s == null) throw DavException(trf('WebDAV 서버 설정이 없습니다 ({0})', [id])); - return _clients.putIfAbsent(id, () => DavClient(s)); - } + static void configure(List servers) => _r.configure(servers); + static List get servers => _r.servers; + static DavServer? server(String id) => _r.server(id); + static DavClient client(String id) => _r.client(id); } diff --git a/test/remote_registry_contract_test.dart b/test/remote_registry_contract_test.dart new file mode 100644 index 0000000..46b35d8 --- /dev/null +++ b/test/remote_registry_contract_test.dart @@ -0,0 +1,99 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/core/ftp.dart'; +import 'package:jj_mkvmaker/core/remote_fs.dart'; +import 'package:jj_mkvmaker/core/secret_gate.dart'; +import 'package:jj_mkvmaker/core/sftp.dart'; +import 'package:jj_mkvmaker/core/smb.dart'; +import 'package:jj_mkvmaker/core/webdav.dart'; + +/// modular_sync #6 F11 약속: 원격 등록 4벌 · 마스터 문 4벌을 한 몸통 (RemoteRegistry · requireSecretGate) 으로. +/// ① 다시 접속하는 바뀜은 예전과 같게: 지움 · 비밀이 아닌 칸 (주소 · 사용자 …) · 비밀 값 (SFTP 는 키 · 키 암호까지) +/// ② 문 (보안): 비밀 값이 있는데 마스터 확인 없이 열리는 길이 없음 - 거절이면 연결 전에 DavLockedException, 비밀 값이 없으면 묻지 않음 +void main() { + tearDown(() { + SecretGate.check = null; + DavRegistry.configure(const []); + SftpRegistry.configure(const []); + SmbRegistry.configure(const []); + FtpRegistry.configure(const []); + SmbRegistry.requireSecure = false; + }); + + /// 등록 하나의 규칙: 같으면 같은 클라이언트 · [changes] 마다 새 클라이언트 · 지우면 [missing] 오류 + void rules(String name, S base, List changes, void Function(List) configure, RemoteClient Function(String) client, + Matcher missing) { + test('$name: 같으면 같은 클라이언트 · 바뀌면 새로 · 지우면 설정 없음', () { + configure([base]); + final first = client('x'); + configure([base]); + expect(identical(client('x'), first), isTrue, reason: '그대로면 다시 접속하지 않음'); + var prev = first; + for (final (i, c) in changes.indexed) { + configure([base]); + prev = client('x'); + configure([c]); + expect(identical(client('x'), prev), isFalse, reason: '바뀜 $i 이면 새 클라이언트'); + } + configure(const []); + expect(() => client('x'), throwsA(missing)); + }); + } + + const dav = DavServer(id: 'x', name: 'n', url: 'http://127.0.0.1:1/dav', user: 'u', password: 'p'); + rules('WebDAV', dav, [dav.copyWith(url: 'http://127.0.0.1:2/dav'), dav.copyWith(user: 'v'), dav.copyWith(password: 'q'), + dav.copyWith(insecure: true)], DavRegistry.configure, DavRegistry.client, isA()); + + const sftp = SftpServer(id: 'x', name: 'n', host: '127.0.0.1', port: 1, user: 'u', password: 'p', privateKey: 'k', keyPassphrase: 'kp'); + rules('SFTP', sftp, [sftp.copyWith(host: 'h2'), sftp.copyWith(user: 'v'), sftp.copyWith(password: 'q'), + sftp.copyWith(privateKey: 'k2'), sftp.copyWith(keyPassphrase: 'kp2')], SftpRegistry.configure, SftpRegistry.client, isA()); + + const smb = SmbServer(id: 'x', name: 'n', host: '127.0.0.1', port: 1, share: 's', user: 'u', password: 'p'); + rules('SMB', smb, [smb.copyWith(host: 'h2'), smb.copyWith(share: 't'), smb.copyWith(user: 'v'), smb.copyWith(password: 'q')], + SmbRegistry.configure, SmbRegistry.client, isA()); + + const ftp = FtpServer(id: 'x', name: 'n', host: '127.0.0.1', port: 1, user: 'u', password: 'p', security: 'none'); + rules('FTP', ftp, [ftp.copyWith(host: 'h2'), ftp.copyWith(user: 'v'), ftp.copyWith(password: 'q'), ftp.copyWith(security: 'explicit')], + FtpRegistry.configure, FtpRegistry.client, isA()); + + test('SMB: 서명 · 암호화 필수를 바꾸면 열린 연결을 버림', () { + SmbRegistry.configure(const [smb]); + final first = SmbRegistry.client('x'); + SmbRegistry.requireSecure = true; + expect(identical(SmbRegistry.client('x'), first), isFalse); + }); + + test('문: 비밀 값이 있으면 4종 모두 마스터 확인을 지나야 함 - 거절이면 연결 전에 DavLockedException', () async { + var asked = 0; + SecretGate.check = (_) async { + asked++; + return false; + }; + final clients = [DavClient(dav), SftpRemote(sftp), SmbRemote(smb), FtpRemote(ftp)]; + for (final c in clients) { + await expectLater(c.list('/'), throwsA(isA()), reason: '${c.runtimeType}'); + c.close(); + } + expect(asked, clients.length, reason: '서버마다 물었음'); + // SFTP 는 개인 키만 있어도 비밀 값 + final keyOnly = SftpRemote(const SftpServer(id: 'y', name: 'n', host: '127.0.0.1', port: 1, user: 'u', privateKey: 'k')); + await expectLater(keyOnly.list('/'), throwsA(isA())); + keyOnly.close(); + }); + + test('문: 비밀 값이 없으면 묻지 않는다 (막히는 것은 마스터가 아니라 그 서버의 까닭)', () async { + var asked = 0; + SecretGate.check = (_) async { + asked++; + return false; + }; + for (final c in [ + DavClient(const DavServer(id: 'z', name: 'n', url: 'http://127.0.0.1:1/dav')), + SftpRemote(const SftpServer(id: 'z', name: 'n', host: '127.0.0.1', port: 1, user: 'u')), + FtpRemote(const FtpServer(id: 'z', name: 'n', host: '127.0.0.1', port: 1, security: 'none')), + ]) { + await expectLater(c.list('/'), throwsA(isNot(isA())), reason: '${c.runtimeType}'); + c.close(); + } + expect(asked, 0); + }); +}