utils/crdt_sync_dart/test/firebase_settings_test.dart
Krzysztof kuhy Rudnicki fc99eff005
Some checks are pending
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
crdt_sync_dart 0.6.0: split Firebase settings by what is safe to publish
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
2026-08-04 23:05:13 +02:00

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