Files
flutter/test/exercises/exercise_provider_db_test.dart
Roland Geider cbdc4a0c56 Increase page size for languages, no need to use pagination for this
Categories and muscles will probably never get so big, but it doesn't do any
harm doing it as well.
2026-01-12 21:59:52 +01:00

551 lines
19 KiB
Dart

/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'dart:convert';
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
import 'package:wger/database/exercises/exercise_database.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/date.dart';
import 'package:wger/helpers/shared_preferences.dart';
import 'package:wger/models/exercises/exercise_api.dart';
import 'package:wger/models/exercises/muscle.dart';
import 'package:wger/providers/exercises.dart';
import '../../test_data/exercises.dart';
import '../fixtures/fixture_reader.dart';
import '../measurements/measurement_provider_test.mocks.dart';
void main() {
late MockWgerBaseProvider mockBaseProvider;
late ExercisesProvider provider;
late ExerciseDatabase database;
const String categoryUrl = 'exercisecategory';
const String exerciseInfoUrl = 'exerciseinfo';
const String muscleUrl = 'muscle';
const String equipmentUrl = 'equipment';
const String languageUrl = 'language';
final Uri tCategoryEntriesUri = Uri(
scheme: 'http',
host: 'localhost',
path: 'api/v2/$categoryUrl/',
);
final Uri tExerciseInfoUri = Uri(
scheme: 'http',
host: 'localhost',
path: 'api/v2/$exerciseInfoUrl/',
);
final Uri tExerciseInfoDetailUri = Uri(
scheme: 'http',
host: 'localhost',
path: 'api/v2/$exerciseInfoUrl/9/',
);
final Uri tMuscleEntriesUri = Uri(
scheme: 'http',
host: 'localhost',
path: 'api/v2/$muscleUrl/',
);
final Uri tEquipmentEntriesUri = Uri(
scheme: 'http',
host: 'localhost',
path: 'api/v2/$equipmentUrl/',
);
final Uri tLanguageEntriesUri = Uri(
scheme: 'http',
host: 'localhost',
path: 'api/v2/$languageUrl/',
);
const muscle1 = Muscle(id: 1, name: 'Biceps brachii', nameEn: 'Biceps', isFront: true);
const muscle2 = Muscle(id: 2, name: 'Anterior deltoid', nameEn: 'Biceps', isFront: true);
const muscle3 = Muscle(id: 4, name: 'Biceps femoris', nameEn: 'Hamstrings', isFront: false);
final Map<String, dynamic> tCategoryMap = jsonDecode(
fixture('exercises/category_entries.json'),
);
final Map<String, dynamic> tMuscleMap = jsonDecode(
fixture('exercises/muscles_entries.json'),
);
final Map<String, dynamic> tEquipmentMap = jsonDecode(
fixture('exercises/equipment_entries.json'),
);
final Map<String, dynamic> tLanguageMap = jsonDecode(
fixture('exercises/language_entries.json'),
);
final Map<String, dynamic> tExerciseInfoMap = jsonDecode(
fixture('exercises/exerciseinfo_response.json'),
);
setUp(() {
database = ExerciseDatabase.inMemory(NativeDatabase.memory());
mockBaseProvider = MockWgerBaseProvider();
provider = ExercisesProvider(
mockBaseProvider,
database: database,
);
WidgetsFlutterBinding.ensureInitialized();
/// Replacement for SharedPreferences.setMockInitialValues()
SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty();
// Mock categories
when(
mockBaseProvider.makeUrl(categoryUrl, query: anyNamed('query')),
).thenReturn(tCategoryEntriesUri);
when(
mockBaseProvider.fetchPaginated(tCategoryEntriesUri),
).thenAnswer((_) => Future.value(tCategoryMap['results']));
// Mock muscles
when(
mockBaseProvider.makeUrl(muscleUrl, query: anyNamed('query')),
).thenReturn(tMuscleEntriesUri);
when(
mockBaseProvider.fetchPaginated(tMuscleEntriesUri),
).thenAnswer((_) => Future.value(tMuscleMap['results']));
// Mock equipment
when(
mockBaseProvider.makeUrl(equipmentUrl, query: anyNamed('query')),
).thenReturn(tEquipmentEntriesUri);
when(
mockBaseProvider.fetchPaginated(tEquipmentEntriesUri),
).thenAnswer((_) => Future.value(tEquipmentMap['results']));
// Mock languages
when(
mockBaseProvider.makeUrl(languageUrl, query: anyNamed('query')),
).thenReturn(tLanguageEntriesUri);
when(mockBaseProvider.fetchPaginated(tLanguageEntriesUri)).thenAnswer(
(_) => Future.value(tLanguageMap['results']),
);
// Mock base info response
when(mockBaseProvider.makeUrl(exerciseInfoUrl)).thenReturn(tExerciseInfoUri);
when(mockBaseProvider.fetch(tExerciseInfoUri)).thenAnswer(
(_) => Future.value(tExerciseInfoMap),
);
when(mockBaseProvider.makeUrl(exerciseInfoUrl, id: 9)).thenReturn(tExerciseInfoDetailUri);
when(mockBaseProvider.fetch(tExerciseInfoDetailUri)).thenAnswer(
(_) => Future.value(tExerciseInfoMap),
);
});
tearDown(() async {
await database.close();
});
group('Muscles', () {
test('that fetched data from the API is written to the DB', () async {
// Arrange
final prefs = PreferenceHelper.asyncPref;
await provider.initCacheTimesLocalPrefs();
// Act
await provider.fetchAndSetMuscles(database);
// Assert
final updateTime = DateTime.parse((await prefs.getString(PREFS_LAST_UPDATED_MUSCLES))!);
final valid = DateTime.now().add(const Duration(days: ExercisesProvider.EXERCISE_CACHE_DAYS));
expect(updateTime.isSameDayAs(valid), true);
final muscles = await database.select(database.muscles).get();
verify(mockBaseProvider.fetchPaginated(any));
expect(muscles[0].id, 2);
expect(muscles[0].data, muscle2);
expect(muscles[1].id, 1);
expect(muscles[1].data, muscle1);
expect(muscles[2].id, 4);
expect(muscles[2].data, muscle3);
expect(provider.muscles.length, 3);
expect(provider.muscles[0], muscle2);
expect(provider.muscles[1], muscle1);
expect(provider.muscles[2], muscle3);
});
test('that if there is already valid data in the DB, the API is not hit', () async {
// Arrange
final prefs = PreferenceHelper.asyncPref;
await provider.initCacheTimesLocalPrefs();
final valid = DateTime.now().add(const Duration(days: 1));
prefs.setString(PREFS_LAST_UPDATED_MUSCLES, valid.toIso8601String());
await database
.into(database.muscles)
.insert(MusclesCompanion.insert(id: muscle1.id, data: muscle1));
await database
.into(database.muscles)
.insert(MusclesCompanion.insert(id: muscle2.id, data: muscle2));
// Act
await provider.fetchAndSetMuscles(database);
// Assert
final updateTime = DateTime.parse((await prefs.getString(PREFS_LAST_UPDATED_MUSCLES))!);
expect(updateTime.isSameDayAs(valid), true);
expect(provider.muscles.length, 2);
expect(provider.muscles[0], muscle1);
expect(provider.muscles[1], muscle2);
verifyNever(mockBaseProvider.fetchPaginated(any));
});
});
group('Languages', () {
test('that fetched data from the API is written to the DB', () async {
// Arrange
final prefs = PreferenceHelper.asyncPref;
await provider.initCacheTimesLocalPrefs();
// Act
await provider.fetchAndSetLanguages(database);
// Assert
final updateTime = DateTime.parse((await prefs.getString(PREFS_LAST_UPDATED_LANGUAGES))!);
final valid = DateTime.now().add(const Duration(days: ExercisesProvider.EXERCISE_CACHE_DAYS));
expect(updateTime.isSameDayAs(valid), true);
final languages = await database.select(database.languages).get();
verify(mockBaseProvider.fetchPaginated(any));
expect(languages[0].id, tLanguage1.id);
expect(languages[0].data, tLanguage1);
expect(languages[1].id, tLanguage2.id);
expect(languages[1].data, tLanguage2);
expect(languages[2].id, tLanguage4.id);
expect(languages[2].data, tLanguage4);
expect(languages[3].id, tLanguage3.id);
expect(languages[3].data, tLanguage3);
expect(provider.languages.length, 5);
expect(provider.languages[0], tLanguage1);
expect(provider.languages[1], tLanguage2);
expect(provider.languages[2], tLanguage4);
expect(provider.languages[3], tLanguage3);
});
test('that if there is already valid data in the DB, the API is not hit', () async {
// Arrange
final prefs = PreferenceHelper.asyncPref;
await provider.initCacheTimesLocalPrefs();
final valid = DateTime.now().add(const Duration(days: 1));
prefs.setString(PREFS_LAST_UPDATED_LANGUAGES, valid.toIso8601String());
await database
.into(database.languages)
.insert(LanguagesCompanion.insert(id: tLanguage1.id, data: tLanguage1));
await database
.into(database.languages)
.insert(LanguagesCompanion.insert(id: tLanguage2.id, data: tLanguage2));
// Act
await provider.fetchAndSetLanguages(database);
// Assert
final updateTime = DateTime.parse((await prefs.getString(PREFS_LAST_UPDATED_LANGUAGES))!);
expect(updateTime.isSameDayAs(valid), true);
expect(provider.languages.length, 2);
expect(provider.languages[0], tLanguage1);
expect(provider.languages[1], tLanguage2);
verifyNever(mockBaseProvider.fetchPaginated(any));
});
});
group('Categories', () {
test('that fetched data from the API is written to the DB', () async {
// Arrange
final prefs = PreferenceHelper.asyncPref;
await provider.initCacheTimesLocalPrefs();
// Act
await provider.fetchAndSetCategories(database);
// Assert
final updateTime = DateTime.parse((await prefs.getString(PREFS_LAST_UPDATED_CATEGORIES))!);
final valid = DateTime.now().add(const Duration(days: ExercisesProvider.EXERCISE_CACHE_DAYS));
expect(updateTime.isSameDayAs(valid), true);
final categories = await database.select(database.categories).get();
verify(mockBaseProvider.fetchPaginated(any));
expect(categories[0].id, tCategory1.id);
expect(categories[0].data, tCategory1);
expect(categories[1].id, tCategory2.id);
expect(categories[1].data, tCategory2);
expect(provider.categories.length, 2);
expect(provider.categories[0], tCategory1);
expect(provider.categories[1], tCategory2);
});
test('that if there is already valid data in the DB, the API is not hit', () async {
// Arrange
final prefs = PreferenceHelper.asyncPref;
await provider.initCacheTimesLocalPrefs();
final valid = DateTime.now().add(const Duration(days: 1));
prefs.setString(PREFS_LAST_UPDATED_CATEGORIES, valid.toIso8601String());
await database
.into(database.categories)
.insert(CategoriesCompanion.insert(id: tCategory1.id, data: tCategory1));
await database
.into(database.categories)
.insert(CategoriesCompanion.insert(id: tCategory2.id, data: tCategory2));
// Act
await provider.fetchAndSetCategories(database);
// Assert
final updateTime = DateTime.parse((await prefs.getString(PREFS_LAST_UPDATED_CATEGORIES))!);
expect(updateTime.isSameDayAs(valid), true);
expect(provider.categories.length, 2);
expect(provider.categories[0], tCategory1);
expect(provider.categories[1], tCategory2);
verifyNever(mockBaseProvider.fetchPaginated(any));
});
});
group('Equipment', () {
test('that fetched data from the API is written to the DB', () async {
// Arrange
final prefs = PreferenceHelper.asyncPref;
await provider.initCacheTimesLocalPrefs();
// Act
await provider.fetchAndSetEquipments(database);
// Assert
final updateTime = DateTime.parse((await prefs.getString(PREFS_LAST_UPDATED_EQUIPMENT))!);
final valid = DateTime.now().add(const Duration(days: ExercisesProvider.EXERCISE_CACHE_DAYS));
expect(updateTime.isSameDayAs(valid), true);
final equipmentList = await database.select(database.equipments).get();
verify(mockBaseProvider.fetchPaginated(any));
expect(equipmentList[0].id, tEquipment1.id);
expect(equipmentList[0].data, tEquipment1);
expect(equipmentList[1].id, tEquipment2.id);
expect(equipmentList[1].data, tEquipment2);
expect(provider.equipment.length, 4);
expect(provider.equipment[0], tEquipment1);
expect(provider.equipment[1], tEquipment2);
expect(provider.equipment[2], tEquipment3);
expect(provider.equipment[3], tEquipment4);
});
test('that if there is already valid data in the DB, the API is not hit', () async {
// Arrange
final prefs = PreferenceHelper.asyncPref;
await provider.initCacheTimesLocalPrefs();
final valid = DateTime.now().add(const Duration(days: 1));
prefs.setString(PREFS_LAST_UPDATED_EQUIPMENT, valid.toIso8601String());
await database
.into(database.equipments)
.insert(EquipmentsCompanion.insert(id: tEquipment1.id, data: tEquipment1));
await database
.into(database.equipments)
.insert(EquipmentsCompanion.insert(id: tCategory2.id, data: tEquipment2));
// Act
await provider.fetchAndSetEquipments(database);
// Assert
final updateTime = DateTime.parse((await prefs.getString(PREFS_LAST_UPDATED_EQUIPMENT))!);
expect(updateTime.isSameDayAs(valid), true);
expect(provider.equipment.length, 2);
expect(provider.equipment[0], tEquipment1);
expect(provider.equipment[1], tEquipment2);
verifyNever(mockBaseProvider.fetchPaginated(any));
});
});
group('Exercise cache DB', () {
test('that if there is already valid data in the DB, the API is not hit', () async {
// Arrange
final prefs = PreferenceHelper.asyncPref;
await provider.initCacheTimesLocalPrefs();
final valid = DateTime.now().add(const Duration(days: 1));
prefs.setString(PREFS_LAST_UPDATED_LANGUAGES, valid.toIso8601String());
await database
.into(database.exercises)
.insert(
ExercisesCompanion.insert(
id: tExerciseInfoMap['id'],
data: json.encode(tExerciseInfoMap),
lastUpdate: DateTime.parse(tExerciseInfoMap['last_update_global']),
lastFetched: DateTime.now(),
),
);
await database
.into(database.languages)
.insert(LanguagesCompanion.insert(id: tLanguage1.id, data: tLanguage1));
await database
.into(database.languages)
.insert(LanguagesCompanion.insert(id: tLanguage2.id, data: tLanguage2));
await database
.into(database.languages)
.insert(LanguagesCompanion.insert(id: tLanguage3.id, data: tLanguage3));
// Act
await provider.fetchAndSetLanguages(database);
await provider.setExercisesFromDatabase(database);
// Assert
expect(provider.exercises.length, 1);
expect(provider.exercises.first.id, 9);
expect(provider.exercises.first.uuid, '1b020b3a-3732-4c7e-92fd-a0cec90ed69b');
verifyNever(mockBaseProvider.fetchPaginated(any));
});
test('fetching a known exercise - no API refresh', () async {
// Arrange
provider.languages = testLanguages;
await database
.into(database.exercises)
.insert(
ExercisesCompanion.insert(
id: tExerciseInfoMap['id'],
data: json.encode(tExerciseInfoMap),
lastUpdate: DateTime.parse(tExerciseInfoMap['last_update_global']),
lastFetched: DateTime.now().subtract(const Duration(hours: 1)),
),
);
// Assert
expect(provider.exercises.length, 0);
// Act
await provider.handleUpdateExerciseFromApi(database, 9);
// Assert
expect(provider.exercises.length, 1);
expect(provider.exercises.first.id, 9);
expect(provider.exercises.first.uuid, '1b020b3a-3732-4c7e-92fd-a0cec90ed69b');
verifyNever(mockBaseProvider.fetch(any));
});
test('fetching a known exercise - needed API refresh - no new data', () async {
// Arrange
provider.languages = testLanguages;
await database
.into(database.exercises)
.insert(
ExercisesCompanion.insert(
id: tExerciseInfoMap['id'],
data: json.encode(tExerciseInfoMap),
lastUpdate: DateTime.parse(tExerciseInfoMap['last_update_global']),
lastFetched: DateTime.now().subtract(const Duration(days: 10)),
),
);
// Assert
expect(provider.exercises.length, 0);
// Act
await provider.handleUpdateExerciseFromApi(database, 9);
final exerciseDb = await (database.select(
database.exercises,
)..where((e) => e.id.equals(9))).getSingleOrNull();
// Assert
verify(mockBaseProvider.fetch(any));
expect(provider.exercises.length, 1);
expect(provider.exercises.first.id, 9);
expect(provider.exercises.first.uuid, '1b020b3a-3732-4c7e-92fd-a0cec90ed69b');
expect(exerciseDb!.lastFetched.isSameDayAs(DateTime.now()), true);
});
test('fetching a known exercise - needed API refresh - new data from API', () async {
// Arrange
provider.languages = testLanguages;
final newData = Map.from(tExerciseInfoMap);
newData['uuid'] = 'bf6d5557-1c49-48fd-922e-75d11f81d4eb';
await database
.into(database.exercises)
.insert(
ExercisesCompanion.insert(
id: newData['id'],
data: json.encode(newData),
lastUpdate: DateTime(2023, 1, 1),
lastFetched: DateTime.now().subtract(const Duration(days: 10)),
),
);
// Assert
expect(provider.exercises.length, 0);
// Act
await provider.handleUpdateExerciseFromApi(database, 9);
final exerciseDb = await (database.select(
database.exercises,
)..where((e) => e.id.equals(9))).getSingleOrNull();
final exerciseData = ExerciseApiData.fromString(exerciseDb!.data);
// Assert
verify(mockBaseProvider.fetch(any));
expect(provider.exercises.length, 1);
expect(provider.exercises.first.id, 9);
expect(provider.exercises.first.uuid, '1b020b3a-3732-4c7e-92fd-a0cec90ed69b');
expect(exerciseDb.lastFetched.isSameDayAs(DateTime.now()), true);
expect(exerciseData.uuid, '1b020b3a-3732-4c7e-92fd-a0cec90ed69b');
});
});
}