95fad37989
驱动真实 AuthNotifier(非桩),仅在可注入接缝 TokenStore 处换内存实现,覆盖最初诉求: - 登陆:saveTokens → isLoggedIn + 令牌持久化 - 重启续登:新 AuthNotifier 从同一 store 恢复登录态(无需重输密码) - 登出:logout → 未登录 + store 清空,再重启仍未登录 - 3 用例全过 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
97 lines
3.2 KiB
Dart
97 lines
3.2 KiB
Dart
// 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();
|
|
});
|
|
}
|