utils/crdt_sync_dart/test/secure_credential_store_test.dart
Krzysztof kuhy Rudnicki 6c9cc71ae1 crdt_sync_dart 0.5.1: a shared secure credential store
Every Flutter consumer already keeps its GitHub token in
flutter_secure_storage, which is where a Firebase refresh token belongs too:
it is the long-lived secret, while ID tokens expire in about an hour.

Takes read/write/delete closures rather than depending on
flutter_secure_storage directly -- this package is pure Dart, and a Flutter
plugin dependency would make it unusable from the command-line tools and from
plain `dart test`. The adapter is two lines at each call site.

Keyed at `firebase.credentials`, namespaced away from each app's `sync.token`
so both secrets coexist for the whole mirrored cutover.

Every load failure degrades to "not signed in" rather than throwing: an app
that threw here would be permanently unable to sync, with no way back short of
a reinstall.

10 tests, 100% line coverage; 228 in the suite.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJ9sxEw3ymzXNjL7SHNpJc
2026-08-04 23:01:54 +02:00

118 lines
3.4 KiB
Dart

/// Tests for the platform-secret-backed credential store.
///
/// The load path degrades to "not signed in" on every failure rather than
/// throwing: an app that threw here would be permanently unable to sync, with
/// no way back short of a reinstall.
library;
import 'dart:convert';
import 'package:crdt_sync/crdt_sync.dart';
import 'package:test/test.dart';
/// An in-memory stand-in for `flutter_secure_storage`.
class _FakeSecrets {
final Map<String, String> values = {};
bool readThrows = false;
Future<String?> read(String key) async {
if (readThrows) throw const FormatException('keystore unavailable');
return values[key];
}
Future<void> write(String key, String value) async => values[key] = value;
Future<void> delete(String key) async => values.remove(key);
}
SecureCredentialStore _store(_FakeSecrets secrets, {String? key}) =>
SecureCredentialStore(
read: secrets.read,
write: secrets.write,
delete: secrets.delete,
key: key ?? SecureCredentialStore.defaultKey,
);
FirebaseCredentials _creds() => FirebaseCredentials(
idToken: 'id',
refreshToken: 'refresh',
expiresAt: DateTime.utc(2026, 8, 5, 7, 45),
);
void main() {
test('round-trips credentials', () async {
final secrets = _FakeSecrets();
await _store(secrets).save(_creds());
final loaded = await _store(secrets).load();
expect(loaded, isNotNull);
expect(loaded!.refreshToken, 'refresh');
expect(loaded.idToken, 'id');
expect(loaded.expiresAt, DateTime.utc(2026, 8, 5, 7, 45));
});
test('does not collide with the app GitHub token key', () async {
// Both secrets coexist for the whole mirrored cutover period.
final secrets = _FakeSecrets();
secrets.values['sync.token'] = 'github-pat';
await _store(secrets).save(_creds());
expect(secrets.values['sync.token'], 'github-pat');
expect(secrets.values.containsKey('firebase.credentials'), isTrue);
});
test('honours a custom key', () async {
final secrets = _FakeSecrets();
await _store(secrets, key: 'other').save(_creds());
expect(secrets.values.containsKey('other'), isTrue);
});
test('reports absent credentials as not signed in', () async {
expect(await _store(_FakeSecrets()).load(), isNull);
});
test('treats an empty value as not signed in', () async {
final secrets = _FakeSecrets()..values['firebase.credentials'] = '';
expect(await _store(secrets).load(), isNull);
});
test('treats corrupt JSON as not signed in', () async {
final secrets = _FakeSecrets()..values['firebase.credentials'] = '{oops';
expect(await _store(secrets).load(), isNull);
});
test('treats a non-object value as not signed in', () async {
final secrets = _FakeSecrets()..values['firebase.credentials'] = '[1,2]';
expect(await _store(secrets).load(), isNull);
});
test('treats a wrong-shaped object as not signed in', () async {
final secrets = _FakeSecrets()
..values['firebase.credentials'] = jsonEncode({'id_token': 'only'});
expect(await _store(secrets).load(), isNull);
});
test('treats an unreadable keystore as not signed in', () async {
final secrets = _FakeSecrets()..readThrows = true;
expect(await _store(secrets).load(), isNull);
});
test('clear removes the credentials', () async {
final secrets = _FakeSecrets();
await _store(secrets).save(_creds());
await _store(secrets).clear();
expect(secrets.values, isEmpty);
});
}