Files
FlClash/test/providers/profiles_action_test.dart
2026-10-16 22:54:53 +08:00

223 lines
7.0 KiB
Dart

import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/core/controller.dart';
import 'package:fl_clash/core/interface.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/l10n/l10n.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/core.dart';
import 'package:fl_clash/providers/database.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:material_ui/material_ui.dart';
import 'package:mocktail/mocktail.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yaml/yaml.dart';
import '../helpers/app_paths.dart';
import '../helpers/test_app.dart';
import '../helpers/test_profiles.dart';
class _MockCoreHandlerInterface extends Mock implements CoreHandlerInterface {}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late Directory home;
late _MockCoreHandlerInterface core;
late ProviderContainer container;
setUpAll(() async {
await AppLocalizations.load(const Locale('en'));
home = Directory.systemTemp.createTempSync('flclash-profiles-action-');
await useAppPaths(home);
});
tearDownAll(() {
if (home.existsSync()) home.deleteSync(recursive: true);
});
setUp(() {
core = _MockCoreHandlerInterface();
when(() => core.validateConfig(any())).thenAnswer((_) async => '');
container = ProviderContainer(
overrides: [
profilesProvider.overrideWith(TestProfiles.new),
coreHandlerProvider.overrideWithValue(CoreController.scoped(core)),
],
);
addTearDown(container.dispose);
globalState.container = container;
});
void decodesTo(String link, List<Map<String, dynamic>> proxies) {
when(
() => core.decodeShareLinks([link]),
).thenAnswer((_) async => [proxies]);
}
Future<void> pumpHost(WidgetTester tester) async {
const size = Size(1200, 1000);
tester.view.physicalSize = size;
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
container.read(viewSizeProvider.notifier).value = size;
await tester.pumpWidget(
UncontrolledProviderScope(
container: container,
child: const TestApp(withStatusManager: true, child: SizedBox()),
),
);
}
Future<List<Map>> savedProxies(Profile profile) async {
final config = loadYaml(await (await profile.file).readAsString()) as Map;
return [for (final proxy in config['proxies'] as List) proxy as Map];
}
group('addProfileFromLink with a share link', () {
test('saves a file profile named after the proxy it holds', () async {
const link = 'vless://id@hk.example.com:443#HK';
decodesTo(link, [
{'name': 'HK', 'type': 'vless', 'server': 'hk.example.com'},
]);
await container
.read(profilesActionProvider.notifier)
.addProfileFromLink(link);
final profile = container.read(profilesProvider).single;
expect(profile.type, ProfileType.file);
expect(profile.url, isEmpty);
expect(profile.label, 'HK');
expect(await savedProxies(profile), [
{'name': 'HK', 'type': 'vless', 'server': 'hk.example.com'},
]);
verify(() => core.validateConfig(any())).called(1);
});
test('names an unnamed proxy after its server', () async {
const link = 'ss://YWVzLTEyOC1nY206cGFzcw@ss.example.com:8388';
decodesTo(link, [
{'name': '', 'type': 'ss', 'server': 'ss.example.com'},
]);
await container
.read(profilesActionProvider.notifier)
.addProfileFromLink(link, label: 'Mine');
final profile = container.read(profilesProvider).single;
expect(profile.label, 'Mine');
expect((await savedProxies(profile)).single['name'], 'ss.example.com');
});
test('moves a proxy off a name mihomo reserves', () async {
const link = 'trojan://pass@t.example.com:443#DIRECT';
decodesTo(link, [
{'name': 'DIRECT', 'type': 'trojan', 'server': 't.example.com'},
]);
await container
.read(profilesActionProvider.notifier)
.addProfileFromLink(link);
final profile = container.read(profilesProvider).single;
expect((await savedProxies(profile)).single['name'], 'DIRECT-01');
});
testWidgets('reports a link no proxy can be read from and adds nothing', (
tester,
) async {
const link = 'vmess://not-base64';
decodesTo(link, const []);
await pumpHost(tester);
await container
.read(profilesActionProvider.notifier)
.addProfileFromLink(link);
await tester.pump(const Duration(seconds: 1));
await tester.pumpAndSettle();
expect(find.text(currentAppLocalizations.shareLinksInvalid), findsOne);
expect(container.read(profilesProvider), isEmpty);
verifyNever(() => core.validateConfig(any()));
});
});
group('addProfilesFromLinks with share links', () {
const links = [
'vless://id@hk.example.com:443#HK',
'trojan://pass@hk2.example.com:443#HK',
'ss://YWVzLTEyOC1nY206cGFzcw@ss.example.com:8388',
];
test('makes one profile of all their proxies', () async {
when(() => core.decodeShareLinks(links)).thenAnswer(
(_) async => [
[
{'name': 'HK', 'type': 'vless', 'server': 'hk.example.com'},
],
[
{'name': 'HK', 'type': 'trojan', 'server': 'hk2.example.com'},
],
[
{'name': '', 'type': 'ss', 'server': 'ss.example.com'},
],
],
);
await container
.read(profilesActionProvider.notifier)
.addProfilesFromLinks(links);
final profile = container.read(profilesProvider).single;
expect(profile.type, ProfileType.file);
expect(
profile.label,
currentAppLocalizations.proxiesProfileLabel('HK', 3),
);
expect(
[for (final proxy in await savedProxies(profile)) proxy['name']],
['HK', 'HK-01', 'ss.example.com'],
);
verify(() => core.decodeShareLinks(links)).called(1);
});
testWidgets('names the link no proxy can be read from and adds nothing', (
tester,
) async {
when(() => core.decodeShareLinks(links)).thenAnswer(
(_) async => [
[
{'name': 'HK', 'type': 'vless', 'server': 'hk.example.com'},
],
const [],
[
{'name': '', 'type': 'ss', 'server': 'ss.example.com'},
],
],
);
await pumpHost(tester);
await container
.read(profilesActionProvider.notifier)
.addProfilesFromLinks(links);
await tester.pump(const Duration(seconds: 1));
await tester.pumpAndSettle();
final l10n = currentAppLocalizations;
expect(
find.text(
l10n.failedItem(links.first, l10n.shareLinkUnreadable(links[1])),
),
findsOne,
);
expect(container.read(profilesProvider), isEmpty);
verifyNever(() => core.validateConfig(any()));
});
});
}