feat(sync): préparation à la synchronisation cloud (ticket #12)
Ajuste les ports et le bootstrap applicatif, ainsi que le mapping Drift des repositories, pour préparer une future synchronisation cloud (invariants documentés dans docs/sync-invariants.md). Corrige au passage un bug de fuseau horaire UTC dans le mapping Drift. flutter analyze propre, 35/35 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:gametime/application/application.dart';
|
||||
@ -8,12 +9,16 @@ import 'package:gametime/infrastructure/local/local.dart' as local;
|
||||
|
||||
void main() {
|
||||
late local.AppDatabase database;
|
||||
late local.DriftExerciseRepository exerciseRepository;
|
||||
late local.DriftProgramRepository programRepository;
|
||||
late local.DriftActiveSessionRepository activeRepository;
|
||||
late local.DriftWorkoutTemplateRepository templateRepository;
|
||||
late local.DriftWorkoutHistoryRepository historyRepository;
|
||||
|
||||
setUp(() {
|
||||
database = local.AppDatabase(NativeDatabase.memory());
|
||||
exerciseRepository = local.DriftExerciseRepository(database);
|
||||
programRepository = local.DriftProgramRepository(database);
|
||||
activeRepository = local.DriftActiveSessionRepository(database);
|
||||
templateRepository = local.DriftWorkoutTemplateRepository(database);
|
||||
historyRepository = local.DriftWorkoutHistoryRepository(database);
|
||||
@ -23,6 +28,102 @@ void main() {
|
||||
await database.close();
|
||||
});
|
||||
|
||||
test('exercise mutations are written to change log', () async {
|
||||
final now = DateTime.utc(2026, 7, 17, 12);
|
||||
final exercise = Exercise(
|
||||
metadata: _metadata('exercise-1', now),
|
||||
name: 'Squat',
|
||||
hasTimeMeasure: false,
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: false,
|
||||
);
|
||||
|
||||
await exerciseRepository.save(exercise);
|
||||
await exerciseRepository.save(
|
||||
exercise.copyWith(
|
||||
metadata: _metadata(
|
||||
'exercise-1',
|
||||
now.add(const Duration(seconds: 1)),
|
||||
1,
|
||||
),
|
||||
name: 'Front squat',
|
||||
),
|
||||
);
|
||||
await exerciseRepository.save(
|
||||
exercise
|
||||
.copyWith(
|
||||
metadata: _metadata(
|
||||
'exercise-1',
|
||||
now.add(const Duration(seconds: 2)),
|
||||
2,
|
||||
),
|
||||
name: 'Front squat',
|
||||
)
|
||||
.archive(now.add(const Duration(seconds: 2))),
|
||||
);
|
||||
|
||||
final changes =
|
||||
await (database.select(database.changeLogEntries)
|
||||
..where((table) => table.entityId.equals('exercise-1'))
|
||||
..orderBy([
|
||||
(table) => drift.OrderingTerm.asc(table.localRevision),
|
||||
]))
|
||||
.get();
|
||||
|
||||
expect(changes.map((change) => change.operation), [
|
||||
'insert',
|
||||
'update',
|
||||
'update',
|
||||
]);
|
||||
expect(changes.every((change) => change.entityType == 'Exercise'), isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'removing a program exercise writes a soft delete change log entry',
|
||||
() async {
|
||||
final now = DateTime.utc(2026, 7, 17, 12);
|
||||
final first = _programExercise('program-exercise-1', now, position: 0);
|
||||
final second = _programExercise('program-exercise-2', now, position: 1);
|
||||
await programRepository.save(
|
||||
Program(
|
||||
metadata: _metadata('program-1', now),
|
||||
name: 'Jambes',
|
||||
defaultRestSeconds: 60,
|
||||
exercises: [first, second],
|
||||
),
|
||||
);
|
||||
|
||||
final deletedAt = now.add(const Duration(minutes: 1));
|
||||
await programRepository.replaceExercises(
|
||||
Program(
|
||||
metadata: _metadata('program-1', deletedAt, 1),
|
||||
name: 'Jambes',
|
||||
defaultRestSeconds: 60,
|
||||
exercises: [first],
|
||||
),
|
||||
deletedAt,
|
||||
);
|
||||
|
||||
final deletedRow = await (database.select(
|
||||
database.programExercises,
|
||||
)..where((table) => table.id.equals('program-exercise-2'))).getSingle();
|
||||
final change =
|
||||
await (database.select(database.changeLogEntries)..where(
|
||||
(table) =>
|
||||
table.entityType.equals('ProgramExercise') &
|
||||
table.entityId.equals('program-exercise-2') &
|
||||
table.operation.equals('softDelete'),
|
||||
))
|
||||
.getSingleOrNull();
|
||||
|
||||
expect(deletedRow.deletedAt?.toUtc(), deletedAt);
|
||||
expect(deletedRow.syncState, 'deleted');
|
||||
expect(deletedRow.localRevision, 1);
|
||||
expect(change, isNotNull);
|
||||
expect(change!.localRevision, 1);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'running session elapsed time survives repository reconstruction',
|
||||
() async {
|
||||
@ -167,12 +268,33 @@ String _resolvedSnapshot() {
|
||||
});
|
||||
}
|
||||
|
||||
EntityMetadata _metadata(String id, DateTime now) {
|
||||
EntityMetadata _metadata(String id, DateTime now, [int localRevision = 0]) {
|
||||
return EntityMetadata(
|
||||
id: id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
originDeviceId: 'device-1',
|
||||
localRevision: localRevision,
|
||||
);
|
||||
}
|
||||
|
||||
ProgramExercise _programExercise(
|
||||
String id,
|
||||
DateTime now, {
|
||||
required int position,
|
||||
}) {
|
||||
return ProgramExercise(
|
||||
metadata: _metadata(id, now),
|
||||
programId: 'program-1',
|
||||
position: position,
|
||||
exerciseNameSnapshot: 'Exercise $position',
|
||||
availableTimeSnapshot: false,
|
||||
availableRepsSnapshot: true,
|
||||
availableScoreSnapshot: false,
|
||||
setsCount: 1,
|
||||
timeEnabled: false,
|
||||
repsEnabled: true,
|
||||
scoreEnabled: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user