import 'package:dio/dio.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:jinzhi_flutter_app/common/storage/secure_identity_storage.dart'; import 'package:jinzhi_flutter_app/common/storage/storage_keys.dart'; import 'package:jinzhi_flutter_app/features/auth/application/auth_controller.dart'; import 'package:jinzhi_flutter_app/features/auth/data/auth_models.dart'; import 'package:jinzhi_flutter_app/models/pb/jinzhi/v1/account.pb.dart' as pb; import 'package:shared_preferences/shared_preferences.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() => SharedPreferences.setMockInitialValues({})); test('restores a token by loading the real profile', () async { final fixture = await _Fixture.create(); await fixture.identity.saveJwt('token'); await fixture.controller.retryBootstrap(); final auth = fixture.controller.state.value! as LoggedInAuthState; expect(auth.token, 'token'); expect(auth.user.id, 'user-1'); expect(auth.user.phone, '13800138000'); expect(fixture.preferences.getString(storageUserId), 'user-1'); }); test('deletes the server account before clearing local identity', () async { final fixture = await _Fixture.create(); await fixture.identity.saveJwt('token'); await fixture.controller.retryBootstrap(); await fixture.controller.deleteAccount('123456'); expect(fixture.deletedWithCode, '123456'); expect(fixture.controller.state.value, isA()); expect(await fixture.identity.readJwt(), isNull); expect(fixture.preferences.getString(storageUserId), isNull); }); test('keeps the local session when server deletion fails', () async { final fixture = await _Fixture.create(); await fixture.identity.saveJwt('token'); await fixture.controller.retryBootstrap(); fixture.failDeletion = true; await expectLater( fixture.controller.deleteAccount('123456'), throwsA(isA()), ); expect(fixture.controller.state.value, isA()); expect(await fixture.identity.readJwt(), 'token'); expect(fixture.preferences.getString(storageUserId), 'user-1'); }); test('clears an unauthorized restored session', () async { final fixture = await _Fixture.create(); await fixture.identity.saveJwt('expired'); fixture.profileUnauthorized = true; await fixture.controller.retryBootstrap(); expect(fixture.controller.state.value, isA()); expect(await fixture.identity.readJwt(), isNull); }); test('keeps a valid token and cached profile on a network failure', () async { final fixture = await _Fixture.create(); await fixture.identity.saveJwt('token'); await fixture.preferences.setString('jinzhi_mock_logged_in', '13900139000'); fixture.failProfile = true; await fixture.controller.retryBootstrap(); final auth = fixture.controller.state.value! as LoggedInAuthState; expect(auth.user.id, isEmpty); expect(auth.user.phone, '13900139000'); expect(await fixture.identity.readJwt(), 'token'); expect(fixture.preferences.getString(storageUserPhone), '13900139000'); expect(fixture.preferences.containsKey('jinzhi_mock_logged_in'), isFalse); }); test('keeps the verified one-key login state shape', () async { final fixture = await _Fixture.create(); await fixture.controller.verifyOneKeyLogin('umeng-token'); final auth = fixture.controller.state.value! as LoggedInAuthState; expect(auth.token, 'one-key-token'); expect(auth.user.phone, '13800138000'); expect(await fixture.identity.readJwt(), 'one-key-token'); }); } class _Fixture { _Fixture({ required this.preferences, required this.identity, required this.controller, }); final SharedPreferences preferences; final SecureIdentityStorage identity; final AuthController controller; bool failDeletion = false; bool failProfile = false; bool profileUnauthorized = false; String? deletedWithCode; static Future<_Fixture> create() async { final preferences = await SharedPreferences.getInstance(); final identity = SecureIdentityStorage( preferences, secureStorage: _MemorySecureStorage(), ); late _Fixture fixture; final dio = Dio() ..interceptors.add( InterceptorsWrapper( onRequest: (options, handler) { if (options.path == '/api/v1/me' && options.method == 'GET') { if (fixture.failProfile) { handler.reject(DioException(requestOptions: options)); return; } if (fixture.profileUnauthorized) { handler.reject( DioException( requestOptions: options, response: Response( requestOptions: options, statusCode: 401, ), ), ); return; } handler.resolve( Response>( requestOptions: options, statusCode: 200, data: pb.ProfileReply( user: pb.UserInfo( id: 'user-1', phone: '13800138000', createdAt: '2026-09-08T00:00:00Z', ), ).writeToBuffer(), ), ); return; } if (options.path == '/api/v1/me' && options.method == 'DELETE') { fixture.deletedWithCode = pb.DeleteAccountRequest.fromBuffer( options.data as List, ).smsCode; if (fixture.failDeletion) { handler.reject(DioException(requestOptions: options)); } else { handler.resolve( Response>( requestOptions: options, statusCode: 200, data: const [], ), ); } return; } if (options.path == '/api/v1/auth/onekey/verify') { handler.resolve( Response>( requestOptions: options, statusCode: 200, data: pb.VerifySmsCodeReply( accessToken: 'one-key-token', user: pb.UserInfo(phone: '13800138000'), ).writeToBuffer(), ), ); return; } handler.next(options); }, ), ); final controller = AuthController( dio, identityStorage: identity, preferences: preferences, autoBootstrap: false, ); fixture = _Fixture( preferences: preferences, identity: identity, controller: controller, ); return fixture; } } class _MemorySecureStorage implements SecureKeyValueStorage { final Map _values = {}; @override Future delete(String key) async => _values.remove(key); @override Future read(String key) async => _values[key]; @override Future write(String key, String value) async { _values[key] = value; } }