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:
@@ -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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user