Files
jinzhi/test/common/storage/secure_identity_storage_test.dart
T

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);
}
}