diet-guard/app/test/services/log_storage_service_test.dart
Krzysztof kuhy Rudnicki 4b1de83e27
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file-length / file length <= 250 lines (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint) (push) Waiting to run
Tests / test (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Pre-log meals for a future date and slot (CLI --date/--hour, app 'Log for later')
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-27 19:59:38 +02:00

204 lines
6.8 KiB
Dart

import 'dart:io';
import 'package:diet_guard_app/models/food_entry.dart';
import 'package:diet_guard_app/models/local_time.dart';
import 'package:diet_guard_app/models/meal_component.dart';
import 'package:diet_guard_app/models/nutrition.dart';
import 'package:diet_guard_app/services/document_store_io.dart';
import 'package:diet_guard_app/services/log_storage_service.dart';
import 'package:flutter_test/flutter_test.dart';
const _manual = Nutrition(
kcal: 150,
proteinG: 5,
carbsG: 20,
fatG: 3,
grams: 50,
source: 'manual',
);
void main() {
late Directory tempDir;
setUp(() async {
tempDir = await Directory.systemTemp.createTemp('diet_guard_test_');
LogStorageService.resetForTesting(store: FileDocumentStore(tempDir));
});
tearDown(() async {
LogStorageService.resetForTesting();
await tempDir.delete(recursive: true);
});
group('readLog', () {
test('returns an empty log when no file exists yet', () async {
expect(await LogStorageService.instance.readLog(), isEmpty);
});
test('returns an empty log for unparsable JSON', () async {
final file = File('${tempDir.path}/food_log.json');
await file.writeAsString('not json');
expect(await LogStorageService.instance.readLog(), isEmpty);
});
test('returns an empty log when the JSON root is not a map', () async {
final file = File('${tempDir.path}/food_log.json');
await file.writeAsString('[]');
expect(await LogStorageService.instance.readLog(), isEmpty);
});
test('skips a date key whose value is not a list', () async {
final file = File('${tempDir.path}/food_log.json');
await file.writeAsString('{"2026-06-22": "not a list"}');
expect(await LogStorageService.instance.readLog(), isEmpty);
});
});
group('logMeal', () {
test('assigns a fresh id and never an hmac', () async {
final entry = await LogStorageService.instance.logMeal(
'toast',
_manual,
slot: 8,
);
expect(entry.id, isNotEmpty);
expect(entry.hmac, isNull);
expect(entry.slot, 8);
expect(entry.desc, 'toast');
});
test('persists components when given', () async {
const components = [
MealComponent(
name: 'bread',
kcal: 100,
proteinG: 3,
carbsG: 18,
fatG: 1,
grams: 40,
),
];
final entry = await LogStorageService.instance.logMeal(
'toast meal',
_manual,
components: components,
);
expect(entry.components, hasLength(1));
final reloaded = await LogStorageService.instance.todayEntries();
expect(reloaded.single.components!.single.name, 'bread');
});
test("two logged meals both persist under today's date key", () async {
await LogStorageService.instance.logMeal('a', _manual);
await LogStorageService.instance.logMeal('b', _manual);
final entries = await LogStorageService.instance.todayEntries();
expect(entries, hasLength(2));
});
test('a future `when` buckets the entry under that future date', () async {
final future = DateTime.now().add(const Duration(days: 1, hours: 1));
final entry = await LogStorageService.instance.logMeal(
'future meal',
_manual,
slot: 12,
when: future,
);
final log = await LogStorageService.instance.readLog();
expect(log[localDateKey(future)]!.single.id, entry.id);
expect(await LogStorageService.instance.todayEntries(), isEmpty);
});
test('rejects a `when` before now', () {
final past = DateTime.now().subtract(const Duration(days: 1));
expect(
() => LogStorageService.instance.logMeal('late', _manual, when: past),
throwsArgumentError,
);
});
});
group('undoLastToday', () {
test('returns null when today has no entries', () async {
expect(await LogStorageService.instance.undoLastToday(), isNull);
});
test('tombstones the most recent entry in place', () async {
await LogStorageService.instance.logMeal('first', _manual);
final second = await LogStorageService.instance.logMeal(
'second',
_manual,
);
final undone = await LogStorageService.instance.undoLastToday();
expect(undone!.id, second.id);
expect(undone.deleted, isTrue);
});
test('tombstoned entries are excluded from todayEntries', () async {
await LogStorageService.instance.logMeal('only', _manual);
await LogStorageService.instance.undoLastToday();
expect(await LogStorageService.instance.todayEntries(), isEmpty);
});
test("never touches a previous day's entries", () async {
final yesterdayKey = localDateKey(
DateTime.now().subtract(const Duration(days: 1)),
);
final yesterday = FoodEntry(
id: 'yesterday-1',
time: '${yesterdayKey}T08:00:00+02:00',
desc: 'yesterday meal',
grams: 50,
kcal: 150,
proteinG: 5,
carbsG: 20,
fatG: 3,
source: 'manual',
);
await LogStorageService.instance.writeLog({
yesterdayKey: [yesterday],
});
await LogStorageService.instance.logMeal('today', _manual);
expect(await LogStorageService.instance.undoLastToday(), isNotNull);
final log = await LogStorageService.instance.readLog();
expect(log[yesterdayKey]!.single.deleted, isFalse);
});
test(
'skips an already-tombstoned entry and undoes the one before it',
() async {
final first = await LogStorageService.instance.logMeal(
'first',
_manual,
);
await LogStorageService.instance.logMeal('second', _manual);
await LogStorageService.instance.undoLastToday();
final undoneAgain = await LogStorageService.instance.undoLastToday();
expect(undoneAgain!.id, first.id);
expect(await LogStorageService.instance.undoLastToday(), isNull);
},
);
});
group('todayTotalKcal', () {
test("sums kcal across today's non-tombstoned entries", () async {
await LogStorageService.instance.logMeal('a', _manual);
await LogStorageService.instance.logMeal('b', _manual);
expect(await LogStorageService.instance.todayTotalKcal(), 300.0);
});
test('excludes a tombstoned entry from the total', () async {
await LogStorageService.instance.logMeal('a', _manual);
await LogStorageService.instance.logMeal('b', _manual);
await LogStorageService.instance.undoLastToday();
expect(await LogStorageService.instance.todayTotalKcal(), 150.0);
});
});
group('loggedSlotsToday', () {
test('returns only the slots with a logged entry', () async {
await LogStorageService.instance.logMeal('a', _manual, slot: 8);
await LogStorageService.instance.logMeal('b', _manual);
expect(await LogStorageService.instance.loggedSlotsToday(), {8});
});
});
}