217 lines
7.2 KiB
Dart
217 lines
7.2 KiB
Dart
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<GuestAuthState>());
|
|
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<DioException>()),
|
|
);
|
|
|
|
expect(fixture.controller.state.value, isA<LoggedInAuthState>());
|
|
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<GuestAuthState>());
|
|
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<void>(
|
|
requestOptions: options,
|
|
statusCode: 401,
|
|
),
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
handler.resolve(
|
|
Response<List<int>>(
|
|
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<int>,
|
|
).smsCode;
|
|
if (fixture.failDeletion) {
|
|
handler.reject(DioException(requestOptions: options));
|
|
} else {
|
|
handler.resolve(
|
|
Response<List<int>>(
|
|
requestOptions: options,
|
|
statusCode: 200,
|
|
data: const <int>[],
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
if (options.path == '/api/v1/auth/onekey/verify') {
|
|
handler.resolve(
|
|
Response<List<int>>(
|
|
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<String, String> _values = {};
|
|
|
|
@override
|
|
Future<void> delete(String key) async => _values.remove(key);
|
|
|
|
@override
|
|
Future<String?> read(String key) async => _values[key];
|
|
|
|
@override
|
|
Future<void> write(String key, String value) async {
|
|
_values[key] = value;
|
|
}
|
|
}
|