Add proxy routing and sing-box support
This commit is contained in:
+174
@@ -0,0 +1,174 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_singbox_proxy/flutter_singbox_proxy.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_tor/flutter_tor.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/data/database/functions/lexo_rank_functions.dart';
|
||||
import 'package:weblibre/data/database/functions/url_functions.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/services/proxy_settings_replication.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/database.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/site_assignment.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||
import 'package:weblibre/features/proxy/data/proxy_connection.dart';
|
||||
import 'package:weblibre/features/proxy/domain/repositories/container_proxy.dart';
|
||||
import 'package:weblibre/features/proxy/domain/repositories/singbox_proxy_runtime.dart';
|
||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
||||
import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
test(
|
||||
'container assignments ensure sing-box profiles are running before syncing',
|
||||
() async {
|
||||
final profileId = SingboxProxyConnectionId('profile-1');
|
||||
final assignedContainer = _container(
|
||||
id: 'container-1',
|
||||
contextId: 'context-a',
|
||||
proxyConnectionId: profileId,
|
||||
);
|
||||
final db = TabDatabase(
|
||||
NativeDatabase.memory(
|
||||
setup: (database) {
|
||||
registerLexorankFunctions(database);
|
||||
registerUrlFunctions(database);
|
||||
},
|
||||
),
|
||||
);
|
||||
final containerProxyRepository = _FakeContainerProxyRepository();
|
||||
final containerRepository = _FakeContainerRepository([assignedContainer]);
|
||||
final runtimeRepository = _FakeSingboxProxyRuntimeRepository();
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
tabDatabaseProvider.overrideWith((ref) => db),
|
||||
containerProxyRepositoryProvider.overrideWith(
|
||||
() => containerProxyRepository,
|
||||
),
|
||||
containerRepositoryProvider.overrideWith(() => containerRepository),
|
||||
singboxProxyRuntimeRepositoryProvider.overrideWith(
|
||||
() => runtimeRepository,
|
||||
),
|
||||
torProxyServiceProvider.overrideWith(_FakeTorProxyService.new),
|
||||
proxyRoutingSettingsWithDefaultsProvider.overrideWith(
|
||||
(ref) => ProxyRoutingSettings.withDefaults(),
|
||||
),
|
||||
watchContainersWithCountProvider.overrideWith(
|
||||
(ref) => Stream.value([assignedContainer]),
|
||||
),
|
||||
watchAllAssignedSitesProvider.overrideWith(
|
||||
(ref) => Stream.value(const <SiteAssignment>[]),
|
||||
),
|
||||
watchIsolatedContextContainerMapProvider.overrideWith(
|
||||
(ref) => Stream.value(const <String, Set<String>>{}),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(() async {
|
||||
container.dispose();
|
||||
await db.close();
|
||||
});
|
||||
|
||||
final subscription = container.listen<void>(
|
||||
proxySettingsReplicationProvider,
|
||||
(previous, next) {},
|
||||
fireImmediately: true,
|
||||
);
|
||||
addTearDown(subscription.close);
|
||||
await pumpEventQueue();
|
||||
|
||||
expect(runtimeRepository.ensuredProxyConnectionIds, [profileId]);
|
||||
expect(containerProxyRepository.setContainerProxyCalls, [
|
||||
('context-a', profileId.encode()),
|
||||
]);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ContainerDataWithCount _container({
|
||||
required String id,
|
||||
required String contextId,
|
||||
required ProxyConnectionId proxyConnectionId,
|
||||
}) {
|
||||
return ContainerDataWithCount(
|
||||
id: id,
|
||||
name: 'Container',
|
||||
color: const Color(0xFF336699),
|
||||
orderKey: 'a',
|
||||
metadata: ContainerMetadata.withDefaults(
|
||||
contextualIdentity: contextId,
|
||||
proxyConnectionId: proxyConnectionId,
|
||||
),
|
||||
tabCount: 0,
|
||||
);
|
||||
}
|
||||
|
||||
class _FakeContainerProxyRepository extends ContainerProxyRepository {
|
||||
final setContainerProxyCalls = <(String, String)>[];
|
||||
|
||||
@override
|
||||
Future<void> setTorProxyPort(int? port) async {}
|
||||
|
||||
@override
|
||||
Future<void> setSiteAssignments(List<SiteAssignment> assignements) async {}
|
||||
|
||||
@override
|
||||
Future<void> setContainerProxy(String contextId, String proxyId) async {
|
||||
setContainerProxyCalls.add((contextId, proxyId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearContainerProxy(String contextId) async {}
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
}
|
||||
|
||||
class _FakeContainerRepository extends ContainerRepository {
|
||||
final List<ContainerDataWithCount> containers;
|
||||
|
||||
_FakeContainerRepository(this.containers);
|
||||
|
||||
@override
|
||||
Future<List<ContainerDataWithCount>> getAllContainersWithCount() async {
|
||||
return containers;
|
||||
}
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
}
|
||||
|
||||
class _FakeSingboxProxyRuntimeRepository extends SingboxProxyRuntimeRepository {
|
||||
final ensuredProxyConnectionIds = <SingboxProxyConnectionId>[];
|
||||
|
||||
@override
|
||||
Future<void> ensureProxyConnectionAvailable(
|
||||
SingboxProxyConnectionId connectionId,
|
||||
) async {
|
||||
ensuredProxyConnectionIds.add(connectionId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SingboxProxyRuntimeState> build() async {
|
||||
return SingboxProxyRuntimeState(
|
||||
status: SingboxProxyRuntimeStatus.stopped,
|
||||
endpoints: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FakeTorProxyService extends TorProxyService {
|
||||
@override
|
||||
Stream<TorStatus> build() async* {
|
||||
// Mirror production: the real TorProxyService seeds the provider with the
|
||||
// current status so downstream `selectAsync` paths don't stay in
|
||||
// AsyncLoading forever. Tor is stopped in these tests.
|
||||
yield TorStatus(isRunning: false, bootstrapProgress: 0);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/proxy/data/proxy_connection.dart';
|
||||
|
||||
void main() {
|
||||
group('ContainerMetadata proxy serialization', () {
|
||||
test('round-trips proxyConnectionId through JSON', () {
|
||||
final metadata = ContainerMetadata.withDefaults(
|
||||
proxyConnectionId: const SingboxProxyConnectionId('profile-1'),
|
||||
clearDataOnExit: true,
|
||||
);
|
||||
|
||||
final json = metadata.toJson();
|
||||
final restored = ContainerMetadata.fromJson(json);
|
||||
|
||||
expect(
|
||||
json['proxyConnectionId'],
|
||||
const SingboxProxyConnectionId('profile-1').encode(),
|
||||
);
|
||||
expect(json['clearDataOnExit'], isTrue);
|
||||
expect(restored.proxyConnectionId, metadata.proxyConnectionId);
|
||||
expect(restored.usesTorProxy, isFalse);
|
||||
});
|
||||
|
||||
test('usesTorProxy is true only for the Tor connection id', () {
|
||||
final tor = ContainerMetadata.withDefaults(
|
||||
proxyConnectionId: const TorProxyConnectionId(),
|
||||
);
|
||||
|
||||
expect(tor.usesTorProxy, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_edit.dart';
|
||||
import 'package:weblibre/features/proxy/data/proxy_connection.dart';
|
||||
import 'package:weblibre/features/proxy/domain/providers/proxy_connection_options.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'clearing proxy selection does not leave cookie isolation enabled in create mode',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
proxyConnectionOptionsProvider.overrideWith(
|
||||
(ref) => const <ProxyConnectionOption>[],
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: ContainerEditScreen.create(
|
||||
initialContainer: ContainerData(
|
||||
id: 'container-1',
|
||||
color: Colors.blue,
|
||||
orderKey: 'a',
|
||||
metadata: ContainerMetadata.withDefaults(
|
||||
proxyConnectionId: SingboxProxyConnectionId('missing-proxy'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Proxy Connection'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Clear'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final cookieIsolationTile = tester.widget<SwitchListTile>(
|
||||
find.widgetWithText(SwitchListTile, 'Cookie Isolation'),
|
||||
);
|
||||
|
||||
expect(cookieIsolationTile.value, isFalse);
|
||||
expect(find.text('None'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'dismissing proxy picker does not leave cookie isolation enabled in create mode',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: [
|
||||
proxyConnectionOptionsProvider.overrideWith(
|
||||
(ref) => const <ProxyConnectionOption>[],
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
home: ContainerEditScreen.create(
|
||||
initialContainer: ContainerData(
|
||||
id: 'container-1',
|
||||
color: Colors.blue,
|
||||
orderKey: 'a',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text('Proxy Connection'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapAt(const Offset(8, 8));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final cookieIsolationTile = tester.widget<SwitchListTile>(
|
||||
find.widgetWithText(SwitchListTile, 'Cookie Isolation'),
|
||||
);
|
||||
|
||||
expect(cookieIsolationTile.value, isFalse);
|
||||
expect(find.text('New Container'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user