From 6470368c2e1a2592b6bc423ffc21cb96c8d2c005 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Mon, 12 Oct 2026 01:27:00 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8B=BF=EC=A7=80=20=EC=95=8A=EB=8A=94=20DNS:?= =?UTF-8?q?=20=EC=83=88=20=EB=B2=84=EC=A0=84=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=EB=A5=BC=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B8=80=EB=A1=9C=20=C2=B7=20=EC=95=88=EB=82=B4=EB=8A=94=20?= =?UTF-8?q?=EB=8B=AB=EC=9D=84=20=EB=95=8C=EA=B9=8C=EC=A7=80=20=C2=B7=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=84=A4=EC=A0=95=20=C2=B7=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=ED=98=84=ED=99=A9=EC=97=90=20(247=20?= =?UTF-8?q?=C2=B7=20248=20=C2=B7=20249)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 테스터 a4 의 914 기기 확인 · 관리자 지시 (10/12, 버그 수정 · 편의): - 247: 새 버전 확인 실패가 영어 원문 (HttpException: Proxy failed to establish tunnel (502 ...)) 이던 것. DNS 강제 프록시에서 실패했으면 "DNS 서버 (…) 에 닿지 않아 새 버전을 확인하지 못했습니다" (원문은 작업 기록). - 248: [시스템 DNS 로 바꾸기] 가 붙은 안내가 12초 만에 사라지던 것 → 사용자가 닫거나 누를 때까지. - 249: 안내가 1분마다 다시 뜨던 것 → 처음 실패 때 한 번. 상태는 서버가 다시 답하거나 DNS 설정을 바꿀 때까지 환경 설정 > 네트워크 · DNS 와 작업 현황에 "DNS 서버 (…) 에 닿지 않음 (시각부터) · [시스템 DNS 로 바꾸기]". Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 5 ++++ assets/l10n/ja.json | 5 ++++ assets/l10n/zh-Hans.json | 5 ++++ lib/app/net_guard.dart | 45 +++++++++++++++++++++++++++----- lib/main.dart | 4 +-- lib/ui/network_settings.dart | 30 ++++++++++++++++++++++ lib/ui/update_dialog.dart | 6 ++++- lib/ui/work_panel.dart | 3 +++ test/dns_proxy_test.dart | 50 ++++++++++++++++++++++++++++++++++++ test/tor_test.dart | 19 ++++++++++++++ 10 files changed, 162 insertions(+), 10 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 6ead742..9402d9a 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -144,9 +144,12 @@ "DNS 강제 프록시를 시작하지 못했습니다: {0}": "Could not start the DNS-forcing proxy: {0}", "DNS 서버": "DNS server", "DNS 서버 ({0}) 에 닿지 않습니다: {1}": "Cannot reach the DNS server ({0}): {1}", + "DNS 서버 ({0}) 에 닿지 않아 {1}": "Couldn't reach the DNS server ({0}), so {1}", "DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다. 인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.": "Couldn't reach the DNS server ({0}), so the address couldn't be found. Check the internet connection or the server address in Settings > Network · DNS.", "DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다: {1}": "Could not look up the address because the DNS server ({0}) is unreachable: {1}", + "DNS 서버 ({0}) 에 닿지 않음 ({1}부터)": "Can't reach DNS server ({0}) (since {1})", "DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "No DNS server address (Settings > Network · DNS)", + "DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "The DNS server couldn't find the address of {0}, so {1}", "DNS 설정: {0}": "DNS setting: {0}", "DNS 실패 ({0}): {1}": "DNS failure ({0}): {1}", "DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "View DOC · DOCX · HWP · XLS · XLSX converted to PDF (downloads a converter)", @@ -406,6 +409,7 @@ "{0} 안의 파일": "Files in {0}", "{0} 에 보내는 중": "Sending to {0}", "{0} 에 붙여넣기 ({1}개)": "Paste into {0} ({1})", + "{0} 에 연결하지 못해 {1} (DNS 강제 프록시)": "Couldn't connect to {0}, so {1} (forced DNS proxy)", "{0} 에 연결할 수 없습니다": "Cannot connect to {0}", "{0} 에 연결할 수 없습니다. 네트워크 · NAS 가 켜져 있는지 확인하세요.": "Cannot connect to {0}. Check that the network and NAS are on.", "{0} 에서 그리는 중 ({1}초)": "Drawing on {0} ({1} s)", @@ -1135,6 +1139,7 @@ "새 버전 v{0}": "New version v{0}", "새 버전 v{0} 이 있습니다": "New version v{0} is available", "새 버전 확인: 지금 v{0} · 최신 v{1}": "Update check: current v{0} · latest v{1}", + "새 버전을 확인하지 못했습니다": "the update check failed.", "새 버전을 확인할 수 없습니다: {0}": "Cannot check for a new version: {0}", "새 버전이 예전 기본값을 바꿨으면 켤 때 한 번 무엇이 바뀌었는지 알려 줍니다": "If a new version changed old defaults, shows once at startup what changed", "새 비밀번호": "New password", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 3c9ac15..e18d2ae 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -144,9 +144,12 @@ "DNS 강제 프록시를 시작하지 못했습니다: {0}": "DNS 強制プロキシを開始できませんでした: {0}", "DNS 서버": "DNS サーバー", "DNS 서버 ({0}) 에 닿지 않습니다: {1}": "DNS サーバー ({0}) に届きません: {1}", + "DNS 서버 ({0}) 에 닿지 않아 {1}": "DNS サーバー ({0}) に接続できないため、{1}", "DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다. 인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.": "DNS サーバー ({0}) に届かず、アドレスを見つけられませんでした。インターネット接続か、環境設定 > ネットワーク · DNS のサーバーアドレスを確認してください。", "DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다: {1}": "DNS サーバー ({0}) に届かず、アドレスを解決できませんでした: {1}", + "DNS 서버 ({0}) 에 닿지 않음 ({1}부터)": "DNS サーバー ({0}) に接続できません ({1} から)", "DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "DNS サーバーのアドレスがありません (環境設定 > ネットワーク・DNS)", + "DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "DNS サーバーが {0} のアドレスを見つけられないため、{1}", "DNS 설정: {0}": "DNS 設定: {0}", "DNS 실패 ({0}): {1}": "DNS 失敗 ({0}): {1}", "DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "DOC・DOCX・HWP・XLS・XLSX を PDF に変換して表示 (変換ツールをダウンロード)", @@ -406,6 +409,7 @@ "{0} 안의 파일": "{0} 内のファイル", "{0} 에 보내는 중": "{0} に送信中", "{0} 에 붙여넣기 ({1}개)": "{0} に貼り付け ({1} 件)", + "{0} 에 연결하지 못해 {1} (DNS 강제 프록시)": "{0} に接続できないため、{1} (DNS 強制プロキシ)", "{0} 에 연결할 수 없습니다": "{0} に接続できません", "{0} 에 연결할 수 없습니다. 네트워크 · NAS 가 켜져 있는지 확인하세요.": "{0} に接続できません。ネットワーク・NAS がオンか確認してください。", "{0} 에서 그리는 중 ({1}초)": "{0} で描画中 ({1} 秒)", @@ -1135,6 +1139,7 @@ "새 버전 v{0}": "新しいバージョン v{0}", "새 버전 v{0} 이 있습니다": "新しいバージョン v{0} があります", "새 버전 확인: 지금 v{0} · 최신 v{1}": "新しいバージョンの確認: 現在 v{0} · 最新 v{1}", + "새 버전을 확인하지 못했습니다": "新しいバージョンを確認できませんでした。", "새 버전을 확인할 수 없습니다: {0}": "新しいバージョンを確認できません: {0}", "새 버전이 예전 기본값을 바꿨으면 켤 때 한 번 무엇이 바뀌었는지 알려 줍니다": "新しいバージョンが以前の既定値を変更した場合、起動時に一度だけ何が変わったかを知らせます", "새 비밀번호": "新しいパスワード", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index f4186a9..a2217ff 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -144,9 +144,12 @@ "DNS 강제 프록시를 시작하지 못했습니다: {0}": "无法启动 DNS 强制代理: {0}", "DNS 서버": "DNS 服务器", "DNS 서버 ({0}) 에 닿지 않습니다: {1}": "无法连接 DNS 服务器 ({0}): {1}", + "DNS 서버 ({0}) 에 닿지 않아 {1}": "无法连接 DNS 服务器 ({0}),因此{1}", "DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다. 인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.": "无法连接 DNS 服务器({0}),未能找到地址。请检查网络连接,或检查 设置 > 网络 · DNS 中的服务器地址。", "DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다: {1}": "无法连接 DNS 服务器 ({0}),未能解析地址: {1}", + "DNS 서버 ({0}) 에 닿지 않음 ({1}부터)": "无法连接 DNS 服务器 ({0}) (自 {1} 起)", "DNS 서버 주소가 없습니다 (환경 설정 > 네트워크 · DNS)": "没有 DNS 服务器地址 (设置 > 网络 · DNS)", + "DNS 서버가 {0} 의 주소를 찾지 못해 {1}": "DNS 服务器找不到 {0} 的地址,因此{1}", "DNS 설정: {0}": "DNS 设置: {0}", "DNS 실패 ({0}): {1}": "DNS 失败 ({0}): {1}", "DOC · DOCX · HWP · XLS · XLSX 를 PDF 로 바꿔 보기 (변환기를 받음)": "将 DOC · DOCX · HWP · XLS · XLSX 转为 PDF 查看 (需下载转换器)", @@ -406,6 +409,7 @@ "{0} 안의 파일": "{0} 中的文件", "{0} 에 보내는 중": "正在发送到 {0}", "{0} 에 붙여넣기 ({1}개)": "粘贴到 {0} ({1} 个)", + "{0} 에 연결하지 못해 {1} (DNS 강제 프록시)": "无法连接 {0},因此{1} (强制 DNS 代理)", "{0} 에 연결할 수 없습니다": "无法连接到 {0}", "{0} 에 연결할 수 없습니다. 네트워크 · NAS 가 켜져 있는지 확인하세요.": "无法连接到 {0}。请确认网络和 NAS 已开启。", "{0} 에서 그리는 중 ({1}초)": "正在 {0} 上绘制 ({1} 秒)", @@ -1135,6 +1139,7 @@ "새 버전 v{0}": "新版本 v{0}", "새 버전 v{0} 이 있습니다": "有新版本 v{0}", "새 버전 확인: 지금 v{0} · 최신 v{1}": "检查新版本: 当前 v{0} · 最新 v{1}", + "새 버전을 확인하지 못했습니다": "未能检查新版本。", "새 버전을 확인할 수 없습니다: {0}": "无法检查新版本: {0}", "새 버전이 예전 기본값을 바꿨으면 켤 때 한 번 무엇이 바뀌었는지 알려 줍니다": "如果新版本更改了旧的默认值,启动时提示一次更改了什么", "새 비밀번호": "新密码", diff --git a/lib/app/net_guard.dart b/lib/app/net_guard.dart index 52181a2..aef93b8 100644 --- a/lib/app/net_guard.dart +++ b/lib/app/net_guard.dart @@ -28,11 +28,13 @@ class NetGuard { /// 지금 DNS 설정 (main 에서 환경 설정을 읽게 바꾼다) DnsConfig Function() config = () => const DnsConfig.system(); - late final DnsResolver resolver = DnsResolver(config: () => config()); + // 249: 정한 서버가 다시 답하면 "닿지 않음" 상태를 지운다 + late final DnsResolver resolver = DnsResolver(config: () => config())..onAnswer = (_, _) => _answered(); LocalProxy? _proxy; Future? _starting; - /// DNS 서버에 닿지 않음 (화면에 알림 - [시스템 DNS 로 바꾸기]) + /// DNS 서버에 닿지 않음 (화면에 알림 - [시스템 DNS 로 바꾸기]). + /// 249: 서버가 다시 답하거나 DNS 설정을 바꿀 때까지 남는다 - 아래 안내를 놓쳐도 환경 설정 > 네트워크 · DNS 와 작업 현황에 보인다. final failure = ValueNotifier(null); /// 작업 기록 @@ -47,6 +49,7 @@ class NetGuard { /// 설정이 강제면 프록시를 띄운다 (이미 떠 있으면 그대로). 설정을 바꾼 뒤에도 부른다 Future ensure() async { resolver.clearCache(); + failure.value = null; // 설정이 바뀌었다 - 새 설정으로 다시 본다 if (!config().forced) return; if (port != null) return; _starting ??= () async { @@ -67,17 +70,45 @@ class NetGuard { } } - DateTime? _lastAlert; + DateTime? _lastLog; + + /// 247: 프록시가 연결하지 못한 이름과 그 까닭 (앱의 HTTP 오류를 사용자 글로 바꿀 때) + final _lastError = {}; + void reportError(String host, Object e) { + if (_lastError.length > 200) _lastError.clear(); + _lastError[host.toLowerCase()] = (e, DateTime.now()); if (e is! DnsException || !e.unreachable) return; - log?.call(trf('DNS 실패 ({0}): {1}', [host, e])); final now = DateTime.now(); - // 같은 알림을 계속 띄우지 않게 (1분에 한 번) - if (_lastAlert != null && now.difference(_lastAlert!) < const Duration(minutes: 1)) return; - _lastAlert = now; + // 작업 기록은 1분에 한 번 (같은 실패가 줄줄이 나지 않게) + if (_lastLog == null || now.difference(_lastLog!) >= const Duration(minutes: 1)) { + _lastLog = now; + log?.call(trf('DNS 실패 ({0}): {1}', [host, e])); + } + // 248 · 249: 안내는 "닿지 않음" 이 처음 생길 때 한 번 (다시 답한 뒤 또 실패하면 다시). 상태는 [failure] 에 남는다 + if (failure.value != null) return; failure.value = DnsFailure(e.server.isEmpty ? config().label : e.server, e.message, now); } + void _answered() { + if (failure.value != null) failure.value = null; + } + + /// 247: 앱의 HTTP 요청이 DNS 강제 프록시에서 실패했으면 그 까닭을 사용자 글로 ("DNS 서버 (…) 에 닿지 않아 …"). + /// 프록시 실패가 아니면 null (부르는 쪽이 원래 글을 쓴다). [what] 은 무엇을 못 했는지 ("새 버전을 확인하지 못했습니다"). + String? explain(Object e, String what) { + if (e is! HttpException || !'$e'.contains('Proxy failed to establish tunnel')) return null; + final host = (e.uri?.host ?? '').toLowerCase(); + final last = _lastError[host]; + final recent = last != null && DateTime.now().difference(last.$2) < const Duration(minutes: 2); + final why = recent ? last.$1 : null; + if (why is DnsException && why.unreachable) { + return trf('DNS 서버 ({0}) 에 닿지 않아 {1}', [why.server.isEmpty ? config().label : why.server, what]); + } + if (why is DnsException) return trf('DNS 서버가 {0} 의 주소를 찾지 못해 {1}', [host, what]); + return trf('{0} 에 연결하지 못해 {1} (DNS 강제 프록시)', [host.isEmpty ? '?' : host, what]); + } + /// 이 주소는 프록시를 거치지 않는다: IP 주소 (DNS 를 묻지 않음) · 같은 집 안 이름 (LAN · NAS · Tailscale) static bool isDirectHost(String host) => host.isEmpty || InternetAddress.tryParse(host) != null || isLocalName(host); diff --git a/lib/main.dart b/lib/main.dart index cac4bf0..1deaf7f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -503,9 +503,9 @@ void watchDnsFailure(GlobalKey messengerKey, AppControll final f = NetGuard.instance.failure.value; if (f == null) return; messengerKey.currentState?.showSnackBar(SnackBar( - duration: const Duration(seconds: 12), + // 248: 버튼이 있는 안내라 사용자가 닫거나 누를 때까지 둔다 (한 번만 뜨고, 상태는 설정 · 작업 현황에 남음 - 249) + persist: true, showCloseIcon: true, - persist: false, // 사용자에게는 무엇이 안 되고 무엇을 할지만 (예외 원문은 작업 기록에 - NetGuard.reportError) content: Text(trf('DNS 서버 ({0}) 에 닿지 않아 주소를 찾지 못했습니다. 인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.', [f.server])), diff --git a/lib/ui/network_settings.dart b/lib/ui/network_settings.dart index 7ab8418..678299f 100644 --- a/lib/ui/network_settings.dart +++ b/lib/ui/network_settings.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import '../app/app_controller.dart'; +import '../app/net_guard.dart'; import '../app/settings.dart'; import '../app/tor_service.dart'; import '../app/web_proxy.dart'; @@ -13,6 +14,34 @@ import 'setting_tile.dart'; import 'theme.dart'; import 'tor_page.dart' show showTorNotice; +/// 249: DNS 서버에 닿지 않는 동안 보이는 줄 (환경 설정 > 네트워크 · DNS · 작업 현황) - 아래 안내를 놓쳐도 찾을 수 있게. +/// 서버가 다시 답하거나 DNS 설정을 바꾸면 사라진다. +class DnsFailureLine extends StatelessWidget { + final AppController c; + const DnsFailureLine({super.key, required this.c}); + + @override + Widget build(BuildContext context) => ValueListenableBuilder( + valueListenable: NetGuard.instance.failure, + builder: (context, f, _) { + if (f == null) return const SizedBox.shrink(); + String two(int x) => x.toString().padLeft(2, '0'); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ + const Icon(Icons.warning_amber_rounded, size: 18, color: Colors.orange), + Text(trf('DNS 서버 ({0}) 에 닿지 않음 ({1}부터)', [f.server, '${two(f.at.hour)}:${two(f.at.minute)}']), + style: const TextStyle(fontSize: 12, color: Colors.orange)), + TextButton( + onPressed: () => c.updateSettings((x) => x.dnsMode = 'system'), + child: Text(tr('시스템 DNS 로 바꾸기')), + ), + ]), + ); + }, + ); +} + /// 환경 설정 > 네트워크 · DNS (사용자 요청 10/10): DNS 서버 강제 (처음: 직접 지정 8.8.8.8 · 8.8.4.4) class DnsSettings extends StatefulWidget { final AppController c; @@ -68,6 +97,7 @@ class _DnsSettingsState extends State { final s = c.settings; final desk = Platform.isWindows; return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + DnsFailureLine(c: c), SettingTile( leading: const Icon(Icons.dns_outlined), title: Text(tr('DNS 서버')), diff --git a/lib/ui/update_dialog.dart b/lib/ui/update_dialog.dart index b679951..3dcbb90 100644 --- a/lib/ui/update_dialog.dart +++ b/lib/ui/update_dialog.dart @@ -8,6 +8,7 @@ import '../app/ai_local.dart' show AiStore; import '../app/app_controller.dart'; import '../app/copy_center.dart' show diskSpace; import '../app/model_backup.dart'; +import '../app/net_guard.dart'; import '../app/version_snapshot.dart'; import '../core/app_update.dart'; import '../services/updater.dart' show InstallPermissionNeeded, Updater; @@ -44,7 +45,10 @@ Future checkForUpdate(BuildContext context, AppController c, {bool manual r = await up.latest(); } catch (e) { c.note(trf('새 버전을 확인할 수 없습니다: {0}', [e])); - if (manual) snack(trf('새 버전을 확인할 수 없습니다: {0}', [e])); + // 247: DNS 강제 프록시에서 실패했으면 까닭을 사용자 글로 (원문은 위 작업 기록에) + if (manual) { + snack(NetGuard.instance.explain(e, tr('새 버전을 확인하지 못했습니다')) ?? trf('새 버전을 확인할 수 없습니다: {0}', [e])); + } return; } await c.updateSettings((x) => x diff --git a/lib/ui/work_panel.dart b/lib/ui/work_panel.dart index 2b83a88..b031de2 100644 --- a/lib/ui/work_panel.dart +++ b/lib/ui/work_panel.dart @@ -8,6 +8,7 @@ import '../services/downloader.dart'; import 'ai_upscale_ui.dart' show openFolderInApp; import 'app_actions.dart'; import 'downloads_page.dart'; +import 'network_settings.dart' show DnsFailureLine; import 'theme.dart'; import '../l10n/tr.dart'; @@ -186,6 +187,8 @@ class WorkPanel extends StatelessWidget { ); List _jobs(BuildContext context) => [ + // 249: DNS 서버에 닿지 않음 (안내를 놓쳐도 여기서 보고 바꿀 수 있게) + DnsFailureLine(c: c), _title(tr('작업')), // 123: AI 그림 (남은 장 · 예상 시간) if (AiJobs.instance.statusLine case final ai?) ...[ diff --git a/test/dns_proxy_test.dart b/test/dns_proxy_test.dart index 1581a43..f8881ff 100644 --- a/test/dns_proxy_test.dart +++ b/test/dns_proxy_test.dart @@ -460,4 +460,54 @@ void main() { // Tor 설정 처음 값 expect((s.torKeepData, s.torHomeUrl, s.torJavaScript, s.torNoticeSeen), (false, 'https://check.torproject.org/', true, false)); }); + + test('247 · 249: 닿지 않는 DNS - 앱의 HTTP 오류를 사용자 글로 · 상태는 다시 답하거나 설정을 바꿀 때까지 남음', () async { + final g = NetGuard.instance; + final old = HttpOverrides.current; + HttpOverrides.global = JjHttpOverrides(); + addTearDown(() async { + HttpOverrides.global = old; + g.config = () => const DnsConfig.system(); + await g.ensure(); + }); + var shown = 0; + void count() { + if (g.failure.value != null) shown++; + } + + g.failure.addListener(count); + addTearDown(() => g.failure.removeListener(count)); + g.config = () => custom('192.0.2.1'); + await g.ensure(); + expect(g.failure.value, isNull); + // 새 버전 확인처럼 https 로 (프록시 CONNECT 가 502 → HttpException "Proxy failed to establish tunnel") + final client = HttpClient(); + addTearDown(client.close); + Object? err; + try { + await (await client.getUrl(Uri.parse('https://api.github.com/repos/x/y/releases/latest'))).close(); + } catch (e) { + err = e; + } + expect('$err', contains('Proxy failed to establish tunnel')); + final text = g.explain(err!, '새 버전을 확인하지 못했습니다'); + expect(text, 'DNS 서버 (192.0.2.1) 에 닿지 않아 새 버전을 확인하지 못했습니다'); + expect(text, isNot(contains('HttpException'))); + // 프록시 실패가 아니면 원래 글 (null) + expect(g.explain(const SocketException('x'), '새 버전을 확인하지 못했습니다'), isNull); + + // 상태가 남고 안내는 한 번 (또 실패해도 다시 띄우지 않음) + expect(g.failure.value?.server, '192.0.2.1'); + g.reportError('www.example.com', const DnsException('again', unreachable: true, server: '192.0.2.1')); + expect(shown, 1); + // 서버가 다시 답하면 지운다 + g.resolver.onAnswer?.call('www.example.com', '192.0.2.1'); + expect(g.failure.value, isNull); + g.reportError('www.example.com', const DnsException('again', unreachable: true, server: '192.0.2.1')); + expect(shown, 2); + // DNS 설정을 바꾸면 지운다 + g.config = () => const DnsConfig.system(); + await g.ensure(); + expect(g.failure.value, isNull); + }); } diff --git a/test/tor_test.dart b/test/tor_test.dart index ecba8d9..a2cf492 100644 --- a/test/tor_test.dart +++ b/test/tor_test.dart @@ -9,6 +9,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/app/component_store.dart'; import 'package:jj_mkvmaker/app/components.dart'; +import 'package:jj_mkvmaker/app/net_guard.dart'; import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/app/tor_bundle.dart'; import 'package:jj_mkvmaker/app/tor_service.dart'; @@ -362,6 +363,24 @@ void main() { expect(find.textContaining('Tor 대기 중'), findsNothing); }); + testWidgets('249: DNS 서버에 닿지 않는 동안 환경 설정 > 네트워크 · DNS 에 상태 줄 · [시스템 DNS 로 바꾸기]', (t) async { + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + final g = NetGuard.instance; + addTearDown(() => g.failure.value = null); + await t.pumpWidget(MaterialApp(home: Scaffold(body: SingleChildScrollView(child: DnsSettings(c: c))))); + expect(find.textContaining('에 닿지 않음'), findsNothing); + 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'); + await t.tap(find.text('시스템 DNS 로 바꾸기')); + await t.pump(); + expect(c.settings.dnsMode, 'system'); + g.failure.value = null; + await t.pump(); + expect(find.textContaining('에 닿지 않음'), findsNothing); + }); + testWidgets('파일 링크를 누르면 받지 않고 이유를 알린다 (아무 반응 없음이 아니게)', (t) async { final loads = []; final hosts = [];