fix(online): validation client online offline-first (ticket #70)

Corrige l'acceptation de partage hors-ligne (acceptShare,
_acceptCachedShareAndQueueAck, _importSharedPayload dans
application/use_cases.dart) : crée bien une copie locale indépendante
avec de nouveaux IDs avant de mettre l'accusé serveur en file
d'attente. Ajuste app_bootstrap.dart, profile_screen.dart et
share_screen.dart en conséquence. flutter pub get OK, dart format
appliqué (aucun changement), analyze propre (mêmes infos
préexistantes), 137/137 tests verts, build APK debug validé. Dernier
ticket du chantier "Ajouter les features serveur au client" (#63,
tickets #64 à #70 tous terminés).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 09:39:40 +02:00
parent c00f61ef06
commit b6f048ad64
10 changed files with 470 additions and 13 deletions

View File

@ -1434,6 +1434,50 @@ void main() {
expect(inboxRepository.items.single.status, ShareInboxStatus.accepted);
});
test(
'ShareUseCases acceptShare sans token importe une copie locale indépendante',
() async {
final remoteShareApi = _FakeRemoteShareApi();
final pendingRepository = _FakePendingShareActionRepository();
final programRepository = _FakeProgramRepository();
final inboxRepository = _FakeShareInboxRepository()
..items.add(
_shareInboxItem(
status: ShareInboxStatus.pending,
payloadJson: jsonEncode(_sharedProgramPayload()),
),
);
await _shareUseCase(
tokenStore: _FakeAuthTokenStore(),
remoteShareApi: remoteShareApi,
inboxRepository: inboxRepository,
pendingActionRepository: pendingRepository,
programRepository: programRepository,
).acceptShare('share-1');
expect(remoteShareApi.acceptCalls, 0);
expect(
pendingRepository.actions.single.actionType,
PendingShareActionType.accept,
);
expect(inboxRepository.items.single.status, ShareInboxStatus.accepted);
final imported = programRepository.programs.single;
expect(imported.metadata.id, isNot('program-from-sender'));
expect(imported.metadata.originDeviceId, 'device-1');
expect(imported.name, 'Programme partagé');
expect(
imported.exercises.single.metadata.id,
isNot('exercise-from-sender'),
);
expect(imported.exercises.single.sourceExerciseId, isNull);
expect(
imported.exercises.single.exerciseStepsSnapshot.single.name,
'Sprint',
);
},
);
test('ShareUseCases refreshInbox updates the local cache', () async {
final remoteShareApi = _FakeRemoteShareApi()
..inboxItems = [_shareInboxItem(status: ShareInboxStatus.pending)];
@ -1866,6 +1910,7 @@ ShareUseCases _shareUseCase({
templateRepository: templateRepository ?? _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
@ -2027,17 +2072,58 @@ Program _shareableProgram() {
ShareInboxItem _shareInboxItem({
String shareId = 'share-1',
required ShareInboxStatus status,
String payloadJson = '{"name":"Programme partagé"}',
}) {
return ShareInboxItem(
shareId: shareId,
senderUserId: 'sender-1',
resourceType: ShareResourceType.program,
payloadJson: '{"name":"Programme partagé"}',
payloadJson: payloadJson,
status: status,
createdAt: DateTime.utc(2026, 7, 17, 12),
);
}
Map<String, Object?> _sharedProgramPayload() {
return {
'id': 'program-from-sender',
'name': 'Programme partagé',
'defaultRestSeconds': 45,
'exercises': [
{
'id': 'exercise-from-sender',
'sourceExerciseId': 'source-exercise-from-sender',
'position': 0,
'exerciseNameSnapshot': 'Sprint',
'exerciseStepsSnapshot': [
{
'id': 'step-from-sender',
'position': 0,
'name': 'Sprint',
'type': 'time',
'defaultTargetValue': 20,
'hasScore': false,
'scoreInputMode': 'manual',
'scoreLabel': null,
'scoreUnit': null,
'defaultTargetScore': null,
'defaultTargetScoreTimeMs': null,
},
],
'availableTimeSnapshot': true,
'availableRepsSnapshot': false,
'availableScoreSnapshot': false,
'scoreInputModeSnapshot': 'manual',
'setsCount': 2,
'timeEnabled': true,
'repsEnabled': false,
'scoreEnabled': false,
'targetTimeSeconds': 20,
},
],
};
}
RemoteSyncedItem _remoteSharedProgramItem() {
return RemoteSyncedItem(
resourceType: SyncResourceType.program,