diet-guard/app/test/services/app_settings_service_test.dart
Krzysztof kuhy Rudnicki c0ed6154ac Split the remaining seven Dart test files
Eleventh batch of the 250-line sweep (over-cap count 12 -> 5). Every test file
in the repo, Python and Dart, is now under cap; the five left are all in
app/lib/.

  wrapper_server_test.dart        349 -> 145  (+ proxy, account siblings)
  app_settings_service_test.dart  321 -> 162  (+ sync sibling)
  average_service_test.dart       299 -> 158  (+ periods sibling)
  settings_screen_test.dart       274 -> 189  (+ battery sibling)
  day_status_service_test.dart    270 -> 161  (+ streaks sibling)
  edit_entry_screen_test.dart     269 -> 159  (+ foodbank sibling)
  day_status_calendar_test.dart   256 -> 237  (+ _test_support sibling)

day_status_calendar_test is a single group whose tests close over group-local
state, so it could not be split by block. Its ~25 lines of file-level fixtures
moved to `day_status_calendar_test_support.dart` instead, which required the
same publicisation as the sync support file (`_theme` -> `appTheme`, `_wrap` ->
`wrapInApp`, `_colorOfDay` -> `colorOfDay`) with doc comments.

Block counts were compared against HEAD per family rather than trusting the
suite total, since the duplication bug in the previous batch passed green at an
inflated count. All seven families match exactly (29/26/34/11/34/8/14).

Verified: flutter test 555 (unchanged), flutter analyze clean, pre-commit
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
2026-08-15 20:53:55 +02:00

163 lines
6.3 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:diet_guard_app/services/document_store_io.dart';
import 'package:diet_guard_app/services/app_settings_service.dart';
import 'package:diet_guard_app/services/budget_history_service.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
late Directory tempDir;
setUp(() async {
tempDir = await Directory.systemTemp.createTemp(
'diet_guard_settings_test_',
);
AppSettingsService.resetForTesting();
BudgetHistoryService.resetForTesting();
});
tearDown(() async {
AppSettingsService.resetForTesting();
BudgetHistoryService.resetForTesting();
await tempDir.delete(recursive: true);
});
group('dailyKcalGoal static getter', () {
test('returns 2200 when singleton is uninitialised', () {
// Singleton is null after resetForTesting() — exercises the ?? 2200 branch.
expect(AppSettingsService.dailyKcalGoal, 2200);
});
});
group('resetForTesting', () {
test('with testDir creates a working instance', () {
AppSettingsService.resetForTesting(store: FileDocumentStore(tempDir));
BudgetHistoryService.resetForTesting(
store: FileDocumentStore(tempDir),
);
expect(AppSettingsService.instance, isNotNull);
});
test('without testDir nulls the singleton', () {
AppSettingsService.resetForTesting(store: FileDocumentStore(tempDir));
BudgetHistoryService.resetForTesting(
store: FileDocumentStore(tempDir),
);
AppSettingsService.resetForTesting();
BudgetHistoryService.resetForTesting();
// instance getter throws when null — verify via dailyKcalGoal fallback.
expect(AppSettingsService.dailyKcalGoal, 2200);
});
});
group('init early-return', () {
test('returns existing instance without re-initialising', () async {
AppSettingsService.resetForTesting(store: FileDocumentStore(tempDir));
BudgetHistoryService.resetForTesting(
store: FileDocumentStore(tempDir),
);
final first = AppSettingsService.instance;
// init() sees _instance != null and returns early (no platform channel).
final second = await AppSettingsService.init();
expect(identical(first, second), isTrue);
});
});
group('saveDailyKcalGoal', () {
test('updates in-memory value and persists to file', () async {
AppSettingsService.resetForTesting(store: FileDocumentStore(tempDir));
BudgetHistoryService.resetForTesting(
store: FileDocumentStore(tempDir),
);
await AppSettingsService.instance.saveDailyKcalGoal(1800);
expect(AppSettingsService.dailyKcalGoal, 1800);
final raw = await File(
'${tempDir.path}/app_settings.json',
).readAsString();
final data = jsonDecode(raw) as Map;
expect(data['daily_kcal_goal'], 1800);
});
test('grandfathers the previous value to every past day', () async {
// The load-bearing ordering test, on the real upgrade path: a device
// that already had a goal (with an updatedAt, i.e. genuinely set)
// gains a history the first time it loads, and a later edit must not
// drag past days along with it. Asserts a PAST day -- a today-only
// assertion passes even when the seed happens after the upsert.
await File('${tempDir.path}/app_settings.json').writeAsString(
jsonEncode({
'daily_kcal_goal': 2200,
'daily_kcal_goal_updated_at': '2026-07-13T21:15:09.000',
}),
);
BudgetHistoryService.resetForTesting(store: FileDocumentStore(tempDir));
await AppSettingsService.initForTesting(FileDocumentStore(tempDir));
await AppSettingsService.instance.saveDailyKcalGoal(2000);
expect(BudgetHistoryService.schedule.forDay('2020-01-01'), 2200);
expect(BudgetHistoryService.schedule.current, 2000);
expect(AppSettingsService.dailyKcalGoal, 2000);
});
test('loading an already-set goal seeds the history on its own', () async {
// Seeding must not depend on the user making an edit: a device that
// merely *synced* a goal still has to classify its history against
// that goal, matching _budget.current_schedule's seed-on-read.
await File('${tempDir.path}/app_settings.json').writeAsString(
jsonEncode({
'daily_kcal_goal': 1800,
'daily_kcal_goal_updated_at': '2026-07-13T21:15:09.000',
}),
);
BudgetHistoryService.resetForTesting(store: FileDocumentStore(tempDir));
await AppSettingsService.initForTesting(FileDocumentStore(tempDir));
expect(BudgetHistoryService.schedule.forDay('2020-01-01'), 1800);
});
test('a never-set goal seeds no phantom history', () async {
AppSettingsService.resetForTesting(store: FileDocumentStore(tempDir));
BudgetHistoryService.resetForTesting(store: FileDocumentStore(tempDir));
await AppSettingsService.instance.saveDailyKcalGoal(2000);
// Nothing to grandfather, so the only entry is today's; past days fall
// back to the current goal, exactly as _budget.current_schedule does.
expect(BudgetHistoryService.schedule.entries, hasLength(1));
expect(BudgetHistoryService.schedule.forDay('2020-01-01'), 2000);
});
test('a merge write-back records no history change', () async {
AppSettingsService.resetForTesting(store: FileDocumentStore(tempDir));
BudgetHistoryService.resetForTesting(store: FileDocumentStore(tempDir));
await AppSettingsService.instance.applySyncedBudget(
1700,
updatedAt: DateTime(2026, 7, 26),
);
// The sync layer applies merged history entries itself; a write-back
// must not manufacture a local edit on top of them.
expect(BudgetHistoryService.schedule.entries, isEmpty);
});
test('stamps a fresh dailyKcalGoalUpdatedAt', () async {
AppSettingsService.resetForTesting(store: FileDocumentStore(tempDir));
BudgetHistoryService.resetForTesting(
store: FileDocumentStore(tempDir),
);
final before = DateTime.now();
await AppSettingsService.instance.saveDailyKcalGoal(1800);
final after = DateTime.now();
final updatedAt = AppSettingsService.dailyKcalGoalUpdatedAt;
expect(updatedAt, isNotNull);
expect(updatedAt!.isBefore(before), isFalse);
expect(updatedAt.isAfter(after), isFalse);
});
});
}