glp-companion/test/features/nutrition/data/nutrition_repository_test.dart

162 lines
5.4 KiB
Dart

import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:glp1_companion/core/database/app_database.dart';
import 'package:glp1_companion/features/nutrition/data/repositories/nutrition_repository.dart';
import 'package:glp1_companion/features/nutrition/domain/entities/food_log_entry.dart';
import 'package:glp1_companion/features/nutrition/domain/entities/hydration_log_entry.dart';
import 'package:glp1_companion/features/nutrition/domain/entities/meal_type.dart';
void main() {
late AppDatabase database;
late NutritionRepository repository;
final morning = DateTime(2026, 10, 16, 8);
final noon = DateTime(2026, 10, 16, 12);
final previousDay = DateTime(2026, 10, 15, 9);
setUp(() {
database = AppDatabase.forTesting(NativeDatabase.memory());
repository = NutritionRepository(database);
});
tearDown(() => database.close());
FoodLogEntry makeFood({
int? id,
DateTime? timestamp,
MealType mealType = MealType.breakfast,
String description = 'Greek yogurt',
double proteinGrams = 20,
int calories = 150,
double? carbsGrams,
double? fatGrams,
}) =>
FoodLogEntry(
id: id,
timestamp: timestamp ?? morning,
mealType: mealType,
description: description,
proteinGrams: proteinGrams,
calories: calories,
carbsGrams: carbsGrams,
fatGrams: fatGrams,
);
group('NutritionRepository food diary', () {
test('saveFoodLog assigns an id and persists all fields', () async {
final saved = await repository.saveFoodLog(
makeFood(
mealType: MealType.lunch,
description: 'Chicken breast bowl',
proteinGrams: 42.5,
calories: 520,
carbsGrams: 45,
fatGrams: 12,
),
);
expect(saved.id, isNotNull);
final fetched = await repository.getFoodLogs();
expect(fetched, hasLength(1));
expect(fetched.single.id, saved.id);
expect(fetched.single.mealType, MealType.lunch);
expect(fetched.single.description, 'Chicken breast bowl');
expect(fetched.single.proteinGrams, 42.5);
expect(fetched.single.calories, 520);
expect(fetched.single.carbsGrams, 45);
expect(fetched.single.fatGrams, 12);
});
test('getFoodLogs returns entries newest first', () async {
await repository.saveFoodLog(makeFood(timestamp: previousDay));
await repository.saveFoodLog(makeFood(timestamp: noon));
final fetched = await repository.getFoodLogs();
expect(fetched, hasLength(2));
expect(fetched.first.timestamp, noon);
expect(fetched.last.timestamp, previousDay);
});
test('getFoodLogs filters by the half-open [from, to) range', () async {
await repository.saveFoodLog(makeFood(timestamp: previousDay));
await repository.saveFoodLog(makeFood(timestamp: morning));
await repository.saveFoodLog(makeFood(timestamp: noon));
final dayStart = DateTime(2026, 10, 16);
final dayEnd = DateTime(2026, 10, 17);
final todays = await repository.getFoodLogs(from: dayStart, to: dayEnd);
expect(todays, hasLength(2));
expect(
todays.every((entry) => entry.timestamp.isAfter(previousDay)),
isTrue,
);
});
test('deleteFoodLog removes only the targeted row', () async {
final first = await repository.saveFoodLog(makeFood());
await repository.saveFoodLog(
makeFood(mealType: MealType.dinner, description: 'Salmon'),
);
await repository.deleteFoodLog(first.id!);
final remaining = await repository.getFoodLogs();
expect(remaining, hasLength(1));
expect(remaining.single.description, 'Salmon');
});
test('watchFoodLogs emits on every write', () async {
final events = <List<FoodLogEntry>>[];
final subscription = repository.watchFoodLogs().listen(events.add);
await repository.saveFoodLog(makeFood());
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(events, isNotEmpty);
expect(events.last, hasLength(1));
await subscription.cancel();
});
});
group('NutritionRepository hydration', () {
test('hydration entries persist and filter by day', () async {
await repository.saveHydrationLog(
HydrationLogEntry(timestamp: morning, volumeOz: 8),
);
await repository.saveHydrationLog(
HydrationLogEntry(timestamp: noon, volumeOz: 16),
);
await repository.saveHydrationLog(
HydrationLogEntry(timestamp: previousDay, volumeOz: 24),
);
final dayStart = DateTime(2026, 10, 16);
final dayEnd = DateTime(2026, 10, 17);
final todays =
await repository.getHydrationLogs(from: dayStart, to: dayEnd);
expect(todays, hasLength(2));
final totalOz = todays.fold<double>(0, (sum, e) => sum + e.volumeOz);
expect(totalOz, 24);
await repository.deleteHydrationLog(todays.first.id!);
final afterDelete = await repository.getHydrationLogs(
from: dayStart,
to: dayEnd,
);
expect(afterDelete, hasLength(1));
});
});
group('MealType persistence keys', () {
test('tryParse round-trips every value', () {
for (final type in MealType.values) {
expect(MealType.tryParse(type.persistenceKey), type);
}
expect(MealType.tryParse('brunch'), isNull);
expect(MealType.tryParse(null), isNull);
});
});
}