diet-guard/app/test/firebase_backend_google_test.dart
Krzysztof kuhy Rudnicki 546041584f
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
release-apk / release (APK) (push) Has been cancelled
Add Google sign-in support for the Firebase sync account
One-tap sign-in for a fresh install, matching the pattern already shipped
in todo and crdt_sync_dart: openFirebaseWithGoogle() (google_platform.dart
abstracts the plugin's platform-channel dependence), isFirebaseConfigured()
that reads the stored session rather than trusting the account marker
alone (a marker with no live session is what made a synced phone read as
"not connected" for a full session), and storedAccount() for read-back
without the desktop-wrapper fallback that throws on Android.

Also generalizes crdt_sync_dart's ref pin to v0.10.0 and vendors the
override-guard script for the pre-commit hook already wired to it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KdSp6Nbrs9xvsWkCMDN9a1
2026-08-12 23:36:10 +02:00

129 lines
4.9 KiB
Dart

import 'dart:convert';
import 'package:crdt_sync/crdt_sync.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart' as http_testing;
import 'package:diet_guard_app/services/firebase_backend.dart';
import 'package:diet_guard_app/services/google_sign_in_backend.dart';
import 'fake_secure_storage.dart';
/// Answers a `signInWithIdp` call the way Firebase does.
http_testing.MockClient _signsInAs(String email, {String uid = kSyncUid}) =>
http_testing.MockClient(
(request) async => http.Response(
jsonEncode({
'idToken': 'id',
'refreshToken': 'refresh',
'expiresIn': '3600',
'email': email,
'localId': uid,
}),
200,
),
);
/// Answers the way Firebase does for an account that withholds its email.
///
/// `signInWithIdp` simply omits the field -- the session is otherwise
/// identical and perfectly usable.
http_testing.MockClient _signsInWithoutEmail() => http_testing.MockClient(
(request) async => http.Response(
jsonEncode({
'idToken': 'id',
'refreshToken': 'refresh',
'expiresIn': '3600',
'localId': kSyncUid,
}),
200,
),
);
void main() {
// installFakeSecureStorage touches the test binary messenger, which only
// exists once the binding is initialised.
TestWidgetsFlutterBinding.ensureInitialized();
// openFirebaseWithGoogle reaches the keystore through a platform channel;
// the fake stands in so the account write is really exercised rather than
// stubbed out at the call site.
setUp(installFakeSecureStorage);
group('openFirebaseWithGoogle', () {
test('returns null when the picker is dismissed', () async {
// Cancelling is ordinary: no session, no account, no error.
final saved = <FirebaseAccount>[];
final client = await openFirebaseWithGoogle(
tokenFetcher: () async => null,
accountSaver: (account) async => saved.add(account),
);
expect(client, isNull);
expect(saved, isEmpty);
});
test('stores the account under the email Firebase reports', () async {
// The regression this test exists for. A fresh install has no email
// anywhere on the device, so it must come from the sign-in response. An
// earlier cut read it from the settings screen's text field -- empty on
// exactly this path -- and persisted FirebaseAccount(email: ''); the
// next launch then took the password branch with '' and failed.
final saved = <FirebaseAccount>[];
final client = await openFirebaseWithGoogle(
tokenFetcher: () async => 'a-token',
accountSaver: (account) async => saved.add(account),
httpClient: _signsInAs('signed-in@example.com'),
);
expect(client, isNotNull);
expect(saved, hasLength(1));
expect(saved.single.email, 'signed-in@example.com');
// No password to store: this device authenticates with the refresh
// token, and openFirebase() must not later try to sign in with ''.
expect(saved.single.password, isEmpty);
client!.close();
});
test('stores the account even when Firebase reports no email', () async {
// The regression that cost a night of syncing. `signInWithIdp` omits
// `email` when the Google account withholds it; the account write used
// to be gated on that field while the client was returned regardless,
// so this call reported success, persisted nothing, and the next launch
// found no account and silently dropped to GitHub-only with a 401.
//
// The refresh token -- the thing that actually authenticates -- is
// already stored by now. An empty email is the honest value and costs
// nothing: openFirebase() reads the session, not the address.
final saved = <FirebaseAccount>[];
final client = await openFirebaseWithGoogle(
tokenFetcher: () async => 'a-token',
accountSaver: (account) async => saved.add(account),
httpClient: _signsInWithoutEmail(),
);
expect(client, isNotNull);
expect(saved, hasLength(1));
expect(saved.single.email, isEmpty);
expect(saved.single.password, isEmpty);
client!.close();
});
test('stores nothing when Google resolves to the wrong uid', () async {
// Authentication succeeds and the uid is wrong, which the security
// rules would deny on every read and write. Failing here -- with
// nothing persisted -- leaves the device retryable rather than stuck.
final saved = <FirebaseAccount>[];
await expectLater(
openFirebaseWithGoogle(
tokenFetcher: () async => 'a-token',
accountSaver: (account) async => saved.add(account),
httpClient: _signsInAs('someone@example.com', uid: 'a-different-uid'),
),
throwsA(isA<FirebaseAuthError>()),
);
expect(saved, isEmpty);
});
});
}