test(client): L2 登陆/重启续登/登出 流程(真 AuthNotifier)

驱动真实 AuthNotifier(非桩),仅在可注入接缝 TokenStore 处换内存实现,覆盖最初诉求:
- 登陆:saveTokens → isLoggedIn + 令牌持久化
- 重启续登:新 AuthNotifier 从同一 store 恢复登录态(无需重输密码)
- 登出:logout → 未登录 + store 清空,再重启仍未登录
- 3 用例全过

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-25 12:10:52 +08:00
parent cb6fc25bc9
commit 95fad37989
+96
View File
@@ -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<void> saveTokens({required String access, required String refresh}) async {
_access = access;
_refresh = refresh;
}
@override
Future<String?> loadAccessToken() async => _access;
@override
Future<String?> loadRefreshToken() async => _refresh;
@override
Future<void> clear() async {
_access = null;
_refresh = null;
}
@override
Future<void> markOnboarded() async => _onboarded = true;
@override
Future<bool> isOnboarded() async => _onboarded;
@override
Future<void> saveLastEmail(String email) async => _email = email;
@override
Future<String?> loadLastEmail() async => _email;
}
/// 等 AuthNotifier 构造时的 _loadFromStore() 这个 microtask 跑完。
Future<void> _settle() => Future<void>.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();
});
}