import 'dart:convert'; import 'package:crdt_sync/crdt_sync.dart'; import 'package:diet_guard_app/services/firebase_client.dart'; import 'package:diet_guard_app/services/google_sign_in_backend.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart' as http_testing; 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 = []; 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 = []; 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 = []; 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 = []; await expectLater( openFirebaseWithGoogle( tokenFetcher: () async => 'a-token', accountSaver: (account) async => saved.add(account), httpClient: _signsInAs('someone@example.com', uid: 'a-different-uid'), ), throwsA(isA()), ); expect(saved, isEmpty); }); }); }