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

188 lines
5.3 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:drift/native.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/database/database.dart' as db;
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/database.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/widgets.dart' show Locale;
import 'package:flutter_test/flutter_test.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:riverpod/riverpod.dart';
String _iconSetJson(String name, List<String> icons) => json.encode({
'name': name,
'icons': [
for (final icon in icons)
{'name': '$icon.png', 'url': 'https://example.com/$icon.png'},
],
});
/// Real downloads over loopback: the test binding answers every request with a
/// 400, so its override is lifted while a test runs.
void main() {
late HttpServer server;
late String body;
Completer<void>? held;
late db.Database testDatabase;
late ProviderContainer container;
setUpAll(() async {
await AppLocalizations.load(const Locale('en'));
globalState.packageInfo = PackageInfo(
appName: 'FlClash',
packageName: 'com.follow.clash',
version: '1.0.0',
buildNumber: '1',
);
server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
server.listen((request) async {
await held?.future;
final response = request.response;
if (request.uri.path == '/missing') {
response.statusCode = HttpStatus.notFound;
} else {
response.write(body);
}
await response.close();
});
});
tearDownAll(() => server.close(force: true));
setUp(() {
final bindingOverrides = HttpOverrides.current;
HttpOverrides.global = null;
addTearDown(() => HttpOverrides.global = bindingOverrides);
held = null;
testDatabase = db.Database(NativeDatabase.memory());
db.database = testDatabase;
container = ProviderContainer();
globalState.container = container;
container.listen(iconSetsProvider, (_, _) {});
});
tearDown(() async {
container.dispose();
await testDatabase.close();
});
String urlOf(String path) => 'http://127.0.0.1:${server.port}/$path';
IconSetsAction action() => container.read(iconSetsActionProvider.notifier);
Future<List<IconSet>> stored() async {
await pumpEventQueue();
return testDatabase.iconSetsDao.query().get();
}
test('a url import takes the name of the set unless one is given', () async {
body = _iconSetJson('Qure', ['Proxy']);
await action().importUrl(urlOf('qure.json'));
await action().importUrl(urlOf('mine.json'), name: 'Mine');
final iconSets = await stored();
expect(iconSets.map((iconSet) => iconSet.name), ['Qure', 'Mine']);
expect(iconSets.first.url, urlOf('qure.json'));
expect(iconSets.first.icons.single.name, 'Proxy.png');
});
test('importing a url again syncs that set instead of adding one', () async {
body = _iconSetJson('Qure', ['Proxy']);
await action().importUrl(urlOf('qure.json'));
await stored();
body = _iconSetJson('Renamed upstream', ['Proxy', 'Direct']);
await action().importUrl(urlOf('qure.json'));
final iconSets = await stored();
expect(iconSets.single.name, 'Qure');
expect(iconSets.single.icons.map((icon) => icon.name), [
'Proxy.png',
'Direct.png',
]);
});
group('a set changed while it downloads', () {
late IconSet saved;
late Future<IconSet> syncing;
setUp(() async {
body = _iconSetJson('Qure', ['Proxy']);
await action().importUrl(urlOf('qure.json'));
saved = (await stored()).single;
body = _iconSetJson('Qure', ['Proxy', 'Direct']);
held = Completer();
syncing = action().sync(saved);
await pumpEventQueue();
});
Future<void> download() async {
held!.complete();
await syncing;
}
test('stays deleted', () async {
container.read(iconSetsProvider.notifier).del(saved.id);
await pumpEventQueue();
await download();
expect(await stored(), isEmpty);
});
test('keeps its new name and takes the new icons', () async {
container
.read(iconSetsProvider.notifier)
.put(saved.copyWith(name: 'Renamed'));
await pumpEventQueue();
await download();
final iconSet = (await stored()).single;
expect(iconSet.name, 'Renamed');
expect(iconSet.icons, hasLength(2));
});
});
test(
'a failed download or a document that is no icon set saves nothing',
() async {
body = '{"name": "x"}';
await expectLater(
action().importUrl(urlOf('missing')),
throwsA(anything),
);
await expectLater(
action().importUrl(urlOf('set.json')),
throwsA(
isA<MessageException>().having(
(e) => e.message,
'message',
'Not a valid icon set',
),
),
);
expect(await stored(), isEmpty);
},
);
test('a file import falls back to the file name', () async {
action().importContent(
'My icons.json',
'{"icons": [{"url": "https://example.com/a.png"}]}',
);
final iconSets = await stored();
expect(iconSets.single.name, 'My icons');
expect(iconSets.single.isRemote, isFalse);
});
}