test(server): ajoute tests fonctionnels sync et fixtures versionnees (#187)
Fixe .gitignore pour exclure .build-home/ et .pub-cache-local/ des environnements locaux de build (bruit non versionne). Documente le checklist d'integration serveur et met a jour le README en consequence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
22
server/test/fixtures/README.md
vendored
Normal file
22
server/test/fixtures/README.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
# Versioned sync fixtures
|
||||
|
||||
These JSON files are server contract fixtures. They intentionally model client
|
||||
payloads as opaque snapshots: server sync tests must push them, pull them back
|
||||
and compare the payloads strictly without teaching the server the client schema.
|
||||
|
||||
Structure:
|
||||
|
||||
- `exercises/v1/`: first captured exercise fixture batch.
|
||||
- `exercises/v2/`: second captured exercise fixture batch used to prove tests
|
||||
can run multiple fixture generations side by side.
|
||||
- `programs/v1/`, `workout_templates/v1/`, `workout_histories/v1/`: reusable
|
||||
minimal sync batches for non-exercise resources.
|
||||
|
||||
Folder names such as `v1` and `v2` identify fixture batches, not the payload
|
||||
schema itself. The authoritative client schema value remains the JSON
|
||||
`schemaVersion` field inside each fixture, and tests assert that the server
|
||||
preserves that value exactly.
|
||||
|
||||
When the client schema changes, add a new fixture batch folder instead of
|
||||
rewriting older fixtures. Old fixture versions are kept to verify backward and
|
||||
forward compatibility.
|
||||
51
server/test/fixtures/exercises/v1/full_combo.json
vendored
Normal file
51
server/test/fixtures/exercises/v1/full_combo.json
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"schemaVersion": 5,
|
||||
"id": "exercise-full-combo",
|
||||
"name": "Intervals complex",
|
||||
"type": "mixed",
|
||||
"category": "full_body",
|
||||
"media": {
|
||||
"coverAssetId": "media-cover-1",
|
||||
"videoAssetId": "media-video-1"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"id": "step-row",
|
||||
"order": 0,
|
||||
"title": "Row",
|
||||
"body": "Complete calories before moving on.",
|
||||
"defaultDurationSeconds": 60
|
||||
},
|
||||
{
|
||||
"id": "step-thruster",
|
||||
"order": 1,
|
||||
"title": "Thruster",
|
||||
"body": "Break sets only if form degrades.",
|
||||
"defaultReps": 15
|
||||
}
|
||||
],
|
||||
"score": {
|
||||
"mode": "for_time",
|
||||
"unit": "seconds",
|
||||
"capSeconds": 900
|
||||
},
|
||||
"chrono": {
|
||||
"mode": "elapsed",
|
||||
"autoStart": true
|
||||
},
|
||||
"timers": {
|
||||
"preparationSeconds": 20,
|
||||
"workSeconds": 180,
|
||||
"restSeconds": 60
|
||||
},
|
||||
"chainOverrides": {
|
||||
"nextExerciseId": "exercise-cooldown",
|
||||
"inheritRest": false
|
||||
},
|
||||
"legacy": {
|
||||
"unknownClientField": {
|
||||
"nested": ["preserve", 1, true, null]
|
||||
}
|
||||
},
|
||||
"updatedAt": "2026-07-19T10:20:00.000Z"
|
||||
}
|
||||
8
server/test/fixtures/exercises/v1/minimal.json
vendored
Normal file
8
server/test/fixtures/exercises/v1/minimal.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "exercise-minimal",
|
||||
"name": "Air squat",
|
||||
"type": "strength",
|
||||
"category": "legs",
|
||||
"updatedAt": "2026-07-19T10:00:00.000Z"
|
||||
}
|
||||
17
server/test/fixtures/exercises/v1/with_score_chrono.json
vendored
Normal file
17
server/test/fixtures/exercises/v1/with_score_chrono.json
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": 3,
|
||||
"id": "exercise-score-chrono",
|
||||
"name": "Shuttle run",
|
||||
"type": "conditioning",
|
||||
"score": {
|
||||
"mode": "rounds_reps",
|
||||
"unit": "reps",
|
||||
"target": 120
|
||||
},
|
||||
"chrono": {
|
||||
"mode": "countdown",
|
||||
"durationSeconds": 600,
|
||||
"warningSeconds": [60, 10]
|
||||
},
|
||||
"updatedAt": "2026-07-19T10:10:00.000Z"
|
||||
}
|
||||
21
server/test/fixtures/exercises/v1/with_steps.json
vendored
Normal file
21
server/test/fixtures/exercises/v1/with_steps.json
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"id": "exercise-with-steps",
|
||||
"name": "Kettlebell swing",
|
||||
"type": "strength",
|
||||
"steps": [
|
||||
{
|
||||
"id": "step-setup",
|
||||
"order": 0,
|
||||
"title": "Setup",
|
||||
"body": "Hinge with the bell slightly in front of the feet."
|
||||
},
|
||||
{
|
||||
"id": "step-drive",
|
||||
"order": 1,
|
||||
"title": "Drive",
|
||||
"body": "Extend the hips and let the bell float to chest height."
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-07-19T10:05:00.000Z"
|
||||
}
|
||||
18
server/test/fixtures/exercises/v1/with_timers.json
vendored
Normal file
18
server/test/fixtures/exercises/v1/with_timers.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 4,
|
||||
"id": "exercise-with-timers",
|
||||
"name": "Tempo bench press",
|
||||
"type": "strength",
|
||||
"timers": {
|
||||
"preparationSeconds": 15,
|
||||
"workSeconds": 45,
|
||||
"restSeconds": 90,
|
||||
"transitionSeconds": 10
|
||||
},
|
||||
"defaultTargets": {
|
||||
"sets": 5,
|
||||
"reps": 5,
|
||||
"weightKg": 80
|
||||
},
|
||||
"updatedAt": "2026-07-19T10:15:00.000Z"
|
||||
}
|
||||
16
server/test/fixtures/exercises/v2/minimal.json
vendored
Normal file
16
server/test/fixtures/exercises/v2/minimal.json
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"schemaVersion": 6,
|
||||
"id": "exercise-minimal-v2",
|
||||
"name": "Air squat",
|
||||
"type": "strength",
|
||||
"category": "legs",
|
||||
"defaultTargets": {
|
||||
"sets": 3,
|
||||
"reps": 12
|
||||
},
|
||||
"clientFormat": {
|
||||
"versionFolder": "v2",
|
||||
"migratedFrom": "exercises/v1/minimal"
|
||||
},
|
||||
"updatedAt": "2026-07-20T10:00:00.000Z"
|
||||
}
|
||||
7
server/test/fixtures/programs/v1/minimal.json
vendored
Normal file
7
server/test/fixtures/programs/v1/minimal.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "program-minimal",
|
||||
"name": "Starter strength",
|
||||
"exerciseIds": ["exercise-minimal"],
|
||||
"updatedAt": "2026-07-19T10:30:00.000Z"
|
||||
}
|
||||
101
server/test/fixtures/sync_fixtures.dart
vendored
Normal file
101
server/test/fixtures/sync_fixtures.dart
vendored
Normal file
@ -0,0 +1,101 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
final class VersionedSyncFixture {
|
||||
const VersionedSyncFixture({
|
||||
required this.resourceType,
|
||||
required this.name,
|
||||
required this.schemaVersion,
|
||||
required this.payload,
|
||||
});
|
||||
|
||||
final String resourceType;
|
||||
final String name;
|
||||
final int schemaVersion;
|
||||
final Map<String, Object?> payload;
|
||||
}
|
||||
|
||||
VersionedSyncFixture loadExerciseFixture(String name, {String version = 'v1'}) {
|
||||
return _loadFixture(
|
||||
resourceType: 'exercise',
|
||||
path: 'test/fixtures/exercises/$version/$name.json',
|
||||
name: name,
|
||||
);
|
||||
}
|
||||
|
||||
VersionedSyncFixture loadProgramFixture(String name, {String version = 'v1'}) {
|
||||
return _loadFixture(
|
||||
resourceType: 'program',
|
||||
path: 'test/fixtures/programs/$version/$name.json',
|
||||
name: name,
|
||||
);
|
||||
}
|
||||
|
||||
VersionedSyncFixture loadWorkoutTemplateFixture(
|
||||
String name, {
|
||||
String version = 'v1',
|
||||
}) {
|
||||
return _loadFixture(
|
||||
resourceType: 'workoutTemplate',
|
||||
path: 'test/fixtures/workout_templates/$version/$name.json',
|
||||
name: name,
|
||||
);
|
||||
}
|
||||
|
||||
VersionedSyncFixture loadWorkoutHistoryFixture(
|
||||
String name, {
|
||||
String version = 'v1',
|
||||
}) {
|
||||
return _loadFixture(
|
||||
resourceType: 'workoutHistory',
|
||||
path: 'test/fixtures/workout_histories/$version/$name.json',
|
||||
name: name,
|
||||
);
|
||||
}
|
||||
|
||||
List<VersionedSyncFixture> loadExerciseFixtures({
|
||||
String version = 'v1',
|
||||
List<String> names = const [
|
||||
'minimal',
|
||||
'with_steps',
|
||||
'with_score_chrono',
|
||||
'with_timers',
|
||||
'full_combo',
|
||||
],
|
||||
}) {
|
||||
return [
|
||||
for (final name in names) loadExerciseFixture(name, version: version),
|
||||
];
|
||||
}
|
||||
|
||||
List<VersionedSyncFixture> loadMinimalResourceFixtures({
|
||||
String version = 'v1',
|
||||
}) {
|
||||
return [
|
||||
loadProgramFixture('minimal', version: version),
|
||||
loadWorkoutTemplateFixture('minimal', version: version),
|
||||
loadWorkoutHistoryFixture('minimal', version: version),
|
||||
];
|
||||
}
|
||||
|
||||
VersionedSyncFixture _loadFixture({
|
||||
required String resourceType,
|
||||
required String path,
|
||||
required String name,
|
||||
}) {
|
||||
final raw = File(path).readAsStringSync();
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map<String, Object?>) {
|
||||
throw StateError('Fixture $path must contain a JSON object.');
|
||||
}
|
||||
final schemaVersion = decoded['schemaVersion'];
|
||||
if (schemaVersion is! int || schemaVersion <= 0) {
|
||||
throw StateError('Fixture $path must contain a positive schemaVersion.');
|
||||
}
|
||||
return VersionedSyncFixture(
|
||||
resourceType: resourceType,
|
||||
name: name,
|
||||
schemaVersion: schemaVersion,
|
||||
payload: decoded,
|
||||
);
|
||||
}
|
||||
19
server/test/fixtures/workout_histories/v1/minimal.json
vendored
Normal file
19
server/test/fixtures/workout_histories/v1/minimal.json
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "history-minimal",
|
||||
"templateId": "template-minimal",
|
||||
"startedAt": "2026-07-19T11:00:00.000Z",
|
||||
"finishedAt": "2026-07-19T11:45:00.000Z",
|
||||
"entries": [
|
||||
{
|
||||
"exerciseId": "exercise-minimal",
|
||||
"sets": [
|
||||
{
|
||||
"reps": 8,
|
||||
"weightKg": 60
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-07-19T11:45:00.000Z"
|
||||
}
|
||||
13
server/test/fixtures/workout_templates/v1/minimal.json
vendored
Normal file
13
server/test/fixtures/workout_templates/v1/minimal.json
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "template-minimal",
|
||||
"name": "Full body A",
|
||||
"blocks": [
|
||||
{
|
||||
"exerciseId": "exercise-minimal",
|
||||
"sets": 3,
|
||||
"reps": 8
|
||||
}
|
||||
],
|
||||
"updatedAt": "2026-07-19T10:35:00.000Z"
|
||||
}
|
||||
Reference in New Issue
Block a user