JJ_MKVMaker/test/tor_test.dart

382 lines
18 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:archive/archive.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
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/settings.dart';
import 'package:jj_mkvmaker/app/tor_bundle.dart';
import 'package:jj_mkvmaker/app/tor_service.dart';
import 'package:jj_mkvmaker/core/tor_control.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/app_actions.dart';
import 'package:jj_mkvmaker/ui/tor_page.dart';
import 'package:jj_mkvmaker/ui/network_settings.dart';
void main() {
test('tor 알림 줄 읽기 · torrc', () {
expect(parseBootstrap('Oct 10 19:16:01.000 [notice] Bootstrapped 45% (loading_descriptors): Loading relay descriptors'),
(45, 'Loading relay descriptors'));
expect(parseBootstrap('[notice] Bootstrapped 100% (done): Done')?.$1, 100);
expect(parseBootstrap('[notice] Opening Socks listener on 127.0.0.1:0'), isNull);
expect(parseSocksPort('[notice] Opened Socks listener connection (ready) on 127.0.0.1:53017'), 53017);
final rc = buildTorrc(dataDir: r'C:\d\tor_data', controlPortFile: r'C:\d\tor_data\cp.txt', ownerPid: 1234, geoip: r'C:\g\geoip');
expect(rc, contains('SocksPort 127.0.0.1:auto'));
expect(rc, contains('ControlPort 127.0.0.1:auto'));
expect(rc, contains('CookieAuthentication 1'));
expect(rc, contains('__OwningControllerProcess 1234'));
expect(rc, contains(r'GeoIPFile C:\g\geoip'));
expect(rc, isNot(contains('GeoIPv6File')));
});
test('새는 것 막기: Tor 100% · 프록시 · 전용 프로필이 모두 되어야 웹 뷰를 만든다', () {
const ready = TorStatus(TorPhase.ready, percent: 100);
expect(torViewMayLoad(status: ready, proxyApplied: true, profileReady: true), isTrue);
expect(torViewMayLoad(status: ready, proxyApplied: false, profileReady: true), isFalse);
expect(torViewMayLoad(status: ready, proxyApplied: true, profileReady: false), isFalse);
for (final s in [TorStatus.off, const TorStatus(TorPhase.starting, percent: 99), const TorStatus(TorPhase.failed)]) {
expect(torViewMayLoad(status: s, proxyApplied: true, profileReady: true), isFalse, reason: '${s.phase}');
}
});
test('Tor 검색은 DuckDuckGo (Google 로 보내지 않음), 주소는 그대로', () {
expect(torAddress('tor project'), 'https://duckduckgo.com/?q=tor+project');
expect(torAddress('check.torproject.org'), 'https://check.torproject.org');
expect(torAddress('http://example.onion/'), 'http://example.onion/');
});
test('Edge 실행 인수: 모든 연결 (localhost 포함) 을 Tor 다리로 · WebRTC UDP 막기 · QUIC 끄기', () {
final a = torBrowserArguments(41234);
expect(a, contains('--proxy-server=http://127.0.0.1:41234'));
expect(a, contains('--proxy-bypass-list=<-loopback>'));
expect(a, contains('--force-webrtc-ip-handling-policy=disable_non_proxied_udp'));
expect(a, contains('--disable-quic'));
expect(torWebRtcBlockScript, contains('RTCPeerConnection'));
});
test('컴포넌트: Tor 는 처음 꺼짐 · 화면은 웹 브라우저 바로 뒤 (예전 순서에도)', () {
expect(AppComponent.tor.builtIn, isFalse);
expect(AppComponent.defaultInstalled, isNot(contains('tor')));
expect(AppComponent.defaultOrder.indexOf('tor'), AppComponent.defaultOrder.indexOf('browser') + 1);
final ids = [for (final p in AppComponent.pages(['mkv', 'browser', 'tor', 'explorer'], ['explorer', 'mkv', 'browser'])) p.id];
expect(ids, ['explorer', 'mkv', 'browser', 'tor']);
expect([for (final p in AppComponent.pages(['mkv', 'browser', 'tor'], ['browser', 'mkv'])) p.id], ['browser', 'tor', 'mkv']);
final s = AppSettings()..startScreen = 'tor';
expect(AppNavButtons.homeId(s), 'mkv', reason: '설치하지 않았으면 첫 화면');
s.components = [...s.components, 'tor'];
expect(AppNavButtons.homeId(s), 'tor');
});
group('Tor 컴포넌트 (Windows 번들)', () {
test('판 · SHA-256 고정 (공식 sha256sums-signed-build.txt)', () {
final hex = RegExp(r'^[0-9a-f]{64}$');
expect(TorBundle.windowsSha256, matches(hex));
expect(TorBundle.androidSha256, matches(hex));
expect(TorBundle.windowsSha256, 'e9dc6ccc93cd6afa507193f4de284d6424233ff5102155cd2c94b259e8a22b65');
expect(TorBundle.windowsFile, 'tor-expert-bundle-windows-x86_64-${TorBundle.version}.tar.gz');
for (final u in TorBundle.urls(TorBundle.windowsFile)) {
expect(u, startsWith('https://'));
expect(u, contains('torproject.org/'));
expect(u, endsWith('/${TorBundle.version}/${TorBundle.windowsFile}'));
}
// Android 준비 스크립트도 같은 판 · 같은 SHA-256
final ps = File('tool/fetch_tor_android.ps1').readAsStringSync();
expect(ps, contains("'${TorBundle.version}'"));
expect(ps, contains(TorBundle.androidSha256));
});
late HttpServer server;
late Directory tmp;
late List<int> bundle;
setUp(() async {
// 이 파일의 testWidgets 가 HttpClient 를 막아 둔다 - 이 시험은 실제 (로컬) 서버에 연결
HttpOverrides.global = null;
tmp = Directory.systemTemp.createTempSync('jj_tor_test_');
final a = Archive()
..addFile(ArchiveFile.bytes('tor/tor.exe', [77, 90, 1, 2]))
..addFile(ArchiveFile.bytes('data/geoip', [1]))
..addFile(ArchiveFile.bytes('docs/tor.txt', 'Tor license'.codeUnits));
bundle = GZipEncoder().encode(TarEncoder().encode(a));
server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
server.listen((req) async {
req.response.add(bundle);
await req.response.close();
});
});
tearDown(() async {
await server.close(force: true);
tmp.deleteSync(recursive: true);
});
test('SHA-256 이 다르면 설치하지 않는다', () async {
final target = '${tmp.path}${Platform.pathSeparator}tor';
await expectLater(
TorBundle.installWindows(
urlsOverride: ['http://127.0.0.1:${server.port}/b.tar.gz'], sha256Override: '0' * 64, targetOverride: target),
throwsA(isA<ComponentException>()),
);
expect(Directory(target).existsSync(), isFalse);
});
test('맞으면 tor.exe · geoip · 라이선스 문서 · 설치 표시 (판)', () async {
final target = '${tmp.path}${Platform.pathSeparator}tor';
await TorBundle.installWindows(
urlsOverride: ['http://127.0.0.1:${server.port}/b.tar.gz'],
sha256Override: sha256.convert(bundle).toString(),
targetOverride: target);
for (final f in ['tor.exe', 'geoip', 'docs/tor.txt', 'installed.json']) {
expect(File('$target/$f').existsSync(), isTrue, reason: f);
}
expect(File('$target/installed.json').readAsStringSync(), contains('"version":"${TorBundle.version}"'));
}, skip: Platform.isWindows ? false : 'Windows tar');
});
group('Tor 화면: 준비되기 전에는 아무것도 읽지 않는다', () {
late Directory tmp;
setUp(() => tmp = Directory.systemTemp.createTempSync('jj_tor_page_'));
tearDown(() {
TorService.instance.status.value = TorStatus.off;
tmp.deleteSync(recursive: true);
});
Future<AppController> open(WidgetTester t,
{required Future<void> Function() startTor,
Future<List<String>> Function()? missing,
required Future<bool> Function() applyProxy,
required List<String> loads,
List<TorViewHost>? hosts,
Future<void> Function()? onLeft,
GlobalKey<NavigatorState>? navKey}) async {
t.view.physicalSize = const Size(1400, 900);
t.view.devicePixelRatio = 1;
addTearDown(t.view.reset);
const pp = MethodChannel('plugins.flutter.io/path_provider');
t.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, (_) async => tmp.path);
addTearDown(() => t.binding.defaultBinaryMessenger.setMockMethodCallHandler(pp, null));
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
c.settings
..torNoticeSeen = true
..components = [...c.settings.components, 'tor'];
await t.pumpWidget(MaterialApp(
navigatorKey: navKey,
builder: (context, child) => AppScope(controller: c, child: child!),
home: TorPage(
c: c,
onLeft: onLeft,
startTor: startTor,
missingFeatures: missing ?? () async => const [],
applyProxy: applyProxy,
viewBuilder: (host, url) {
loads.add(url);
hosts?.add(host);
return Text('VIEW $url');
},
),
));
await t.pump();
return c;
}
testWidgets('연결 중 → 100% → 프록시 적용 뒤에야 웹 뷰', (t) async {
final loads = <String>[];
final proxy = Completer<bool>();
final st = TorService.instance.status;
await open(t,
startTor: () async => st.value = const TorStatus(TorPhase.starting, percent: 10, message: 'x'),
applyProxy: () => proxy.future,
loads: loads);
await t.pump();
expect(find.textContaining('Tor 연결 중 (10%)'), findsOneWidget);
expect(loads, isEmpty);
st.value = const TorStatus(TorPhase.starting, percent: 90);
await t.pump();
expect(loads, isEmpty);
st.value = const TorStatus(TorPhase.ready, percent: 100);
await t.pump();
expect(loads, isEmpty, reason: '프록시를 걸기 전');
proxy.complete(true);
await t.pump();
await t.pump();
expect(loads, ['https://check.torproject.org/']);
expect(find.text('Tor 연결됨'), findsOneWidget);
// Tor 가 끊기면 웹 뷰를 치운다 (다리는 Tor 없이 잇지 않음)
st.value = const TorStatus(TorPhase.failed, message: 'x');
await t.pump();
expect(find.textContaining('VIEW'), findsNothing);
expect(find.text('다시 시도'), findsOneWidget);
});
testWidgets('프록시를 걸지 못하면 (Android 웹 뷰 프록시 실패) 읽지 않는다', (t) async {
final loads = <String>[];
await open(t,
startTor: () async => TorService.instance.status.value = const TorStatus(TorPhase.ready, percent: 100),
applyProxy: () async => false,
loads: loads);
await t.pump();
await t.pump();
expect(loads, isEmpty);
});
testWidgets('웹 뷰에 필요한 기능이 없으면 Tor 를 켜지 않고 안내만', (t) async {
final loads = <String>[];
var started = false;
await open(t,
startTor: () async {
started = true;
TorService.instance.status.value = const TorStatus(TorPhase.ready, percent: 100);
},
missing: () async => ['PROXY_OVERRIDE'],
applyProxy: () async => true,
loads: loads);
await t.pump();
expect(started, isFalse);
expect(loads, isEmpty);
expect(find.textContaining('PROXY_OVERRIDE'), findsOneWidget);
});
testWidgets('다른 화면으로 가면 웹 뷰를 바로 치우고 정리 · 돌아오면 홈부터 다시', (t) async {
final loads = <String>[];
var left = 0, applied = 0;
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
final nav = GlobalKey<NavigatorState>();
final c = await open(t,
startTor: () async => TorService.instance.status.value = const TorStatus(TorPhase.ready, percent: 100),
applyProxy: () async {
applied++;
return true;
},
loads: loads,
onLeft: () async => left++,
navKey: nav);
await t.pump();
await t.pump();
expect(find.textContaining('VIEW'), findsOneWidget);
expect(applied, 1);
// 다른 화면 (예: 일반 브라우저) 이 위로
nav.currentState!.push(MaterialPageRoute<void>(builder: (_) => const Scaffold(body: Text('other'))));
await t.pumpAndSettle();
expect(left, 1, reason: '떠나자마자 정리');
expect(find.textContaining('VIEW', skipOffstage: false), findsNothing, reason: '웹 뷰를 두지 않음');
// 돌아오면 홈부터 다시 (프록시도 다시 건 뒤에)
nav.currentState!.pop();
await t.pumpAndSettle();
expect(applied, 2);
expect(loads.last, c.settings.torHomeUrl);
expect(find.textContaining('VIEW'), findsOneWidget);
// 앱이 뒤로 가도 같다
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.hidden);
await t.pump();
await t.pump();
// 앱이 뒤에 있으면 화면을 그리지 않으므로 웹 뷰는 돌아와서 빠진다 - 정리 (프록시 · 다리 · 프로필) 는 바로
expect(left, 2);
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive);
t.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed);
await t.pump();
await t.pump();
expect(applied, 3, reason: '돌아오면 프록시를 다시 건 뒤에');
expect(loads.last, c.settings.torHomeUrl);
expect(find.textContaining('VIEW'), findsOneWidget);
});
testWidgets('떠난 뒤 tor 는 정한 시간 뒤에 끈다 · 돌아오면 그대로 · 0 은 바로 (가짜 시간)', (t) async {
final s = TorService.instance;
addTearDown(() => s.debugRunning = false);
s.debugRunning = true;
s.status.value = const TorStatus(TorPhase.ready, percent: 100);
await s.stopAfter(const Duration(minutes: 2));
expect(s.idleStopAt.value, isNotNull);
await t.pump(const Duration(minutes: 1, seconds: 59));
expect(s.running, isTrue);
await t.pump(const Duration(seconds: 2));
expect(s.running, isFalse);
expect(s.idleStopAt.value, isNull);
expect(s.status.value.phase, TorPhase.off);
// 그 사이 돌아오면 그대로 켜 둔다
s.debugRunning = true;
s.status.value = const TorStatus(TorPhase.ready, percent: 100);
await s.stopAfter(const Duration(minutes: 2));
await t.pump(const Duration(minutes: 1));
s.cancelIdleStop();
await t.pump(const Duration(minutes: 5));
expect(s.running, isTrue);
// 0 = 바로
await s.stopAfter(Duration.zero);
expect(s.running, isFalse);
});
test('설정: Tor 를 켜 둘 시간 (처음 2분, 틀린 값은 2분)', () {
expect(AppSettings().torIdleMinutes, 2);
expect(AppSettings.fromJson({'torIdleMinutes': 0}).torIdleMinutes, 0);
expect(AppSettings.fromJson({'torIdleMinutes': 5}).torIdleMinutes, 5);
expect(AppSettings.fromJson({'torIdleMinutes': -1}).torIdleMinutes, 2);
expect(AppSettings.fromJson({'torIdleMinutes': 'x'}).torIdleMinutes, 2);
});
testWidgets('227: 막은 파일 주소는 주소 칸에 남지 않고 보이던 페이지 주소로', (t) async {
final loads = <String>[];
final hosts = <TorViewHost>[];
await open(t,
startTor: () async => TorService.instance.status.value = const TorStatus(TorPhase.ready, percent: 100),
applyProxy: () async => true,
loads: loads,
hosts: hosts);
await t.pump();
await t.pump();
final h = hosts.last;
h.onUrl('https://check.torproject.org/');
h.onShown?.call('https://check.torproject.org/');
// 파일 링크: 읽기 시작에 주소 칸이 파일 주소로 바뀐 뒤 막힘
h.onUrl('https://dist.torproject.org/x.tar.gz.asc');
await t.pump();
expect(find.text('https://dist.torproject.org/x.tar.gz.asc'), findsOneWidget);
h.onDownloadBlocked('https://dist.torproject.org/x.tar.gz.asc');
await t.pump();
expect(find.text('https://dist.torproject.org/x.tar.gz.asc'), findsNothing);
expect(find.text('https://check.torproject.org/'), findsOneWidget);
});
testWidgets('226: 떠난 뒤 tor 를 켜 두는 동안 환경 설정 > Tor 에 "Tor 대기 중 (n분 뒤 끔)" · [지금 끄기]', (t) async {
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
final s = TorService.instance;
addTearDown(() async {
s.cancelIdleStop();
s.debugRunning = false;
});
await t.pumpWidget(MaterialApp(home: Scaffold(body: SingleChildScrollView(child: TorSettings(c: c)))));
expect(find.textContaining('Tor 대기 중'), findsNothing);
s.debugRunning = true;
await s.stopAfter(const Duration(minutes: 2));
await t.pump();
expect(find.text('Tor 대기 중 (2분 뒤 끔)'), findsOneWidget);
await t.tap(find.text('지금 끄기'));
await t.pump();
expect(s.running, isFalse);
expect(find.textContaining('Tor 대기 중'), findsNothing);
});
testWidgets('파일 링크를 누르면 받지 않고 이유를 알린다 (아무 반응 없음이 아니게)', (t) async {
final loads = <String>[];
final hosts = <TorViewHost>[];
await open(t,
startTor: () async => TorService.instance.status.value = const TorStatus(TorPhase.ready, percent: 100),
applyProxy: () async => true,
loads: loads,
hosts: hosts);
await t.pump();
await t.pump();
expect(hosts, isNotEmpty);
hosts.last.onDownloadBlocked('https://example.com/a.zip');
await t.pump();
expect(find.text('Tor 화면에서는 파일을 받지 않습니다 (Tor 밖으로 나가지 않게)'), findsOneWidget);
});
});
}