Files
flutter/test/exercises/exercise_provider_db_test.dart
2026-01-30 13:50:35 +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, timeout: anyNamed('timeout'))).thenAnswer(
(_) => Future.value(tExerciseInfoMap),
);
when(mockBaseProvider.makeUrl(exerciseInfoUrl, id: 9)).thenReturn(tExerciseInfoDetailUri);
when(mockBaseProvider.fetch(tExerciseInfoDetailUri, timeout: anyNamed('timeout'))).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, timeout: anyNamed('timeout')));
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, timeout: anyNamed('timeout')));
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');
});
});
}