// flow_connect_test.dart — L2 点连接能否连上(真 toggle → fetchConfig → bridge → on)。 // // 驱动真实 ConnectionController + 真实 ConnectApi(注入 MockClient,不打真网络) // + VpnBridgeMock,验证你最初诉求「点击连接是否能连上」: // toggle → connecting →(取配置 + bridge.start)→ statusStream 确认 → on → 再 toggle → off。 // 关键红线:on 必须由 bridge.statusStream 确认后才置(非乐观翻转)。 import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:pangolin_vpn/bridge/vpn_bridge_mock.dart'; import 'package:pangolin_vpn/bridge/vpn_bridge_provider.dart'; import 'package:pangolin_vpn/models/node.dart'; import 'package:pangolin_vpn/services/connect_api.dart'; import 'package:pangolin_vpn/services/token_store.dart'; import 'package:pangolin_vpn/state/connection_provider.dart'; import 'package:pangolin_vpn/state/nodes_provider.dart'; import 'package:pangolin_vpn/state/auth_provider.dart'; class _NullTokenStore implements TokenStore { const _NullTokenStore(); @override Future saveTokens({required String access, required String refresh}) async {} @override Future loadAccessToken() async => null; @override Future loadRefreshToken() async => null; @override Future clear() async {} @override Future markOnboarded() async {} @override Future isOnboarded() async => true; @override Future saveLastEmail(String email) async {} @override Future loadLastEmail() async => null; } /// uuid 非空 = 就绪节点,_connect 会真正取配置 + 启动 bridge。 const _readyNode = Node(code: 'HK', nameZh: '香港', nameEn: 'Hong Kong', ping: 18, uuid: 'hk-01'); Future _until(bool Function() cond, {Duration timeout = const Duration(seconds: 2)}) async { final sw = Stopwatch()..start(); while (!cond() && sw.elapsed < timeout) { await Future.delayed(const Duration(milliseconds: 5)); } } void main() { test('点连接 → connecting → on → 再点 → off', () async { // 假控制面:任何 /v1/nodes/.../connect 都回一份占位 config(200)。 final mock = MockClient((req) async => http.Response('{"fake":"singbox-config"}', 200)); final c = ProviderContainer(overrides: [ tokenStoreProvider.overrideWithValue(const _NullTokenStore()), effectiveNodeProvider.overrideWithValue(_readyNode), vpnBridgeProvider.overrideWithValue( VpnBridgeMock(connectDelay: const Duration(milliseconds: 20)), ), connectApiFactoryProvider.overrideWithValue( (token) => ConnectApi(baseUrl: 'http://test.local', authToken: token, client: mock), ), ]); addTearDown(c.dispose); expect(c.read(connectionProvider).phase, VpnPhase.off, reason: '初始 off'); // 点连接 c.read(connectionProvider.notifier).toggle(); await Future.delayed(Duration.zero); expect(c.read(connectionProvider).phase, VpnPhase.connecting, reason: 'toggle 立即进 connecting'); // 等取配置(fake)+ bridge.start + connectDelay → 内核回调 on await _until(() => c.read(connectionProvider).phase == VpnPhase.on); expect(c.read(connectionProvider).phase, VpnPhase.on, reason: '连上:由 bridge.statusStream 确认后才置 on'); expect(c.read(connectionProvider).error, isNull); // 再点 → 断开 c.read(connectionProvider.notifier).toggle(); await _until(() => c.read(connectionProvider).phase == VpnPhase.off); expect(c.read(connectionProvider).phase, VpnPhase.off, reason: '断开回 off'); }); }