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:
@ -2,6 +2,8 @@ import 'package:gametime_server/application/application.dart';
|
||||
import 'package:gametime_server/domain/domain.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'fixtures/sync_fixtures.dart';
|
||||
|
||||
void main() {
|
||||
late _FakeSyncedResourceRepository resources;
|
||||
late _FakeClock clock;
|
||||
@ -89,6 +91,231 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test('push and pull preserve versioned exercise fixtures strictly', () async {
|
||||
final push = PushSyncUseCase(resources: resources, clock: clock, ids: ids);
|
||||
final pull = PullSyncUseCase(resources: resources, clock: clock);
|
||||
final fixtures = loadExerciseFixtures();
|
||||
|
||||
final pushResult = await push.execute(
|
||||
ownerUserId: 'user-1',
|
||||
deviceId: 'device-1',
|
||||
items: [
|
||||
for (var index = 0; index < fixtures.length; index += 1)
|
||||
PushSyncItemInput(
|
||||
resourceType: fixtures[index].resourceType,
|
||||
clientId: 'exercise-${fixtures[index].name}',
|
||||
schemaVersion: fixtures[index].schemaVersion,
|
||||
clientUpdatedAt: DateTime.utc(2026, 7, 19, 10, index),
|
||||
deletedAt: null,
|
||||
payloadJson: fixtures[index].payload,
|
||||
),
|
||||
],
|
||||
);
|
||||
final pullResult = await pull.execute(ownerUserId: 'user-1');
|
||||
|
||||
expect(
|
||||
pushResult.results.map((item) => item.status),
|
||||
everyElement('accepted'),
|
||||
);
|
||||
expect(pullResult.items, hasLength(fixtures.length));
|
||||
for (final fixture in fixtures) {
|
||||
final pulled = pullResult.items.singleWhere(
|
||||
(item) => item.clientId == 'exercise-${fixture.name}',
|
||||
);
|
||||
expect(pulled.resourceType, SyncedResourceType.exercise);
|
||||
expect(pulled.schemaVersion, fixture.schemaVersion);
|
||||
expect(pulled.payloadJson, fixture.payload);
|
||||
}
|
||||
});
|
||||
|
||||
test(
|
||||
'push and pull preserve exercise fixture batches across versions',
|
||||
() async {
|
||||
final push = PushSyncUseCase(
|
||||
resources: resources,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
);
|
||||
final pull = PullSyncUseCase(resources: resources, clock: clock);
|
||||
final fixtures = [
|
||||
loadExerciseFixture('minimal'),
|
||||
loadExerciseFixture('minimal', version: 'v2'),
|
||||
];
|
||||
|
||||
final pushResult = await push.execute(
|
||||
ownerUserId: 'user-1',
|
||||
deviceId: 'device-1',
|
||||
items: [
|
||||
for (var index = 0; index < fixtures.length; index += 1)
|
||||
PushSyncItemInput(
|
||||
resourceType: fixtures[index].resourceType,
|
||||
clientId: 'exercise-versioned-$index',
|
||||
schemaVersion: fixtures[index].schemaVersion,
|
||||
clientUpdatedAt: DateTime.utc(2026, 7, 19, 10, index),
|
||||
deletedAt: null,
|
||||
payloadJson: fixtures[index].payload,
|
||||
),
|
||||
],
|
||||
);
|
||||
final pullResult = await pull.execute(ownerUserId: 'user-1');
|
||||
|
||||
expect(pushResult.results.map((item) => item.status), [
|
||||
'accepted',
|
||||
'accepted',
|
||||
]);
|
||||
for (var index = 0; index < fixtures.length; index += 1) {
|
||||
final pulled = pullResult.items.singleWhere(
|
||||
(item) => item.clientId == 'exercise-versioned-$index',
|
||||
);
|
||||
expect(pulled.schemaVersion, fixtures[index].schemaVersion);
|
||||
expect(pulled.payloadJson, fixtures[index].payload);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'push and pull preserve reusable non-exercise fixtures strictly',
|
||||
() async {
|
||||
final push = PushSyncUseCase(
|
||||
resources: resources,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
);
|
||||
final pull = PullSyncUseCase(resources: resources, clock: clock);
|
||||
final fixtures = loadMinimalResourceFixtures();
|
||||
|
||||
final pushResult = await push.execute(
|
||||
ownerUserId: 'user-1',
|
||||
deviceId: 'device-1',
|
||||
items: [
|
||||
for (var index = 0; index < fixtures.length; index += 1)
|
||||
PushSyncItemInput(
|
||||
resourceType: fixtures[index].resourceType,
|
||||
clientId: '${fixtures[index].resourceType}-minimal',
|
||||
schemaVersion: fixtures[index].schemaVersion,
|
||||
clientUpdatedAt: DateTime.utc(2026, 7, 19, 11, index),
|
||||
deletedAt: null,
|
||||
payloadJson: fixtures[index].payload,
|
||||
),
|
||||
],
|
||||
);
|
||||
final pullResult = await pull.execute(ownerUserId: 'user-1');
|
||||
|
||||
expect(
|
||||
pushResult.results.map((item) => item.status),
|
||||
everyElement('accepted'),
|
||||
);
|
||||
for (final fixture in fixtures) {
|
||||
final pulled = pullResult.items.singleWhere(
|
||||
(item) => item.clientId == '${fixture.resourceType}-minimal',
|
||||
);
|
||||
expect(pulled.resourceType.wireName, fixture.resourceType);
|
||||
expect(pulled.schemaVersion, fixture.schemaVersion);
|
||||
expect(pulled.payloadJson, fixture.payload);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'LWW ignores older and equal exercise fixture payloads strictly',
|
||||
() async {
|
||||
final original = loadExerciseFixture('full_combo');
|
||||
final newer = loadExerciseFixture('with_timers');
|
||||
final push = PushSyncUseCase(
|
||||
resources: resources,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
);
|
||||
|
||||
final result = await push.execute(
|
||||
ownerUserId: 'user-1',
|
||||
deviceId: 'device-1',
|
||||
items: [
|
||||
PushSyncItemInput(
|
||||
resourceType: original.resourceType,
|
||||
clientId: 'exercise-lww',
|
||||
schemaVersion: original.schemaVersion,
|
||||
clientUpdatedAt: DateTime.utc(2026, 7, 19, 10),
|
||||
deletedAt: null,
|
||||
payloadJson: original.payload,
|
||||
),
|
||||
PushSyncItemInput(
|
||||
resourceType: newer.resourceType,
|
||||
clientId: 'exercise-lww',
|
||||
schemaVersion: newer.schemaVersion,
|
||||
clientUpdatedAt: DateTime.utc(2026, 7, 19, 9, 59),
|
||||
deletedAt: null,
|
||||
payloadJson: newer.payload,
|
||||
),
|
||||
PushSyncItemInput(
|
||||
resourceType: newer.resourceType,
|
||||
clientId: 'exercise-lww',
|
||||
schemaVersion: newer.schemaVersion,
|
||||
clientUpdatedAt: DateTime.utc(2026, 7, 19, 10),
|
||||
deletedAt: null,
|
||||
payloadJson: newer.payload,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(result.results.map((item) => item.status), [
|
||||
'accepted',
|
||||
'ignoredOlder',
|
||||
'ignoredOlder',
|
||||
]);
|
||||
final stored = resources.get(
|
||||
'user-1',
|
||||
SyncedResourceType.exercise,
|
||||
'exercise-lww',
|
||||
);
|
||||
expect(stored?.schemaVersion, original.schemaVersion);
|
||||
expect(stored?.payloadJson, original.payload);
|
||||
},
|
||||
);
|
||||
|
||||
test('push reports per-item malformations without storing them', () async {
|
||||
final useCase = PushSyncUseCase(
|
||||
resources: resources,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
);
|
||||
|
||||
final result = await useCase.execute(
|
||||
ownerUserId: 'user-1',
|
||||
deviceId: 'device-1',
|
||||
items: [
|
||||
PushSyncItemInput(
|
||||
resourceType: 'exercise',
|
||||
clientId: 'bad-schema',
|
||||
schemaVersion: 0,
|
||||
clientUpdatedAt: DateTime.utc(2026, 7, 19, 10),
|
||||
deletedAt: null,
|
||||
payloadJson: {},
|
||||
),
|
||||
const PushSyncItemInput(
|
||||
resourceType: 'exercise',
|
||||
clientId: 'missing-updated-at',
|
||||
schemaVersion: 1,
|
||||
clientUpdatedAt: null,
|
||||
deletedAt: null,
|
||||
payloadJson: {},
|
||||
),
|
||||
const PushSyncItemInput.invalid(
|
||||
resourceType: 'exercise',
|
||||
clientId: 'bad-payload',
|
||||
message: 'payload must be a JSON object.',
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
expect(result.results.map((item) => item.status), [
|
||||
'error',
|
||||
'error',
|
||||
'error',
|
||||
]);
|
||||
expect(resources._items, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'pull returns resources newer than cursor including soft deletes',
|
||||
() async {
|
||||
|
||||
Reference in New Issue
Block a user