mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 15:54:11 +01:00
Every repo in this fleet is public, so where each value lives is a disclosure decision rather than a style choice. FirebaseProject holds apiKey + databaseUrl: these identify the *project*, and the Web API key is a public identifier by design -- it ships inside every Android APK, and the security rules, not the key, are what protect the data. Safe to bake into app source, which keeps a reinstall zero-setup. FirebaseAccount holds the email and password: these identify *a person*, so they are entered once per device and kept in the platform keystore, never in SharedPreferences and never in a repo. The uid is deliberately absent from both. Nothing client-side needs it -- the rules compare auth.uid server-side, so a wrong account is refused by the database. A local copy would add a way to be wrong without adding protection. Keyed at `firebase.account`, distinct from `sync.token` (the GitHub PAT) and `firebase.credentials` (the refresh token), so all three coexist during the mirrored cutover. 13 tests, 100% line coverage; 241 in the suite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SJ9sxEw3ymzXNjL7SHNpJc
78 lines
2.6 KiB
Dart
78 lines
2.6 KiB
Dart
/// Tests for the public/private split of the Firebase settings.
|
|
///
|
|
/// Every repo in this fleet is public, so which half a value lands in is a
|
|
/// disclosure decision, not a style choice. These tests pin that split.
|
|
library;
|
|
|
|
import 'package:crdt_sync/crdt_sync.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
const _project = FirebaseProject(
|
|
apiKey: 'AIzaSyExample',
|
|
databaseUrl: 'https://x-rtdb.europe-west1.firebasedatabase.app',
|
|
);
|
|
|
|
void main() {
|
|
group('FirebaseProject', () {
|
|
test('parses the project half of a firebase.json', () {
|
|
final project = FirebaseProject.fromJson(const {
|
|
'apiKey': 'AIzaSyExample',
|
|
'databaseUrl': 'https://x-rtdb.europe-west1.firebasedatabase.app',
|
|
'projectId': 'ignored',
|
|
'uid': 'ignored',
|
|
'email': 'ignored',
|
|
});
|
|
|
|
expect(project.apiKey, 'AIzaSyExample');
|
|
expect(project.databaseUrl, endsWith('firebasedatabase.app'));
|
|
});
|
|
|
|
test('builds a usable config from a per-device email', () {
|
|
final config = _project.configFor('someone@example.com');
|
|
|
|
expect(config.apiKey, _project.apiKey);
|
|
expect(config.databaseUrl, _project.databaseUrl);
|
|
expect(config.email, 'someone@example.com');
|
|
});
|
|
|
|
test('carries no uid, because the rules check it server-side', () {
|
|
// A client-side copy of the uid adds a way to be wrong without adding
|
|
// any protection: the database refuses a wrong account regardless.
|
|
expect(_project.configFor('a@b.c').uid, isEmpty);
|
|
});
|
|
});
|
|
|
|
group('FirebaseAccount', () {
|
|
test('round-trips through the keystore blob', () {
|
|
const account = FirebaseAccount(email: 'a@b.c', password: 'hunter2');
|
|
|
|
final parsed = FirebaseAccount.tryParse(account.toJsonString());
|
|
|
|
expect(parsed, isNotNull);
|
|
expect(parsed!.email, 'a@b.c');
|
|
expect(parsed.password, 'hunter2');
|
|
});
|
|
|
|
test('is keyed apart from the token and the refresh credentials', () {
|
|
// All three coexist for the whole mirrored cutover.
|
|
expect(kFirebaseAccountKey, isNot('sync.token'));
|
|
expect(kFirebaseAccountKey, isNot(SecureCredentialStore.defaultKey));
|
|
});
|
|
|
|
for (final (name, raw) in <(String, String?)>[
|
|
('null', null),
|
|
('empty', ''),
|
|
('corrupt JSON', '{oops'),
|
|
('a list', '[1,2]'),
|
|
('a missing password', '{"email":"a@b.c"}'),
|
|
('a non-string email', '{"email":1,"password":"p"}'),
|
|
('an empty email', '{"email":"","password":"p"}'),
|
|
('an empty password', '{"email":"a@b.c","password":""}'),
|
|
]) {
|
|
test('treats $name as not configured', () {
|
|
expect(FirebaseAccount.tryParse(raw), isNull);
|
|
});
|
|
}
|
|
});
|
|
}
|