diff --git a/client/test/unit/flow_auth_test.dart b/client/test/unit/flow_auth_test.dart new file mode 100644 index 0000000..b52e9a8 --- /dev/null +++ b/client/test/unit/flow_auth_test.dart @@ -0,0 +1,96 @@ +// flow_auth_test.dart — L2 登陆/重启续登/登出 流程(真 AuthNotifier + 内存 TokenStore)。 +// +// 驱动真实的 AuthNotifier(非桩),只在可注入接缝(TokenStore)处换内存实现, +// 覆盖你最初诉求里的「自动登陆 / 重启续登 / 登出」。 +import 'package:flutter_test/flutter_test.dart'; +import 'package:pangolin_vpn/services/auth_api.dart'; +import 'package:pangolin_vpn/services/token_store.dart'; +import 'package:pangolin_vpn/state/auth_provider.dart'; + +/// 内存 TokenStore:实现接口、用字段存,避免 FlutterSecureStorage 平台依赖。 +class _MemTokenStore implements TokenStore { + String? _access; + String? _refresh; + bool _onboarded = false; + String? _email; + + @override + Future saveTokens({required String access, required String refresh}) async { + _access = access; + _refresh = refresh; + } + + @override + Future loadAccessToken() async => _access; + @override + Future loadRefreshToken() async => _refresh; + @override + Future clear() async { + _access = null; + _refresh = null; + } + + @override + Future markOnboarded() async => _onboarded = true; + @override + Future isOnboarded() async => _onboarded; + @override + Future saveLastEmail(String email) async => _email = email; + @override + Future loadLastEmail() async => _email; +} + +/// 等 AuthNotifier 构造时的 _loadFromStore() 这个 microtask 跑完。 +Future _settle() => Future.delayed(Duration.zero); + +void main() { + test('登陆:saveTokens → isLoggedIn 且令牌持久化', () async { + final store = _MemTokenStore(); + final auth = AuthNotifier(store); + await _settle(); + expect(auth.state.isLoggedIn, isFalse, reason: '初始未登录'); + + await auth.saveTokens( + const AuthTokens(accessToken: 'access-1', refreshToken: 'refresh-1'), + ); + + expect(auth.state.isLoggedIn, isTrue); + expect(auth.state.accessToken, 'access-1'); + expect(await store.loadAccessToken(), 'access-1', reason: '令牌落盘'); + expect(await store.loadRefreshToken(), 'refresh-1'); + auth.dispose(); + }); + + test('重启续登:新 AuthNotifier 从同一 store 恢复登录态', () async { + final store = _MemTokenStore(); + await store.saveTokens(access: 'persisted-access', refresh: 'persisted-refresh'); + + // 模拟「重启 app」:全新 AuthNotifier 实例读同一持久化 store。 + final auth = AuthNotifier(store); + await _settle(); // 等 _loadFromStore + + expect(auth.state.isLoggedIn, isTrue, reason: '续登:无需重新输密码'); + expect(auth.state.accessToken, 'persisted-access'); + auth.dispose(); + }); + + test('登出:logout → 未登录 且 store 清空(下次重启不再续登)', () async { + final store = _MemTokenStore(); + await store.saveTokens(access: 'a', refresh: 'r'); + final auth = AuthNotifier(store); + await _settle(); + expect(auth.state.isLoggedIn, isTrue); + + await auth.logout(); + + expect(auth.state.isLoggedIn, isFalse); + expect(await store.loadAccessToken(), isNull, reason: '令牌已清'); + + // 登出后再「重启」→ 仍是未登录。 + final after = AuthNotifier(store); + await _settle(); + expect(after.state.isLoggedIn, isFalse); + auth.dispose(); + after.dispose(); + }); +}