import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jj_mkvmaker/app/download_manager.dart'; import 'package:jj_mkvmaker/app/i18n_controller.dart'; import 'package:jj_mkvmaker/app/net_guard.dart'; import 'package:jj_mkvmaker/app/settings.dart'; import 'package:jj_mkvmaker/core/dns_resolver.dart'; import 'package:jj_mkvmaker/core/download_detect.dart'; import 'package:jj_mkvmaker/services/downloader.dart'; import 'package:jj_mkvmaker/ui/downloads_page.dart'; class _Backend implements DownloadBackend { var starts = 0; final String error; _Backend([this.error = 'ERROR: [generic] Unable to download webpage: HTTP Error 404: Not Found ' '(https://cdn.example.com/v.mp4?sig=SECRET123&expire=1)']); @override DownloadKind get kind => DownloadKind.video; @override Future<(String, List)?> expandPlaylist(String url) async => null; @override Future start(DownloadTask t, void Function() changed) async { starts++; t ..state = DownloadState.failed ..error = error; changed(); } @override Future pause(DownloadTask t) async {} @override Future cancel(DownloadTask t) async {} @override Future shutdown() async {} } /// 136: 실패한 다운로드 줄 - 진행 막대 대신 실패 · 사람 말 이유 · [자세히] (원문) · [다시 시도] void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('yt-dlp · aria2 원문 → 사람 말 이유', () { expect(friendlyDownloadError('ERROR: [generic] x: HTTP Error 404: Not Found'), '주소에 영상이 없습니다 (404)'); expect(friendlyDownloadError('ERROR: [vimeo] 1: HTTP Error 403: Forbidden'), '로그인이 필요합니다 (403)'); expect(friendlyDownloadError('ERROR: Unsupported URL: https://example.com/'), '지원하지 않는 사이트입니다'); expect(friendlyDownloadError('ERROR: [generic] Unable to download webpage: '), '연결할 수 없습니다'); expect(friendlyDownloadError('Network problem has occurred. cause:Connection refused (errorCode=6)'), '연결할 수 없습니다'); expect(friendlyDownloadError('Resource not found (errorCode=3)'), '주소에 영상이 없습니다 (404)'); expect(friendlyDownloadError('There is not enough disk space available (errorCode=9)'), '저장 공간이 모자랍니다'); expect(friendlyDownloadError('ERROR: [youtube] x: Sign in to confirm you’re not a bot'), contains('로봇 확인')); expect(friendlyDownloadError('무언가 이상함'), '받지 못했습니다'); }); test('화면 언어를 따른다 (59 와 같은 방식)', () async { await i18n.apply('ja', save: false); addTearDown(() => i18n.apply('ko', save: false)); expect(friendlyDownloadError('HTTP Error 404: Not Found'), 'このアドレスに動画がありません (404)'); }); test('서명 · 키가 든 주소 부분은 가린다', () { expect(redactUrlSecrets('x https://cdn.example.com/v.mp4?sig=AAA&expire=1 y'), 'x https://cdn.example.com/v.mp4?… y'); expect(redactUrlSecrets('(https://user:pw@nas.local/dav/a.mkv#t=1)'), '(https://nas.local/dav/a.mkv?…)'); expect(redactUrlSecrets('https://www.youtube.com/watch'), 'https://www.youtube.com/watch'); }); testWidgets('실패한 줄: 진행 막대 ("준비 중…") 없이 이유 · [자세히] 는 원문 (주소의 서명은 가림) · [다시 시도]', (tester) async { tester.view.physicalSize = const Size(1400, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final b = _Backend(); final d = DownloadManager(backends: [b], settings: () => AppSettings(), readClipboard: () async => null); final t = d.addPage('https://cdn.example.com/page')!..title = '영상'; await tester.pumpWidget(MaterialApp(home: DownloadsPage(d: d))); for (var i = 0; i < 20 && t.state != DownloadState.failed; i++) { await tester.pump(const Duration(milliseconds: 50)); } expect(t.state, DownloadState.failed); await tester.pump(); expect(find.text('준비 중…'), findsNothing); expect(find.byType(DownloadProgressBar), findsNothing); expect(find.textContaining('주소에 영상이 없습니다 (404)'), findsOneWidget); expect(find.textContaining('HTTP Error'), findsNothing, reason: '원문은 [자세히] 에서만'); await tester.tap(find.text('자세히')); await tester.pumpAndSettle(); expect(find.textContaining('HTTP Error 404'), findsOneWidget); expect(find.textContaining('SECRET123'), findsNothing, reason: '서명된 주소를 보이지 않음'); await tester.tap(find.text('닫기')); await tester.pumpAndSettle(); expect(b.starts, 1); await tester.tap(find.text('다시 시도')); for (var i = 0; i < 20 && b.starts < 2; i++) { await tester.pump(const Duration(milliseconds: 50)); } expect(b.starts, 2, reason: '[다시 시도] 로 다시 받기 시작 (이 가짜 백엔드는 또 실패)'); d.dispose(); }); // 263: DNS 강제 프록시를 거친 yt-dlp 는 "Tunnel connection failed" 만 남겨, 이유를 몰랐다 (42 · a4) group('263 DNS 서버에 닿지 않아 실패한 다운로드', () { final g = NetGuard.instance; late DnsConfig Function() before; setUp(() { before = g.config; g.config = () => const DnsConfig(mode: 'custom', primary: '192.0.2.1'); }); tearDown(() { g.config = before; g.failure.value = null; }); DownloadTask failed(String error) => DownloadTask(id: '1', kind: DownloadKind.video, source: 'https://x.example/v', dir: '.') ..state = DownloadState.failed ..error = error; const tunnel = "ERROR: [generic] Unable to download webpage: ('Unable to connect to proxy', OSError('Tunnel connection failed: 502 Bad Gateway'))"; test('연결 · 프록시 실패이고 지금 닿지 않음일 때만 DNS 탓', () { expect(DownloadManager.dnsCause(failed(tunnel)), isNull, reason: '닿지 않음 상태가 아니면 아님'); g.failure.value = DnsFailure('192.0.2.1', 'x', DateTime.now()); expect(DownloadManager.dnsCause(failed(tunnel)), '192.0.2.1'); expect(DownloadManager.dnsCause(failed('Network problem has occurred. cause:Connection refused (errorCode=6)')), '192.0.2.1'); expect(DownloadManager.dnsCause(failed('ERROR: [generic] x: HTTP Error 404: Not Found')), isNull, reason: '404 는 DNS 탓이 아님'); g.config = () => const DnsConfig.system(); expect(DownloadManager.dnsCause(failed(tunnel)), isNull, reason: '시스템 DNS 면 강제 프록시가 없다'); }); testWidgets('줄에 이유 · [시스템 DNS 로 바꾸기] - 누르면 설정이 시스템 DNS 가 되고 버튼은 사라진다 (다시 받기는 사용자가)', (tester) async { tester.view.physicalSize = const Size(1400, 900); tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); g.failure.value = DnsFailure('192.0.2.1', 'x', DateTime.now()); final s = AppSettings(); final b = _Backend(tunnel); final d = DownloadManager(backends: [b], settings: () => s, readClipboard: () async => null); d.useSystemDns = () async { s.network.mode = 'system'; d.refresh(); }; final t = d.addPage('https://cdn.example.com/page')!..title = '영상'; await tester.pumpWidget(MaterialApp(home: DownloadsPage(d: d))); for (var i = 0; i < 20 && t.state != DownloadState.failed; i++) { await tester.pump(const Duration(milliseconds: 50)); } await tester.pump(); expect(find.textContaining('DNS 서버 (192.0.2.1) 에 닿지 않아 받지 못했습니다'), findsOneWidget); expect(find.text('시스템 DNS 로 바꾸기'), findsOneWidget); await tester.tap(find.text('시스템 DNS 로 바꾸기')); await tester.pump(); expect(s.network.mode, 'system'); expect(find.text('시스템 DNS 로 바꾸기'), findsNothing); expect(b.starts, 1, reason: '몰래 다시 받지 않는다'); await tester.tap(find.text('다시 시도')); for (var i = 0; i < 20 && b.starts < 2; i++) { await tester.pump(const Duration(milliseconds: 50)); } expect(b.starts, 2); d.dispose(); }); }); }