173 lines
5.7 KiB
Dart
173 lines
5.7 KiB
Dart
import 'dart:async';
|
|
|
|
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/common/storage/preferences_storage.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
void main() {
|
|
setUp(() => SharedPreferences.setMockInitialValues({}));
|
|
|
|
test(
|
|
'migrates a legacy SharedPreferences token into secure storage',
|
|
() async {
|
|
SharedPreferences.setMockInitialValues({
|
|
storageLegacyAccessToken: ' legacy ',
|
|
});
|
|
final preferences = await SharedPreferences.getInstance();
|
|
final secure = _MemorySecureStorage();
|
|
final storage = SecureIdentityStorage(
|
|
PreferencesStorage(preferences),
|
|
secureStorage: secure,
|
|
);
|
|
|
|
expect(await storage.restoreAccessToken(), 'legacy');
|
|
expect(await secure.read('access_token'), 'legacy');
|
|
expect(preferences.containsKey(storageLegacyAccessToken), isFalse);
|
|
expect(preferences.getBool(storageAuthSessionActive), isTrue);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'rejects an orphaned Keychain token when the install marker is absent',
|
|
() async {
|
|
final preferences = await SharedPreferences.getInstance();
|
|
final secure = _MemorySecureStorage({'jwt_token': 'secure-legacy'});
|
|
final storage = SecureIdentityStorage(
|
|
PreferencesStorage(preferences),
|
|
secureStorage: secure,
|
|
);
|
|
|
|
expect(await storage.restoreAccessToken(), isNull);
|
|
expect(await secure.read('access_token'), isNull);
|
|
expect(await secure.read('jwt_token'), isNull);
|
|
},
|
|
);
|
|
|
|
test('restores a secure token only with the installation marker', () async {
|
|
SharedPreferences.setMockInitialValues({storageAuthSessionActive: true});
|
|
final preferences = await SharedPreferences.getInstance();
|
|
final secure = _MemorySecureStorage({secureAccessToken: 'active'});
|
|
final storage = SecureIdentityStorage(
|
|
PreferencesStorage(preferences),
|
|
secureStorage: secure,
|
|
);
|
|
|
|
expect(await storage.restoreAccessToken(), 'active');
|
|
});
|
|
|
|
test('logout clears session secrets but preserves device identity', () async {
|
|
SharedPreferences.setMockInitialValues({storageAuthSessionActive: true});
|
|
final preferences = await SharedPreferences.getInstance();
|
|
final secure = _MemorySecureStorage({
|
|
secureAccessToken: 'access',
|
|
secureMallCredential: 'mall',
|
|
secureRefreshToken: 'refresh',
|
|
secureBiometricCredential: 'biometric',
|
|
secureDeviceId: 'device',
|
|
});
|
|
final storage = SecureIdentityStorage(
|
|
PreferencesStorage(preferences),
|
|
secureStorage: secure,
|
|
);
|
|
|
|
await storage.clearSessionCredentials();
|
|
|
|
expect(await secure.read(secureAccessToken), isNull);
|
|
expect(await secure.read(secureMallCredential), isNull);
|
|
expect(await secure.read(secureRefreshToken), isNull);
|
|
expect(await secure.read(secureBiometricCredential), 'biometric');
|
|
expect(await secure.read(secureDeviceId), 'device');
|
|
|
|
await storage.clearAccountCredentials();
|
|
expect(await secure.read(secureBiometricCredential), isNull);
|
|
expect(await secure.read(secureDeviceId), 'device');
|
|
});
|
|
|
|
test(
|
|
'serializes restore with logout so an old token cannot revive',
|
|
() async {
|
|
SharedPreferences.setMockInitialValues({storageAuthSessionActive: true});
|
|
final preferences = await SharedPreferences.getInstance();
|
|
final secure = _BlockingReadSecureStorage({
|
|
secureAccessToken: 'old-token',
|
|
});
|
|
final session = AuthSessionStorage(
|
|
SecureIdentityStorage(
|
|
PreferencesStorage(preferences),
|
|
secureStorage: secure,
|
|
),
|
|
);
|
|
|
|
final restoring = session.restore();
|
|
await secure.readStarted.future;
|
|
final clearing = session.clear();
|
|
secure.releaseRead.complete();
|
|
|
|
expect(await restoring, 'old-token');
|
|
await clearing;
|
|
expect(session.accessToken, isNull);
|
|
expect(await secure.read(secureAccessToken), isNull);
|
|
},
|
|
);
|
|
|
|
test('serializes restore with login so the new token wins', () async {
|
|
SharedPreferences.setMockInitialValues({storageAuthSessionActive: true});
|
|
final preferences = await SharedPreferences.getInstance();
|
|
final secure = _BlockingReadSecureStorage({secureAccessToken: 'old-token'});
|
|
final session = AuthSessionStorage(
|
|
SecureIdentityStorage(
|
|
PreferencesStorage(preferences),
|
|
secureStorage: secure,
|
|
),
|
|
);
|
|
|
|
final restoring = session.restore();
|
|
await secure.readStarted.future;
|
|
final saving = session.save('new-token');
|
|
secure.releaseRead.complete();
|
|
|
|
expect(await restoring, 'old-token');
|
|
await saving;
|
|
expect(session.accessToken, 'new-token');
|
|
expect(await secure.read(secureAccessToken), 'new-token');
|
|
});
|
|
}
|
|
|
|
class _MemorySecureStorage implements SecureKeyValueStorage {
|
|
_MemorySecureStorage([Map<String, String>? initial])
|
|
: _values = {...?initial};
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
class _BlockingReadSecureStorage extends _MemorySecureStorage {
|
|
_BlockingReadSecureStorage(super.initial);
|
|
|
|
final readStarted = Completer<void>();
|
|
final releaseRead = Completer<void>();
|
|
var _blocked = false;
|
|
|
|
@override
|
|
Future<String?> read(String key) async {
|
|
if (key == secureAccessToken && !_blocked) {
|
|
_blocked = true;
|
|
readStarted.complete();
|
|
await releaseRead.future;
|
|
}
|
|
return super.read(key);
|
|
}
|
|
}
|