mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 12:47:39 +01:00
Final batch of the 250-line sweep. `check_file_length.sh --all` exits 0: every file in the repo is at or under 250 lines. github_mirror_screen.dart 391 -> 205 (+ actions, client_id_dialog parts) foodbank_service.dart 314 -> 209 (+ foodbank_service_manual part) settings_screen.dart 286 -> 237 (+ settings_kcal_goal part) firebase_backend.dart 282 -> 100 (+ firebase_client.dart) Three of the four are `part` files rather than separate libraries, and that is forced rather than stylistic: the moved code drives private state (`_busy`, `_status`, the text controllers) and calls private helpers (`_readBank`, `_normalize`), none of which are visible across a library boundary in Dart. Widening them to public just to split a file would leak internals the rest of the app has no business calling. Within those parts, mixins rather than extensions, for a concrete reason: an extension is not a subclass of `State`, so calling `setState` from one is an `invalid_use_of_protected_member`. Fields the State must own (the controllers it creates and disposes) stay declared there and are reached through abstract getters. `firebase_client.dart` is a real library split -- opening an authenticated client has no private coupling to the keystore-backed account record, only to its public API. The one exception, dropping the account marker without setting the opt-out flag, became `forgetAccountMarker()` rather than exposing the secure store. Verified: flutter test 555 (unchanged), flutter analyze clean, 707 Python tests / 100% branch coverage, check_patch_targets 0, pre-commit clean, check_file_length.sh --all exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
129 lines
4.9 KiB
Dart
129 lines
4.9 KiB
Dart
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 = <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);
|
|
});
|
|
});
|
|
}
|