설정 A 분야 2: 네트워크 · DNS (NetworkSettings) - 키 4 · dnsConfig → network.config

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
RyuJongJin 2026-10-22 22:29:42 +09:00
parent 428fe99a82
commit 1db0bda829
16 changed files with 105 additions and 81 deletions

View File

@ -45,13 +45,13 @@ void main() {
final answered = <String>[];
final logs = <String>[];
final g = NetGuard.instance
..config = (() => c.settings.dnsConfig)
..config = (() => c.settings.network.config)
..log = logs.add;
g.resolver.onAnswer = (h, s) => answered.add('$h@$s');
// 원인 가르기: JJ_CEF_NO_PROXY=1 이면 프록시 없이 (Chrome 엔진만)
final noProxy = Platform.environment['JJ_CEF_NO_PROXY'] == '1';
if (noProxy) c.settings.dnsMode = 'system';
expect(c.settings.dnsMode, noProxy ? 'system' : 'custom');
if (noProxy) c.settings.network.mode = 'system';
expect(c.settings.network.mode, noProxy ? 'system' : 'custom');
await tester.runAsync(g.ensure);
HttpOverrides.global = JjHttpOverrides();
Future<void> wait(int ms) async {

View File

@ -26,12 +26,12 @@ void main() {
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings
..webViewDataDir = p.join(data, 'webview')
..dnsMode = 'custom'
..dnsPrimary = '192.0.2.1'
..dnsSecondary = '';
..network.mode = 'custom'
..network.primary = '192.0.2.1'
..network.secondary = '';
final logs = <String>[];
final g = NetGuard.instance
..config = (() => c.settings.dnsConfig)
..config = (() => c.settings.network.config)
..log = logs.add;
await tester.runAsync(g.ensure);
Future<void> wait(int ms) async {
@ -69,6 +69,6 @@ void main() {
await tester.pumpWidget(const SizedBox());
await wait(1000);
g.config = () => c.settings.dnsConfig;
g.config = () => c.settings.network.config;
});
}

View File

@ -66,15 +66,15 @@ void main() {
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings
..downloadRoot = dir.path
..dnsMode = forced ? 'custom' : 'system'
..dnsPrimary = '8.8.8.8'
..dnsSecondary = '8.8.4.4';
..network.mode = forced ? 'custom' : 'system'
..network.primary = '8.8.8.8'
..network.secondary = '8.8.4.4';
final g = NetGuard.instance
..config = (() => c.settings.dnsConfig)
..config = (() => c.settings.network.config)
..log = log;
await tester.runAsync(g.ensure);
addTearDown(() async {
c.settings.dnsMode = 'system';
c.settings.network.mode = 'system';
await g.ensure();
});
final d = DownloadManager(backends: [Aria2Backend(aria2c: aria2c)], settings: () => c.settings);

View File

@ -37,12 +37,12 @@ void main() {
final logs = <String>[];
final answered = <String>[];
final g = NetGuard.instance
..config = (() => c.settings.dnsConfig)
..config = (() => c.settings.network.config)
..log = logs.add;
g.resolver.onAnswer = (h, s) => answered.add('$h@$s');
TorService.instance.log = logs.add;
expect(c.settings.dnsMode, 'custom');
expect(c.settings.dnsPrimary, '8.8.8.8');
expect(c.settings.network.mode, 'custom');
expect(c.settings.network.primary, '8.8.8.8');
await tester.runAsync(g.ensure);
HttpOverrides.global = JjHttpOverrides();
Future<void> wait(int ms) async {
@ -97,8 +97,8 @@ void main() {
// 3. 닿지 않는 DNS (TEST-NET) → 기기 DNS 로 바꾸지 않고 실패 + 알림
c.settings
..dnsPrimary = '192.0.2.1'
..dnsSecondary = '';
..network.primary = '192.0.2.1'
..network.secondary = '';
g.resolver.clearCache();
final err = await tester.runAsync(() async {
final cl = HttpClient();
@ -117,8 +117,8 @@ void main() {
expect(err, contains('502'));
expect(g.failure.value, isNotNull);
c.settings
..dnsPrimary = '8.8.8.8'
..dnsSecondary = '8.8.4.4';
..network.primary = '8.8.8.8'
..network.secondary = '8.8.4.4';
g.resolver.clearCache();
// 4. Tor 컴포넌트 받기 (공식 번들 · SHA-256) → Tor 화면 → check.torproject.org

View File

@ -4,7 +4,6 @@ import 'package:path/path.dart' as p;
import 'components.dart';
import '../core/reader_sources.dart' show defaultImageExtensions;
import '../core/dns_resolver.dart';
import '../core/download_detect.dart';
import '../core/encode_options.dart';
import '../core/playlist.dart';
@ -17,11 +16,13 @@ import '../services/app_shell.dart' show appIconOf;
import '../core/remote_idle.dart';
import 'settings/records.dart';
import 'settings/settings_group.dart';
import 'settings/network_settings.dart';
import 'settings/explorer_settings.dart';
export 'settings/records.dart';
export 'settings/settings_group.dart';
export 'settings/settings_store.dart';
export 'settings/network_settings.dart';
export 'settings/explorer_settings.dart';
/// 환경 설정 (settings.json 에 저장)
@ -157,13 +158,7 @@ class AppSettings {
bool webJavaScript = true;
// ── 네트워크 · DNS (사용자 요청 10/10: "DNS 주소를 강제로 내가 설정한 주소로", 기본 8.8.8.8 · 보조 8.8.4.4 - 관리자 판단) ──
/// 'system' 기기 DNS (끄기) · 'custom' 직접 지정 (기본) · 'doh' DNS over HTTPS
String dnsMode = DnsConfig.defaultMode;
String dnsPrimary = DnsConfig.defaultPrimary;
String dnsSecondary = DnsConfig.defaultSecondary;
String dnsDohUrl = DnsConfig.defaultDohUrl;
DnsConfig get dnsConfig => DnsConfig(mode: dnsMode, primary: dnsPrimary, secondary: dnsSecondary, dohUrl: dnsDohUrl);
// ── Tor 브라우저 (사용자 요청 10/10) ──
/// Tor 화면의 쿠키 · 기록 · 캐시를 닫은 뒤에도 남기기 (기본: 닫으면 지움)
@ -507,8 +502,11 @@ class AppSettings {
/// 파일 탐색기 (X-plore 참고)
final explorer = ExplorerSettings();
/// 네트워크 · DNS (사용자 요청 10/10)
final network = NetworkSettings();
/// 설정 분야들 (설정 A) - 저마다 자기 키를 읽고 쓴다. toJson · fromJson 은 이 목록을 돌며 합치기만 한다
List<SettingsGroup> get groups => [explorer];
List<SettingsGroup> get groups => [explorer, network];
/// [plainSecrets]: 안전 저장소를 쓸 수 없을 때만 (예전 파일에 있던 비밀 값을 잃지 않게) 예전처럼 파일에 둔다
Map<String, Object?> toJson({bool plainSecrets = false}) {
@ -570,10 +568,6 @@ class AppSettings {
'externalBrowser': externalBrowser,
'webTranslate': webTranslate,
'webJavaScript': webJavaScript,
'dnsMode': dnsMode,
'dnsPrimary': dnsPrimary,
'dnsSecondary': dnsSecondary,
'dnsDohUrl': dnsDohUrl,
'torKeepData': torKeepData,
'torIdleMinutes': torIdleMinutes,
'torHomeUrl': torHomeUrl,
@ -770,19 +764,6 @@ class AppSettings {
..externalBrowser = j['externalBrowser'] as String? ?? 'system'
..webTranslate = j['webTranslate'] as bool? ?? false
..webJavaScript = j['webJavaScript'] as bool? ?? true
// 값이 없거나 틀리면 처음 값 (사용자가 정한 기본: 직접 지정 8.8.8.8 · 8.8.4.4). 보조는 비워 둘 수 있다
..dnsMode = DnsConfig.modes.contains(j['dnsMode']) ? j['dnsMode'] as String : DnsConfig.defaultMode
..dnsPrimary = j['dnsPrimary'] is String && parseDnsServer(j['dnsPrimary'] as String) != null
? (j['dnsPrimary'] as String).trim()
: DnsConfig.defaultPrimary
..dnsSecondary = j['dnsSecondary'] is String
? (parseDnsServer(j['dnsSecondary'] as String) != null || (j['dnsSecondary'] as String).trim().isEmpty
? (j['dnsSecondary'] as String).trim()
: DnsConfig.defaultSecondary)
: DnsConfig.defaultSecondary
..dnsDohUrl = j['dnsDohUrl'] is String && isValidDohUrl(j['dnsDohUrl'] as String)
? (j['dnsDohUrl'] as String).trim()
: DnsConfig.defaultDohUrl
..torKeepData = j['torKeepData'] == true
..torIdleMinutes = j['torIdleMinutes'] is int && (j['torIdleMinutes'] as int) >= 0 && (j['torIdleMinutes'] as int) <= 60
? j['torIdleMinutes'] as int
@ -1027,7 +1008,7 @@ class AppSettings {
if (j.isNotEmpty && !j.containsKey('swipeNav') && s.swipeNav) 'swipeNav',
if (j.isNotEmpty && !j.containsKey('runInBackground') && s.runInBackground && Platform.isAndroid) 'background',
// DNS 강제 (사용자 요청 10/10, 처음 값 직접 지정 8.8.8.8 · 8.8.4.4) 가 없던 판에서 올라오면 이름을 묻는 서버가 바뀐다 - 193 과 같이 한 번 알림
if (j.isNotEmpty && !j.containsKey('dnsMode') && s.dnsMode == 'custom') 'dnsForced',
if (j.isNotEmpty && !j.containsKey('dnsMode') && s.network.mode == 'custom') 'dnsForced',
// 193 · 229: 예전 판을 쓰던 사람 (설정 파일이 있는데 이 항목이 없음) 에게 복사 · 이동이 받는 쪽의 더 새 파일을 건너뛰게 바뀐 것을 한 번
if (j.isNotEmpty && !j.containsKey('copySkipNewer')) 'copySkipNewer',
// 193: robocopy 를 쓰던 사람 (Windows) 에게 링크 · junction 을 따라가지 않게 (/XJ) 바뀐 것을 한 번

View File

@ -0,0 +1,43 @@
import '../../core/dns_resolver.dart';
import 'settings_group.dart';
/// 약속 (설정 A): 환경 설정 > 네트워크 · DNS (사용자 요청 10/10) - 자기 키 (이름 · 처음 값 · 읽기 · 쓰기) 를 스스로 갖는다.
/// 설정 하나를 더할 때는 이 파일만 고친다. 저장 파일의 키 이름은 예전 그대로 (평평한 키)
class NetworkSettings implements SettingsGroup {
/// 'system' 기기 DNS (끄기) · 'custom' 직접 지정 (기본) · 'doh' DNS over HTTPS
String mode = DnsConfig.defaultMode;
String primary = DnsConfig.defaultPrimary;
String secondary = DnsConfig.defaultSecondary;
String dohUrl = DnsConfig.defaultDohUrl;
DnsConfig get config => DnsConfig(mode: mode, primary: primary, secondary: secondary, dohUrl: dohUrl);
@override
void write(Map<String, Object?> j, {required bool plainSecrets}) {
j['dnsMode'] = mode;
j['dnsPrimary'] = primary;
j['dnsSecondary'] = secondary;
j['dnsDohUrl'] = dohUrl;
}
@override
void read(Map<String, dynamic> j) {
// 값이 없거나 틀리면 처음 값 (사용자가 정한 기본: 직접 지정 8.8.8.8 · 8.8.4.4). 보조는 비워 둘 수 있다
mode = DnsConfig.modes.contains(j['dnsMode']) ? j['dnsMode'] as String : DnsConfig.defaultMode;
primary = j['dnsPrimary'] is String && parseDnsServer(j['dnsPrimary'] as String) != null
? (j['dnsPrimary'] as String).trim()
: DnsConfig.defaultPrimary;
secondary = j['dnsSecondary'] is String
? (parseDnsServer(j['dnsSecondary'] as String) != null || (j['dnsSecondary'] as String).trim().isEmpty
? (j['dnsSecondary'] as String).trim()
: DnsConfig.defaultSecondary)
: DnsConfig.defaultSecondary;
dohUrl = j['dnsDohUrl'] is String && isValidDohUrl(j['dnsDohUrl'] as String)
? (j['dnsDohUrl'] as String).trim()
: DnsConfig.defaultDohUrl;
}
}

View File

@ -312,7 +312,7 @@ Future<void> main(List<String> args) async {
downloads.refresh();
};
downloads.useSystemDns = () async {
await controller.updateSettings((x) => x.dnsMode = 'system');
await controller.updateSettings((x) => x.network.mode = 'system');
downloads.refresh();
};
// COM 준비 상태 지킴이: 풀려 있으면 (앱 안 브라우저가 "CoInitialize 가 호출되지 않았습니다" 로 안 뜨는 원인)
@ -468,7 +468,7 @@ Future<void> openFromOtherApp(
/// [second] (재생만 하는 새 창) 은 설정을 읽기만 한다
Future<void> setupNetwork(AppController c, String dataDir, {bool second = false}) async {
final g = NetGuard.instance
..config = (() => c.settings.dnsConfig)
..config = (() => c.settings.network.config)
..log = c.note;
try {
await g.ensure();
@ -498,17 +498,17 @@ Future<void> setupNetwork(AppController c, String dataDir, {bool second = false}
onHide: () => unawaited(WebProxy.setForeground(false)),
);
}
var last = c.settings.dnsConfig.key;
var last = c.settings.network.config.key;
var lastPause = c.settings.webPauseOnLeave;
c.addListener(() async {
if (Platform.isAndroid && c.settings.webPauseOnLeave != lastPause) {
lastPause = c.settings.webPauseOnLeave;
await WebProxy.apply();
}
final now = c.settings.dnsConfig.key;
final now = c.settings.network.config.key;
if (now == last) return;
last = now;
c.note(trf('DNS 설정: {0}', [c.settings.dnsMode == 'system' ? tr('시스템 DNS') : c.settings.dnsConfig.label]));
c.note(trf('DNS 설정: {0}', [c.settings.network.mode == 'system' ? tr('시스템 DNS') : c.settings.network.config.label]));
await g.ensure();
if (Platform.isAndroid) await WebProxy.apply();
});
@ -528,7 +528,7 @@ void watchDnsFailure(GlobalKey<ScaffoldMessengerState> messengerKey, AppControll
// 사용자에게는 무엇이 안 되고 무엇을 할지만 (예외 원문은 작업 기록에 - NetGuard.reportError).
// 상태가 풀리거나 같은 안내를 보이는 화면이 열리면 스스로 닫힌다 (248)
content: DnsFailureNotice(failure: f),
action: (tr('시스템 DNS 로 바꾸기'), () => c.updateSettings((x) => x.dnsMode = 'system')),
action: (tr('시스템 DNS 로 바꾸기'), () => c.updateSettings((x) => x.network.mode = 'system')),
);
});
}
@ -632,7 +632,7 @@ Future<void> runAndroid(String dataDir) async {
downloads.refresh();
};
downloads.useSystemDns = () async {
await controller.updateSettings((x) => x.dnsMode = 'system');
await controller.updateSettings((x) => x.network.mode = 'system');
downloads.refresh();
};
downloads.addToEditList = controller.addDownloaded;

View File

@ -280,7 +280,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
Wrap(spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: [
FilledButton(
onPressed: () async {
await widget.c.updateSettings((x) => x.dnsMode = 'system');
await widget.c.updateSettings((x) => x.network.mode = 'system');
// 새 DNS 설정이 프록시에 걸린 뒤 다시 읽는다
await Future<void>.delayed(const Duration(milliseconds: 300));
if (mounted) retry();

View File

@ -353,7 +353,7 @@ class _DownloadRow extends StatelessWidget {
child: Text(tr('자세히')),
),
// 263: 아직 DNS 강제면 [시스템 DNS 로 바꾸기] (바꾼 뒤 [다시 시도] 는 사용자가 - 몰래 다시 받지 않음)
if (t.extra['dnsServer'] != null && d.useSystemDns != null && d.settings().dnsMode != 'system')
if (t.extra['dnsServer'] != null && d.useSystemDns != null && d.settings().network.mode != 'system')
TextButton(style: _small, onPressed: () => d.useSystemDns!(), child: Text(tr('시스템 DNS 로 바꾸기'))),
TextButton(style: _small, onPressed: () => d.resume([t]), child: Text(tr('다시 시도'))),
])

View File

@ -47,7 +47,7 @@ class DnsFailureLine extends StatelessWidget {
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 8), minimumSize: const Size(0, 36)),
onPressed: () => c.updateSettings((x) => x.dnsMode = 'system'),
onPressed: () => c.updateSettings((x) => x.network.mode = 'system'),
child: Text(tr('시스템 DNS 로 바꾸기'), style: const TextStyle(fontSize: 13)),
),
]),
@ -59,7 +59,7 @@ class DnsFailureLine extends StatelessWidget {
const Icon(Icons.warning_amber_rounded, size: 18, color: Colors.orange),
Text(text, style: const TextStyle(fontSize: 12, color: Colors.orange)),
TextButton(
onPressed: () => c.updateSettings((x) => x.dnsMode = 'system'),
onPressed: () => c.updateSettings((x) => x.network.mode = 'system'),
child: Text(tr('시스템 DNS 로 바꾸기')),
),
]),
@ -149,11 +149,11 @@ class _DnsSettingsState extends State<DnsSettings> {
_testing = true;
_test = null;
});
final r = DnsResolver(config: () => c.settings.dnsConfig);
final r = DnsResolver(config: () => c.settings.network.config);
String? server;
r.onAnswer = (_, s) => server = s;
try {
final a = c.settings.dnsMode == 'system'
final a = c.settings.network.mode == 'system'
? await InternetAddress.lookup('www.google.com').then((v) {
server = tr('기기 DNS');
return v;
@ -188,34 +188,34 @@ class _DnsSettingsState extends State<DnsSettings> {
subtitle: Text(tr('주소 이름 (예: youtube.com) 을 찾을 때 물어볼 서버. 직접 지정하면 기기의 DNS 대신 이 서버만 씁니다. '
'서버에 닿지 않으면 기기 DNS 로 몰래 바꾸지 않고 알립니다.')),
trailing: DropdownButton<String>(
value: s.dnsMode,
value: s.network.mode,
items: [
DropdownMenuItem(value: 'system', child: Text(tr('시스템 DNS (끄기)'))),
DropdownMenuItem(value: 'custom', child: Text(tr('직접 지정 (기본)'))),
DropdownMenuItem(value: 'doh', child: Text(tr('DNS over HTTPS'))),
],
onChanged: (v) => _save((x) => x.dnsMode = v!),
onChanged: (v) => _save((x) => x.network.mode = v!),
),
),
if (s.dnsMode != 'system')
if (s.network.mode != 'system')
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: Wrap(spacing: 12, runSpacing: 12, children: [
if (s.dnsMode == 'doh')
_field(tr('DoH 주소'), s.dnsDohUrl, _dohError, hint: DnsConfig.defaultDohUrl, (v) {
if (s.network.mode == 'doh')
_field(tr('DoH 주소'), s.network.dohUrl, _dohError, hint: DnsConfig.defaultDohUrl, (v) {
final ok = isValidDohUrl(v);
setState(() => _dohError = ok ? null : tr('https:// 로 시작하는 주소를 넣으세요'));
if (ok) _save((x) => x.dnsDohUrl = v.trim());
if (ok) _save((x) => x.network.dohUrl = v.trim());
}),
_field(s.dnsMode == 'doh' ? tr('DoH 서버 이름을 찾을 서버') : tr('기본 DNS'), s.dnsPrimary, _primaryError, hint: DnsConfig.defaultPrimary, (v) {
_field(s.network.mode == 'doh' ? tr('DoH 서버 이름을 찾을 서버') : tr('기본 DNS'), s.network.primary, _primaryError, hint: DnsConfig.defaultPrimary, (v) {
final ok = parseDnsServer(v) != null;
setState(() => _primaryError = ok ? null : tr('IPv4 · IPv6 주소를 넣으세요 (예: 8.8.8.8)'));
if (ok) _save((x) => x.dnsPrimary = v.trim());
if (ok) _save((x) => x.network.primary = v.trim());
}),
_field(tr('보조 DNS (비워 둘 수 있음)'), s.dnsSecondary, _secondaryError, hint: DnsConfig.defaultSecondary, (v) {
_field(tr('보조 DNS (비워 둘 수 있음)'), s.network.secondary, _secondaryError, hint: DnsConfig.defaultSecondary, (v) {
final ok = v.trim().isEmpty || parseDnsServer(v) != null;
setState(() => _secondaryError = ok ? null : tr('IPv4 · IPv6 주소를 넣으세요 (예: 8.8.4.4)'));
if (ok) _save((x) => x.dnsSecondary = v.trim());
if (ok) _save((x) => x.network.secondary = v.trim());
}),
]),
),

View File

@ -61,7 +61,7 @@ void main() {
testWidgets('멈춘 마그넷 줄: "트래커 · 피어에 닿지 않습니다" · 이유 후보 · [다시 시도] (지금 것을 지우고 새로 넣음)', (tester) async {
final b = _Backend();
final s = AppSettings()..dnsMode = 'custom';
final s = AppSettings()..network.mode = 'custom';
final d = DownloadManager(backends: [b], settings: () => s);
final t = d.add('magnet:?xt=urn:btih:7acf8fb590b2060dd9c3146ef770169d593433b0')!;
await tester.pump();

View File

@ -396,7 +396,7 @@ void main() {
await tester.pump();
await tester.tap(find.text('시스템 DNS 로 바꾸기'));
await tester.pump(const Duration(milliseconds: 400));
expect(c.settings.dnsMode, 'system');
expect(c.settings.network.mode, 'system');
expect(find.text(msg), findsNothing);
expect(nav.reloads, 2);

View File

@ -72,7 +72,7 @@ void main() {
expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않음 (01:35부터)'), findsOneWidget);
await tester.tap(find.text('시스템 DNS 로 바꾸기'));
await tester.pump();
expect(c.settings.dnsMode, 'system');
expect(c.settings.network.mode, 'system');
});
testWidgets('위쪽 이동 버튼: 설치한 화면만 · 설정을 바꾸면 바로 · 하나뿐이면 그것만', (tester) async {

View File

@ -464,15 +464,15 @@ void main() {
test('설정: 처음은 직접 지정 8.8.8.8 · 8.8.4.4, 틀린 값은 처음 값, 사용자가 고른 값은 그대로', () {
final s = AppSettings();
expect((s.dnsMode, s.dnsPrimary, s.dnsSecondary, s.dnsDohUrl), ('custom', '8.8.8.8', '8.8.4.4', 'https://dns.google/dns-query'));
expect(AppSettings.fromJson({}).dnsMode, 'custom');
expect((s.network.mode, s.network.primary, s.network.secondary, s.network.dohUrl), ('custom', '8.8.8.8', '8.8.4.4', 'https://dns.google/dns-query'));
expect(AppSettings.fromJson({}).network.mode, 'custom');
final bad = AppSettings.fromJson({'dnsMode': 'x', 'dnsPrimary': 'dns.google', 'dnsSecondary': '1.2.3', 'dnsDohUrl': 'ftp://x'});
expect((bad.dnsMode, bad.dnsPrimary, bad.dnsSecondary, bad.dnsDohUrl), ('custom', '8.8.8.8', '8.8.4.4', 'https://dns.google/dns-query'));
expect((bad.network.mode, bad.network.primary, bad.network.secondary, bad.network.dohUrl), ('custom', '8.8.8.8', '8.8.4.4', 'https://dns.google/dns-query'));
final mine = AppSettings.fromJson(
AppSettings().toJson()..addAll({'dnsMode': 'system', 'dnsPrimary': '1.1.1.1', 'dnsSecondary': '', 'dnsDohUrl': 'https://cloudflare-dns.com/dns-query'}));
expect((mine.dnsMode, mine.dnsPrimary, mine.dnsSecondary, mine.dnsDohUrl), ('system', '1.1.1.1', '', 'https://cloudflare-dns.com/dns-query'));
expect((mine.network.mode, mine.network.primary, mine.network.secondary, mine.network.dohUrl), ('system', '1.1.1.1', '', 'https://cloudflare-dns.com/dns-query'));
final back = AppSettings.fromJson(mine.toJson());
expect(back.dnsConfig.key, mine.dnsConfig.key);
expect(back.network.config.key, mine.network.config.key);
// Tor 설정 처음 값
expect((s.torKeepData, s.torHomeUrl, s.torJavaScript, s.torNoticeSeen), (false, 'https://check.torproject.org/', true, false));
});

View File

@ -135,7 +135,7 @@ void main() {
final b = _Backend(tunnel);
final d = DownloadManager(backends: [b], settings: () => s, readClipboard: () async => null);
d.useSystemDns = () async {
s.dnsMode = 'system';
s.network.mode = 'system';
d.refresh();
};
final t = d.addPage('https://cdn.example.com/page')!..title = '영상';
@ -149,7 +149,7 @@ void main() {
await tester.tap(find.text('시스템 DNS 로 바꾸기'));
await tester.pump();
expect(s.dnsMode, 'system');
expect(s.network.mode, 'system');
expect(find.text('시스템 DNS 로 바꾸기'), findsNothing);
expect(b.starts, 1, reason: '몰래 다시 받지 않는다');

View File

@ -428,10 +428,10 @@ void main() {
g.failure.value = DnsFailure('192.0.2.1', 'x', DateTime(2026, 10, 12, 9, 5));
await t.pump();
expect(find.text('DNS 서버 (192.0.2.1) 에 닿지 않음 (09:05부터)'), findsOneWidget);
expect(c.settings.dnsMode, 'custom');
expect(c.settings.network.mode, 'custom');
await t.tap(find.text('시스템 DNS 로 바꾸기'));
await t.pump();
expect(c.settings.dnsMode, 'system');
expect(c.settings.network.mode, 'system');
g.failure.value = null;
await t.pump();
expect(find.textContaining('에 닿지 않음'), findsNothing);