From 2f95c6cb6961933f7a20524cc96916c0e088f1c7 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 22 Aug 2026 13:01:18 +0200 Subject: [PATCH] Stop the history tests reading the developer's own Firebase account 14 tests failed with a RemoteSyncError from listDirectory, and the cause was not in the screen they were testing. The sync guard short-circuits on "no backend configured", but it asks that question two ways: the sync token AND whether a Firebase account exists. The tests faked the token into secure storage and stopped there, so on a machine holding a cached refresh token the real factory answered yes, the guard opened, and the fetch escaped to the network -- where flutter_test blocks HttpClient. That went unnoticed until the Linux desktop target landed, because the desktop build is what authenticates from the shared refresh token: the same tests pass on the commit before it, on this machine, this minute. So the fixture now pins firebaseFactory to "no account" by default, which is what the faked token was already trying to express, and HistoryScreen takes it as a tests-only seam next to the httpClient one it already had. The two direct constructions in workout_sync_read_test.dart are pinned the same way. 402 Flutter tests pass and flutter analyze is clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DfJC1Wu1SEgJ6X49k5Jqtg --- .../lib/screens/history_screen.dart | 13 ++++++++++++- .../test/screens/_history_test_fixtures.dart | 18 ++++++++++++++++-- .../test/services/workout_sync_read_test.dart | 3 +++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/stronglift_replacement/workout_app/lib/screens/history_screen.dart b/stronglift_replacement/workout_app/lib/screens/history_screen.dart index d1f15ef..2e82e39 100644 --- a/stronglift_replacement/workout_app/lib/screens/history_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/history_screen.dart @@ -8,6 +8,7 @@ library; import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import 'package:crdt_sync/crdt_sync.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:workout_app/models/exercise.dart'; @@ -29,11 +30,20 @@ class HistoryScreen extends StatefulWidget { /// /// [httpClient] is injected only by tests, so the synced-workout fetch can /// be driven without real network — same pattern as `SettingsScreen`. - const HistoryScreen({super.key, this.httpClient}); + /// + /// [firebaseFactory] is likewise tests-only, and is NOT redundant with + /// [httpClient]: the sync guard short-circuits on "no backend configured", + /// and it asks Firebase that question directly. On a desktop that has a + /// cached refresh token the real factory answers yes, so the guard opens and + /// the fetch goes to the network even with the sync token faked away. + const HistoryScreen({super.key, this.httpClient, this.firebaseFactory}); /// Overrides the HTTP client used to read synced workouts (tests only). final http.Client? httpClient; + /// Overrides how a Firebase account is discovered (tests only). + final Future Function()? firebaseFactory; + @override State createState() => _HistoryScreenState(); } @@ -106,6 +116,7 @@ class _HistoryScreenState extends State { Future>> _readSyncedPayloads() => WorkoutSyncService( httpClient: widget.httpClient, + firebaseFactory: widget.firebaseFactory, ).readMergedWorkoutPayloads(); Future _pickExercise(String name) async { diff --git a/stronglift_replacement/workout_app/test/screens/_history_test_fixtures.dart b/stronglift_replacement/workout_app/test/screens/_history_test_fixtures.dart index 5478934..809ecce 100644 --- a/stronglift_replacement/workout_app/test/screens/_history_test_fixtures.dart +++ b/stronglift_replacement/workout_app/test/screens/_history_test_fixtures.dart @@ -23,9 +23,23 @@ Future pumpHistory(WidgetTester tester, Widget w) async { await tester.pump(); } -Widget wrapHistory({http.Client? httpClient}) => MaterialApp( +/// Wraps the history screen with sync pinned to a known state. +/// +/// [firebaseFactory] defaults to "no account", which is what makes these +/// tests independent of the machine they run on. Faking the sync token is not +/// enough on its own: the guard also asks Firebase whether an account exists, +/// and on a desktop holding a cached refresh token the real factory says yes, +/// so the fetch escapes to the network and the test fails with a +/// RemoteSyncError that has nothing to do with the screen. +Widget wrapHistory({ + http.Client? httpClient, + Future Function()? firebaseFactory, +}) => MaterialApp( theme: buildAppTheme(), - home: HistoryScreen(httpClient: httpClient), + home: HistoryScreen( + httpClient: httpClient, + firebaseFactory: firebaseFactory ?? () async => null, + ), ); /// A GitHub mock serving one PC device log holding a run and a manual. diff --git a/stronglift_replacement/workout_app/test/services/workout_sync_read_test.dart b/stronglift_replacement/workout_app/test/services/workout_sync_read_test.dart index b450685..694a707 100644 --- a/stronglift_replacement/workout_app/test/services/workout_sync_read_test.dart +++ b/stronglift_replacement/workout_app/test/services/workout_sync_read_test.dart @@ -75,6 +75,7 @@ void main() { ); final payloads = await WorkoutSyncService( httpClient: httpClient, + firebaseFactory: () async => null, ).readMergedManualPayloads(); expect(payloads, hasLength(1)); expect(payloads.single['cost'], 'NEW'); @@ -148,6 +149,7 @@ void main() { ); final manuals = await WorkoutSyncService( httpClient: httpClient, + firebaseFactory: () async => null, ).readMergedManualPayloads(); expect(manuals, hasLength(1)); expect(manuals.single['kind'], 'manual_workout'); @@ -207,6 +209,7 @@ void main() { final payloads = await WorkoutSyncService( httpClient: httpClient, + firebaseFactory: () async => null, ).readMergedManualPayloads(); expect(payloads, hasLength(1));