import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:proxy/proxy.dart'; import 'package:proxy/proxy_method_channel.dart'; import 'package:proxy/src/linux_proxy.dart'; import 'package:proxy/src/macos_proxy.dart'; import 'package:proxy/src/proxy_command.dart'; import 'package:proxy/src/windows_proxy.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('Proxy', () { test('rejects ports outside the TCP port range', () async { final proxy = Proxy( processRunner: (executable, arguments, {runInShell = false}) async { fail('invalid ports must not execute platform commands'); }, ); expect(await proxy.startProxy(0), isFalse); expect(await proxy.startProxy(65536), isFalse); }); }); group('ProxyCommandRunner', () { test('treats an empty command set as failure', () async { final runner = ProxyCommandRunner(( executable, arguments, { runInShell = false, }) async { fail('empty command sets must not start a process'); }); expect(await runner.run(const []), isFalse); }); test('stops after the first failed command', () async { var callCount = 0; final runner = ProxyCommandRunner(( executable, arguments, { runInShell = false, }) async { callCount++; return ProcessResult(callCount, callCount == 1 ? 1 : 0, '', ''); }); final result = await runner.run([ ProxyCommand('first', const []), ProxyCommand('second', const []), ]); expect(result, isFalse); expect(callCount, 1); }); test('converts process launch failures to false', () async { final runner = ProxyCommandRunner(( executable, arguments, { runInShell = false, }) async { throw ProcessException(executable, arguments); }); expect(await runner.run([ProxyCommand('missing', const [])]), isFalse); }); test('carries on past an optional command that fails', () async { final started = []; final runner = ProxyCommandRunner(( executable, arguments, { runInShell = false, }) async { started.add(executable); if (executable == 'missing') { throw ProcessException(executable, arguments); } return ProcessResult(0, executable == 'refused' ? 1 : 0, '', ''); }); final result = await runner.run([ ProxyCommand('missing', const [], optional: true), ProxyCommand('refused', const [], optional: true), ProxyCommand('last', const []), ]); expect(result, isTrue); expect(started, ['missing', 'refused', 'last']); }); }); group('Linux proxy command builders', () { test( 'checks each fallback command only once for unknown desktops', () async { final checkedExecutables = []; final executedCommands = []; final proxy = LinuxProxy( commandRunner: ProxyCommandRunner(( executable, arguments, { runInShell = false, }) async { executedCommands.add(executable); return ProcessResult(1, 0, '', ''); }), executableChecker: (executable) async { checkedExecutables.add(executable); return executable == 'kwriteconfig5'; }, ); final result = await proxy.start( 7890, const ['localhost'], desktop: 'UNKNOWN', homeDir: '/home/user', ); expect(result, isTrue); expect(checkedExecutables, [ 'gsettings', 'kwriteconfig6', 'kwriteconfig5', ]); expect(executedCommands.toSet(), {'kwriteconfig5', 'dbus-send'}); }, ); test('builds GNOME commands without duplicate port writes', () { final commands = LinuxProxyCommands.buildStart( port: 7890, bypassDomain: ['localhost', '127.0.0.1'], desktop: 'GNOME', homeDir: '/home/user', ); final portCommands = commands.where( (command) => command.args.length == 4 && command.args[2] == 'port', ); final hostCommands = commands.where( (command) => command.args.length == 4 && command.args[2] == 'host', ); expect(portCommands, hasLength(3)); expect(hostCommands, hasLength(3)); expect(commands.last.args, [ 'set', 'org.gnome.system.proxy', 'mode', 'manual', ]); expect( commands .singleWhere( (command) => command.args.contains('org.gnome.system.proxy') && command.args.contains('ignore-hosts'), ) .args .last, "['localhost', '127.0.0.1']", ); }); test('builds empty GNOME ignore-hosts as an empty list', () { final commands = LinuxProxyCommands.buildStart( port: 7890, bypassDomain: const [], desktop: 'GNOME', homeDir: '/home/user', ); expect( commands .singleWhere( (command) => command.args.contains('org.gnome.system.proxy') && command.args.contains('ignore-hosts'), ) .args .last, '[]', ); }); test('escapes backslashes and quotes in GNOME ignore-hosts', () { final commands = LinuxProxyCommands.buildStart( port: 7890, bypassDomain: [r'local\host', "it's"], desktop: 'GNOME', homeDir: '/home/user', ); expect( commands .singleWhere((command) => command.args.contains('ignore-hosts')) .args .last, r"['local\\host', 'it\'s']", ); }); test('builds MATE commands with MATE proxy schema', () { final commands = LinuxProxyCommands.buildStart( port: 7890, bypassDomain: ['localhost'], desktop: 'MATE', homeDir: '/home/user', ); expect( commands.any( (command) => command.args.contains('org.mate.system.proxy'), ), isTrue, ); expect( commands.any( (command) => command.args.contains('org.gnome.system.proxy'), ), isFalse, ); }); test('falls back to GNOME gsettings commands for XFCE when available', () { final commands = LinuxProxyCommands.buildStart( port: 7890, bypassDomain: ['localhost'], desktop: 'XFCE', homeDir: '/home/user', availableExecutables: {'gsettings'}, ); expect(commands.map((command) => command.executable).toSet(), { 'gsettings', }); expect( commands.any( (command) => command.args.contains('org.gnome.system.proxy') && command.args.contains('manual'), ), isTrue, ); }); test('prefers kwriteconfig6 for KDE when available', () { final commands = LinuxProxyCommands.buildStart( port: 7890, bypassDomain: ['localhost'], desktop: 'KDE', homeDir: '/home/user', availableExecutables: {'kwriteconfig6', 'kwriteconfig5'}, ); expect(commands.map((command) => command.executable).toSet(), { 'kwriteconfig6', 'dbus-send', }); }); test('points every KDE proxy at the plain HTTP or SOCKS listener and ' 'tells running KIO programs to reload', () { for (final commands in [ LinuxProxyCommands.buildStart( port: 7890, bypassDomain: ['localhost'], desktop: 'KDE', homeDir: '/home/user', availableExecutables: {'kwriteconfig6'}, ), LinuxProxyCommands.buildStop( desktop: 'KDE', homeDir: '/home/user', availableExecutables: {'kwriteconfig6'}, ), ]) { final reload = commands.last; expect(reload.executable, 'dbus-send'); expect(reload.args, [ '--session', '--type=signal', '/KIO/Scheduler', 'org.kde.KIO.Scheduler.reparseSlaveConfiguration', 'string:', ]); expect(reload.optional, isTrue); } final proxies = { for (final command in LinuxProxyCommands.buildStart( port: 7890, bypassDomain: ['localhost'], desktop: 'KDE', homeDir: '/home/user', availableExecutables: {'kwriteconfig6'}, )) if (command.args.length > 6 && command.args[5].endsWith('Proxy')) command.args[5]: command.args[6], }; expect(proxies, { 'httpProxy': 'http://127.0.0.1:7890', 'httpsProxy': 'http://127.0.0.1:7890', 'socksProxy': 'socks://127.0.0.1:7890', }); }); test( 'falls back to kwriteconfig5 for KDE when kwriteconfig6 is missing', () { final commands = LinuxProxyCommands.buildStart( port: 7890, bypassDomain: ['localhost'], desktop: 'KDE', homeDir: '/home/user', availableExecutables: {'kwriteconfig5'}, ); expect(commands.map((command) => command.executable).toSet(), { 'kwriteconfig5', 'dbus-send', }); }, ); test('uses available backend for unknown desktops', () { final commands = LinuxProxyCommands.buildStart( port: 7890, bypassDomain: ['localhost'], desktop: 'UNKNOWN', homeDir: '/home/user', availableExecutables: {'kwriteconfig5'}, ); expect(commands.map((command) => command.executable).toSet(), { 'kwriteconfig5', 'dbus-send', }); }); test('does not use an unrelated backend for a known desktop', () { final commands = LinuxProxyCommands.buildStart( port: 7890, bypassDomain: ['localhost'], desktop: 'KDE', homeDir: '/home/user', availableExecutables: {'gsettings'}, ); expect(commands, isEmpty); }); }); group('macOS proxy command builders', () { test( 'filters networksetup service list headers, disabled services, and blanks', () { final services = MacosProxyCommands.parseNetworkServices(''' An asterisk (*) denotes that a network service is disabled. Wi-Fi *Thunderbolt Bridge USB 10/100/1000 LAN '''); expect(services, ['Wi-Fi', 'USB 10/100/1000 LAN']); }, ); test('passes bypass domains as separate networksetup arguments', () { final command = MacosProxyCommands.buildProxyBypass('Wi-Fi', [ 'localhost', '127.0.0.1', ]); expect(command.executable, '/usr/sbin/networksetup'); expect(command.args, [ '-setproxybypassdomains', 'Wi-Fi', 'localhost', '127.0.0.1', ]); }); test('uses Empty when clearing bypass domains', () { final command = MacosProxyCommands.buildProxyBypass('Wi-Fi', const []); expect(command.args, ['-setproxybypassdomains', 'Wi-Fi', 'Empty']); }); test('configures values before enabling proxy states', () { final commands = MacosProxyCommands.buildStart('Wi-Fi', 7890, const [ 'localhost', ]); final firstStateCommand = commands.indexWhere( (command) => command.args.first.endsWith('proxystate'), ); expect(firstStateCommand, 4); expect(commands[3].args.first, '-setproxybypassdomains'); }); test('reports failure when no active network service is found', () async { var callCount = 0; final proxy = MacosProxy( commandRunner: ProxyCommandRunner(( executable, arguments, { runInShell = false, }) async { callCount++; return ProcessResult( callCount, 0, 'An asterisk (*) denotes that a network service is disabled.\n', '', ); }), ); expect(await proxy.start(7890, const []), isFalse); expect(callCount, 1); }); }); group('windowsBypassList', () { test('adds the intranet token once', () { expect(windowsBypassList(['localhost', '127.*', '']), [ 'localhost', '127.*', '', ]); expect(windowsBypassList(['localhost']), ['localhost', '']); }); test('brackets bare IPv6 literals and drops blank entries', () { expect( windowsBypassList([' ::1 ', 'fe80::1', '', '[fd00::1]', '*.lan']), ['[::1]', '[fe80::1]', '[fd00::1]', '*.lan', ''], ); }); }); group('MethodChannelProxy', () { final proxy = MethodChannelProxy(); tearDown(() { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(proxy.methodChannel, null); }); test('sends the Windows start contract', () async { MethodCall? capturedCall; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(proxy.methodChannel, (call) async { capturedCall = call; return true; }); final result = await proxy.startProxy(7890, const ['localhost']); expect(result, isTrue); expect(capturedCall?.method, 'StartProxy'); expect(capturedCall?.arguments, { 'port': 7890, 'bypassDomain': ['localhost'], }); }); test('maps a null native response to false', () async { TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(proxy.methodChannel, (_) async => null); expect(await proxy.stopProxy(), isFalse); }); }); }