feat(core): bind the Android Core with gomobile
Changelog: Share link import and mrs rule sets
This commit is contained in:
@@ -42,4 +42,38 @@ void main() {
|
||||
await expectLater(safeDeletePath(join(root.path, 'missing')), completes);
|
||||
});
|
||||
});
|
||||
|
||||
group('writeAsStringAtomically', () {
|
||||
test('replaces the file and leaves nothing beside it', () async {
|
||||
final file = File(join(root.path, 'config.yaml'))
|
||||
..writeAsStringSync('mixed-port: 7890');
|
||||
|
||||
await file.writeAsStringAtomically('mixed-port: 7891');
|
||||
|
||||
expect(file.readAsStringSync(), 'mixed-port: 7891');
|
||||
expect(root.listSync().map((entry) => basename(entry.path)), [
|
||||
'config.yaml',
|
||||
]);
|
||||
});
|
||||
|
||||
test('creates the directory a first write needs', () async {
|
||||
final file = File(join(root.path, 'home', 'config.yaml'));
|
||||
|
||||
await file.writeAsStringAtomically('mode: rule');
|
||||
|
||||
expect(file.readAsStringSync(), 'mode: rule');
|
||||
});
|
||||
|
||||
test('keeps the old file when the new one cannot be written', () async {
|
||||
final file = File(join(root.path, 'config.yaml'))
|
||||
..writeAsStringSync('mode: rule');
|
||||
Directory('${file.path}.$pid.tmp').createSync();
|
||||
|
||||
await expectLater(
|
||||
file.writeAsStringAtomically('mode: global'),
|
||||
throwsA(isA<FileSystemException>()),
|
||||
);
|
||||
expect(file.readAsStringSync(), 'mode: rule');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import 'package:fl_clash/common/yaml_writer.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
const _awkwardStrings = [
|
||||
'0123', '00', '1e3', '0x1F', '0o17', '0b101', '1_000', '+1', '-0', '.5', //
|
||||
'1.', '.inf', '-.Inf', '.nan', 'yes', 'No', 'on', 'OFF', 'y', 'n', 'true',
|
||||
'False', 'null', 'Null', '~', '', ' ', '2001-12-14', '12:30', '1:20:30',
|
||||
'=', '!a', '- a', '? a', ': a', 'a: b', 'a #b', '#a', '&a', '*a', '|', '>',
|
||||
'%a', '@a', '`a', '[a', '{a', ',a', "'a", '"a', ' lead', 'trail ', 'a\tb',
|
||||
'geosite:cn', '+.google.com', '*.lan', 'rule-set:cn,direct', 'HK 🇭🇰 香港',
|
||||
'p\x00w\x7f\r\u0085\u2028\ufeff\uffff "q" \\', 'G\x07', 'k\x1bey',
|
||||
'a\nb', 'multi\nline \x1b\n', 'tail\n', 'two\n\n', '\nlead break', ' a\nb',
|
||||
'a\n \nb', 'a \nb ', 'a\n\tb', '\n', 'a\n#b\n- c\n---\n...',
|
||||
];
|
||||
|
||||
void main() {
|
||||
test('keeps the layout the Core config has always had', () {
|
||||
final text = writeYaml({
|
||||
'mixed-port': 7890,
|
||||
'proxies': [
|
||||
{'name': 'HK', 'type': 'ss', 'port': 443, 'udp': true, 'plugin': null},
|
||||
],
|
||||
'proxy-groups': [
|
||||
{
|
||||
'name': 'G',
|
||||
'proxies': ['HK', 'DIRECT'],
|
||||
},
|
||||
],
|
||||
'dns': {
|
||||
'enable': true,
|
||||
'nameserver': <String>[],
|
||||
'fallback-filter': <String, Object?>{},
|
||||
},
|
||||
'nameserver-policy': {'geosite:cn': 'system', '+.lan': 'system'},
|
||||
'certificate': 'line 1\nline 2\n',
|
||||
'rules': ['MATCH,G'],
|
||||
'matrix': [
|
||||
[1, 2],
|
||||
[3],
|
||||
],
|
||||
});
|
||||
|
||||
expect(text, '''
|
||||
mixed-port: 7890
|
||||
proxies:
|
||||
- name: "HK"
|
||||
type: "ss"
|
||||
port: 443
|
||||
udp: true
|
||||
plugin: null
|
||||
proxy-groups:
|
||||
- name: "G"
|
||||
proxies:
|
||||
- "HK"
|
||||
- "DIRECT"
|
||||
dns:
|
||||
enable: true
|
||||
nameserver: []
|
||||
fallback-filter: {}
|
||||
nameserver-policy:
|
||||
geosite:cn: "system"
|
||||
+.lan: "system"
|
||||
certificate: |
|
||||
line 1
|
||||
line 2
|
||||
rules:
|
||||
- "MATCH,G"
|
||||
matrix:
|
||||
- - 1
|
||||
- 2
|
||||
- - 3
|
||||
''');
|
||||
});
|
||||
|
||||
test('reads back every string as the same string, as value or key', () {
|
||||
final data = {
|
||||
'values': _awkwardStrings,
|
||||
'keys': {
|
||||
for (final text in _awkwardStrings.where((text) => text.isNotEmpty))
|
||||
text: text,
|
||||
},
|
||||
};
|
||||
|
||||
expect(loadYaml(writeYaml(data)), data);
|
||||
});
|
||||
|
||||
test('reads back numbers, booleans, nulls and non-string keys', () {
|
||||
final data = {
|
||||
'numbers': [0, -1, 1 << 53, 1e21, 1e-7, 3.0, -0.5],
|
||||
'flags': [true, false, null],
|
||||
'keys': {1: 'one', 2.5: 'half', true: 'yes', null: 'none'},
|
||||
};
|
||||
|
||||
expect(loadYaml(writeYaml(data)), data);
|
||||
});
|
||||
|
||||
test('writes non-finite doubles as YAML floats', () {
|
||||
final read =
|
||||
loadYaml(
|
||||
writeYaml([double.nan, double.infinity, double.negativeInfinity]),
|
||||
)
|
||||
as List;
|
||||
|
||||
expect((read[0] as double).isNaN, isTrue);
|
||||
expect(read.sublist(1), [double.infinity, double.negativeInfinity]);
|
||||
});
|
||||
|
||||
test('replaces a lone surrogate, which UTF-8 cannot carry', () {
|
||||
expect(loadYaml(writeYaml({'name': 'a\ud800b\udc00c'})), {
|
||||
'name': 'a\ufffdb\ufffdc',
|
||||
});
|
||||
expect(writeYaml({'flag': '🇭🇰'}), 'flag: "🇭🇰"\n');
|
||||
});
|
||||
|
||||
test('leaves a merge key plain, the way the profile wrote it', () {
|
||||
final text = writeYaml({
|
||||
'proxy': {
|
||||
'<<': {'type': 'ss'},
|
||||
'name': 'HK',
|
||||
},
|
||||
});
|
||||
|
||||
expect(text, contains(' <<:\n type: "ss"\n'));
|
||||
});
|
||||
|
||||
test('encodes sets and objects through toJson', () {
|
||||
expect(
|
||||
loadYaml(
|
||||
writeYaml({
|
||||
'set': {'a', 'b'},
|
||||
'object': _Encodable(),
|
||||
}),
|
||||
),
|
||||
{
|
||||
'set': ['a', 'b'],
|
||||
'object': {'kind': 'encodable'},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class _Encodable {
|
||||
Map<String, Object?> toJson() => {'kind': 'encodable'};
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:fl_clash/common/exception.dart';
|
||||
import 'package:fl_clash/core/controller.dart';
|
||||
import 'package:fl_clash/core/desktop/model.dart';
|
||||
import 'package:fl_clash/core/interface.dart';
|
||||
@@ -108,6 +110,51 @@ void main() {
|
||||
verify(() => mock.validateConfig('/path')).called(1);
|
||||
});
|
||||
|
||||
group('validateProfile', () {
|
||||
late Directory dir;
|
||||
late String path;
|
||||
|
||||
setUp(() {
|
||||
dir = Directory.systemTemp.createTempSync('validate_profile_test');
|
||||
path = '${dir.path}/profile.yaml';
|
||||
when(() => mock.validateConfig(path)).thenAnswer((_) async => '');
|
||||
});
|
||||
|
||||
tearDown(() => dir.deleteSync(recursive: true));
|
||||
|
||||
test('decrypts an age-encrypted config in place', () async {
|
||||
File(
|
||||
path,
|
||||
).writeAsStringSync('-----BEGIN AGE ENCRYPTED FILE-----\nYWdl\n');
|
||||
when(
|
||||
() => mock.decryptConfig(path, 'AGE-SECRET-KEY-1A'),
|
||||
).thenAnswer((_) async => 'proxies: []');
|
||||
|
||||
expect(await controller.validateProfile(path, 'AGE-SECRET-KEY-1A'), '');
|
||||
expect(File(path).readAsStringSync(), 'proxies: []');
|
||||
});
|
||||
|
||||
test('asks for a key an encrypted config lacks', () async {
|
||||
File(
|
||||
path,
|
||||
).writeAsStringSync('-----BEGIN AGE ENCRYPTED FILE-----\nYWdl\n');
|
||||
|
||||
await expectLater(
|
||||
controller.validateProfile(path),
|
||||
throwsA(isA<AgeSecretKeyRequiredException>()),
|
||||
);
|
||||
verifyNever(() => mock.decryptConfig(any(), any()));
|
||||
});
|
||||
|
||||
test('leaves a plain config as it is', () async {
|
||||
File(path).writeAsStringSync('proxies: []');
|
||||
|
||||
expect(await controller.validateProfile(path, 'AGE-SECRET-KEY-1A'), '');
|
||||
verifyNever(() => mock.decryptConfig(any(), any()));
|
||||
expect(File(path).readAsStringSync(), 'proxies: []');
|
||||
});
|
||||
});
|
||||
|
||||
test('updateConfig delegates to interface', () async {
|
||||
const params = UpdateParams(
|
||||
tun: Tun(enable: false),
|
||||
|
||||
@@ -8,12 +8,18 @@ class _RecordingListener with CoreEventListener {
|
||||
|
||||
final void Function()? onLoadedCallback;
|
||||
final List<String> loaded = [];
|
||||
final List<String> dialerLoops = [];
|
||||
|
||||
@override
|
||||
void onLoaded(String providerName) {
|
||||
loaded.add(providerName);
|
||||
onLoadedCallback?.call();
|
||||
}
|
||||
|
||||
@override
|
||||
void onDialerLoop(String proxyName) {
|
||||
dialerLoops.add(proxyName);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
@@ -50,4 +56,18 @@ void main() {
|
||||
expect(second.loaded, ['provider-a', 'provider-b']);
|
||||
},
|
||||
);
|
||||
|
||||
test('a dialer loop the core stopped reaches the listeners', () async {
|
||||
final listener = _RecordingListener();
|
||||
coreEventManager.addListener(listener);
|
||||
addTearDown(() => coreEventManager.removeListener(listener));
|
||||
|
||||
coreEventsFromData({
|
||||
'type': 'dialerLoop',
|
||||
'data': 'Front',
|
||||
}).forEach(coreEventManager.sendEvent);
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(listener.dialerLoops, ['Front']);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -119,6 +119,35 @@ void main() {
|
||||
expect(await core.getExternalProvider('p'), isNull);
|
||||
expect(await core.watchRoute(true), isNull);
|
||||
});
|
||||
|
||||
test('calls nobody waits on absorb a Core that is gone', () async {
|
||||
final core = _DisconnectedCore();
|
||||
|
||||
await core.resetTraffic();
|
||||
await core.startLog();
|
||||
await core.stopLog();
|
||||
|
||||
expect(core.invoked, [
|
||||
CoreMethod.resetTraffic,
|
||||
CoreMethod.startLog,
|
||||
CoreMethod.stopLog,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
class _DisconnectedCore extends _SilentCore {
|
||||
@override
|
||||
Future<T?> invokeMethod<T>({
|
||||
required CoreMethod method,
|
||||
Object? arguments,
|
||||
Duration? timeout,
|
||||
}) async {
|
||||
invoked.add(method);
|
||||
throw const CoreMethodException(
|
||||
code: 'transport_disconnected',
|
||||
message: 'Core RPC client is closed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AnsweringCore extends _SilentCore {
|
||||
|
||||
@@ -94,7 +94,9 @@ class _RecordingCoreHandler extends CoreHandlerInterface {
|
||||
'runtimeOther': 32,
|
||||
},
|
||||
CoreMethod.changeProxy => {'message': '', 'changed': true},
|
||||
CoreMethod.compileRuleSet => {'behavior': 'ipcidr', 'format': 'text'},
|
||||
CoreMethod.getConnectionCount => 3,
|
||||
CoreMethod.validateFilters => ['', 'missing closing )'],
|
||||
CoreMethod.watchRoute => {
|
||||
'core-epoch': 1,
|
||||
'picks-version': 0,
|
||||
@@ -210,6 +212,15 @@ void main() {
|
||||
expect(handler.calls[CoreMethod.clearEffect], 42);
|
||||
});
|
||||
|
||||
test('validateFilters sends the filters and reads one error each', () async {
|
||||
final handler = _RecordingCoreHandler();
|
||||
|
||||
final errors = await handler.validateFilters(['hk`jp', '(hk']);
|
||||
|
||||
expect(handler.calls[CoreMethod.validateFilters], ['hk`jp', '(hk']);
|
||||
expect(errors, ['', 'missing closing )']);
|
||||
});
|
||||
|
||||
test('dumpRuleSet sends the rule set path', () async {
|
||||
final handler = _RecordingCoreHandler();
|
||||
|
||||
@@ -218,6 +229,38 @@ void main() {
|
||||
expect(handler.calls[CoreMethod.dumpRuleSet], '/rules.mrs');
|
||||
});
|
||||
|
||||
test('decryptConfig sends the path and the age key', () async {
|
||||
final handler = _RecordingCoreHandler();
|
||||
|
||||
await handler.decryptConfig('/profile.yaml', 'AGE-SECRET-KEY-1A');
|
||||
|
||||
expect(handler.calls[CoreMethod.decryptConfig], {
|
||||
'path': '/profile.yaml',
|
||||
'age-secret-key': 'AGE-SECRET-KEY-1A',
|
||||
});
|
||||
});
|
||||
|
||||
test(
|
||||
'compileRuleSet names a cached file and reads what the Core found',
|
||||
() async {
|
||||
final handler = _RecordingCoreHandler();
|
||||
|
||||
final info = await handler.compileRuleSet(
|
||||
'abc',
|
||||
url: 'https://example.com/r.list',
|
||||
);
|
||||
|
||||
expect(handler.calls[CoreMethod.compileRuleSet], {
|
||||
'name': 'abc',
|
||||
'url': 'https://example.com/r.list',
|
||||
});
|
||||
expect(info, (
|
||||
behavior: RuleProviderBehavior.ipcidr,
|
||||
format: RuleProviderFormat.text,
|
||||
));
|
||||
},
|
||||
);
|
||||
|
||||
test('event contract accepts batches and legacy single events', () async {
|
||||
final fixture =
|
||||
json.decode(
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:fl_clash/l10n/l10n.dart';
|
||||
import 'package:fl_clash/manager/core_manager.dart';
|
||||
import 'package:fl_clash/manager/status_manager.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/action.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/providers/core.dart';
|
||||
@@ -45,14 +46,13 @@ class _FakePathProvider extends PathProviderPlatform {
|
||||
const _nullProfileSetupState = SetupState(
|
||||
profileId: null,
|
||||
profileLastUpdateDate: null,
|
||||
overwriteType: OverwriteType.standard,
|
||||
profileType: ProfileType.file,
|
||||
extendType: ExtendType.standard,
|
||||
rules: [],
|
||||
proxyGroups: [],
|
||||
addedRules: [],
|
||||
script: null,
|
||||
dnsOverrideKeys: {},
|
||||
overrideDns: false,
|
||||
dns: Dns(),
|
||||
overrides: ProfileOverrides(),
|
||||
);
|
||||
|
||||
const _crash = CoreEvent(type: CoreEventType.crash, data: 'boom');
|
||||
@@ -73,6 +73,21 @@ CoreEvent _geoUpdate({
|
||||
);
|
||||
}
|
||||
|
||||
class _ApplyRecordingSetupAction extends SetupAction {
|
||||
int updates = 0;
|
||||
int applies = 0;
|
||||
|
||||
@override
|
||||
Future<void> updateConfigDebounce() async {
|
||||
updates++;
|
||||
}
|
||||
|
||||
@override
|
||||
void applyProfileDebounce({bool silence = false, bool force = false}) {
|
||||
applies++;
|
||||
}
|
||||
}
|
||||
|
||||
_MockCoreHandlerInterface _coreInterface() {
|
||||
final coreInterface = _MockCoreHandlerInterface();
|
||||
when(() => coreInterface.startLog()).thenAnswer((_) {});
|
||||
@@ -241,6 +256,8 @@ void main() {
|
||||
testWidgets('the log stream follows the openLogs setting', (tester) async {
|
||||
final coreInterface = _coreInterface();
|
||||
final container = await _pumpCoreManager(tester, coreInterface);
|
||||
container.read(coreStatusProvider.notifier).value = CoreStatus.connected;
|
||||
await tester.pump();
|
||||
|
||||
verify(() => coreInterface.stopLog()).called(1);
|
||||
verifyNever(() => coreInterface.startLog());
|
||||
@@ -262,6 +279,87 @@ void main() {
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
testWidgets('the log stream waits for the core and is restored on '
|
||||
'every connection', (tester) async {
|
||||
final coreInterface = _coreInterface();
|
||||
final container = await _pumpCoreManager(tester, coreInterface);
|
||||
container
|
||||
.read(appSettingProvider.notifier)
|
||||
.update((state) => state.copyWith(openLogs: true));
|
||||
await tester.pump();
|
||||
|
||||
verifyNever(() => coreInterface.startLog());
|
||||
verifyNever(() => coreInterface.stopLog());
|
||||
|
||||
container.read(coreStatusProvider.notifier).value = CoreStatus.connected;
|
||||
await tester.pump();
|
||||
|
||||
verify(() => coreInterface.startLog()).called(1);
|
||||
|
||||
container.read(coreStatusProvider.notifier).value = CoreStatus.disconnected;
|
||||
await tester.pump();
|
||||
container.read(coreStatusProvider.notifier).value = CoreStatus.connecting;
|
||||
await tester.pump();
|
||||
|
||||
verifyNever(() => coreInterface.startLog());
|
||||
|
||||
container.read(coreStatusProvider.notifier).value = CoreStatus.connected;
|
||||
await tester.pump();
|
||||
|
||||
verify(() => coreInterface.startLog()).called(1);
|
||||
verifyNever(() => coreInterface.stopLog());
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
testWidgets('a setting the Core patches live is not re-applied', (
|
||||
tester,
|
||||
) async {
|
||||
final setupAction = _ApplyRecordingSetupAction();
|
||||
final container = await _pumpCoreManager(
|
||||
tester,
|
||||
_coreInterface(),
|
||||
overrides: [setupActionProvider.overrideWith(() => setupAction)],
|
||||
);
|
||||
|
||||
container
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.update((state) => state.copyWith(mode: Mode.global, mixedPort: 8899));
|
||||
await tester.pump();
|
||||
|
||||
expect(setupAction.updates, 1);
|
||||
expect(setupAction.applies, 0);
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
testWidgets('a setting only a setup carries re-applies the profile', (
|
||||
tester,
|
||||
) async {
|
||||
final setupAction = _ApplyRecordingSetupAction();
|
||||
final container = await _pumpCoreManager(
|
||||
tester,
|
||||
_coreInterface(),
|
||||
overrides: [setupActionProvider.overrideWith(() => setupAction)],
|
||||
);
|
||||
|
||||
container
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.update((state) => state.copyWith(hosts: const {'a.test': '1.1.1.1'}));
|
||||
await tester.pump();
|
||||
container
|
||||
.read(networkSettingProvider.notifier)
|
||||
.update(
|
||||
(state) => state.copyWith(appendSystemDns: !state.appendSystemDns),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(setupAction.applies, 2);
|
||||
expect(setupAction.updates, 0);
|
||||
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
testWidgets('core logs are recorded for the logs view', (tester) async {
|
||||
final coreInterface = _coreInterface();
|
||||
final container = await _pumpCoreManager(tester, coreInterface);
|
||||
|
||||
@@ -10,10 +10,12 @@ void main() {
|
||||
final json = {
|
||||
'selected-map': {'G1': 'P1'},
|
||||
'test-url': 'http://test.com',
|
||||
'skip-cert-verify': true,
|
||||
};
|
||||
final params = SetupParams.fromJson(json);
|
||||
expect(params.selectedMap, {'G1': 'P1'});
|
||||
expect(params.testUrl, 'http://test.com');
|
||||
expect(params.skipCertVerify, isTrue);
|
||||
});
|
||||
|
||||
test('toJson uses snake-case keys', () {
|
||||
@@ -24,6 +26,7 @@ void main() {
|
||||
final json = params.toJson();
|
||||
expect(json['selected-map'], {'G1': 'P1'});
|
||||
expect(json['test-url'], 'http://t.com');
|
||||
expect(json['skip-cert-verify'], isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +64,100 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('VpnOptions.effective', () {
|
||||
const options = VpnOptions(
|
||||
enable: true,
|
||||
port: 7890,
|
||||
ipv6: false,
|
||||
dnsHijacking: false,
|
||||
accessControlProps: AccessControlProps(
|
||||
enable: true,
|
||||
mode: AccessControlMode.acceptSelected,
|
||||
acceptList: ['a.app'],
|
||||
rejectList: ['b.app'],
|
||||
),
|
||||
allowBypass: true,
|
||||
systemProxy: true,
|
||||
bypassDomain: ['localhost'],
|
||||
stack: 'mixed',
|
||||
routeAddress: ['0.0.0.0/0'],
|
||||
mtu: 9000,
|
||||
);
|
||||
|
||||
test('tells apart every option the tunnel is built from', () {
|
||||
final changes = [
|
||||
options.copyWith(port: 7891),
|
||||
options.copyWith(ipv6: true),
|
||||
options.copyWith(dnsHijacking: true),
|
||||
options.copyWith(allowBypass: false),
|
||||
options.copyWith(systemProxy: false),
|
||||
options.copyWith(bypassDomain: const ['example.com']),
|
||||
options.copyWith(stack: 'gvisor'),
|
||||
options.copyWith(routeAddress: const ['10.0.0.0/8']),
|
||||
options.copyWith(mtu: 1500),
|
||||
options.copyWith(congestionController: 'bbr'),
|
||||
options.copyWith(
|
||||
accessControlProps: options.accessControlProps.copyWith(
|
||||
acceptList: const ['c.app'],
|
||||
),
|
||||
),
|
||||
options.copyWith(enable: false),
|
||||
];
|
||||
|
||||
for (final changed in changes) {
|
||||
expect(changed.effective, isNot(options.effective));
|
||||
}
|
||||
});
|
||||
|
||||
test('ignores how the app list is shown and the list not applied', () {
|
||||
final changed = options.copyWith(
|
||||
accessControlProps: options.accessControlProps.copyWith(
|
||||
rejectList: const ['c.app'],
|
||||
sort: AccessSortType.name,
|
||||
isFilterSystemApp: false,
|
||||
),
|
||||
);
|
||||
|
||||
expect(changed.effective, options.effective);
|
||||
});
|
||||
|
||||
test('ignores the proxy port and bypass list without a system proxy', () {
|
||||
final withoutProxy = options.copyWith(systemProxy: false);
|
||||
|
||||
expect(
|
||||
withoutProxy.copyWith(port: 7891, bypassDomain: const []).effective,
|
||||
withoutProxy.effective,
|
||||
);
|
||||
});
|
||||
|
||||
test('ignores the access lists while access control is off', () {
|
||||
final open = options.copyWith(
|
||||
accessControlProps: options.accessControlProps.copyWith(enable: false),
|
||||
);
|
||||
|
||||
expect(
|
||||
open
|
||||
.copyWith(
|
||||
accessControlProps: open.accessControlProps.copyWith(
|
||||
mode: AccessControlMode.rejectSelected,
|
||||
acceptList: const [],
|
||||
),
|
||||
)
|
||||
.effective,
|
||||
open.effective,
|
||||
);
|
||||
});
|
||||
|
||||
test('a proxy-only run reads none of the tunnel options', () {
|
||||
final proxyOnly = options.copyWith(enable: false);
|
||||
|
||||
expect(
|
||||
proxyOnly.copyWith(ipv6: true, stack: 'gvisor', mtu: 1500).effective,
|
||||
proxyOnly.effective,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('ChangeProxyParams', () {
|
||||
test('fromJson and toJson use snake-case', () {
|
||||
final json = {'group-name': 'Proxy', 'proxy-name': 'auto'};
|
||||
|
||||
Reference in New Issue
Block a user