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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DfJC1Wu1SEgJ6X49k5Jqtg
This commit is contained in:
Krzysztof Rudnicki 2026-08-22 13:01:18 +02:00
parent c05dc9c224
commit 2f95c6cb69
3 changed files with 31 additions and 3 deletions

View File

@ -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<FirebaseRestClient?> Function()? firebaseFactory;
@override
State<HistoryScreen> createState() => _HistoryScreenState();
}
@ -106,6 +116,7 @@ class _HistoryScreenState extends State<HistoryScreen> {
Future<List<Map<String, dynamic>>> _readSyncedPayloads() =>
WorkoutSyncService(
httpClient: widget.httpClient,
firebaseFactory: widget.firebaseFactory,
).readMergedWorkoutPayloads();
Future<void> _pickExercise(String name) async {

View File

@ -23,9 +23,23 @@ Future<void> 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<FirebaseRestClient?> 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.

View File

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