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

@ -156,6 +156,7 @@ final class _FakeBootstrap implements AppDependencies {
templateRepository: _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
),
activeWorkoutSessionUseCases = ActiveWorkoutSessionUseCases(
sessionRepository: activeRepository,

View File

@ -93,6 +93,7 @@ void main() {
expect(find.text('alex@example.com'), findsOneWidget);
expect(find.text('Synchronisation'), findsOneWidget);
expect(find.text('Partages reçus'), findsOneWidget);
expect(harness.syncRemote.pullCalls, 1);
});
testWidgets('la déconnexion repasse au profil déconnecté', (tester) async {
@ -273,6 +274,7 @@ final class _AuthHarness {
templateRepository: templateRepository,
clock: const _FakeClock(),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}

View File

@ -604,6 +604,7 @@ ShareUseCases _shareUseCases({
templateRepository: templateRepository ?? _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}

View File

@ -70,22 +70,78 @@ void main() {
ShareInboxStatus.declined,
);
});
testWidgets(
'accepter un partage en cache hors connexion importe une copie locale',
(tester) async {
final remoteShareApi = _FakeRemoteShareApi();
final inboxRepository = _FakeShareInboxRepository()
..items.add(
_shareItem(
shareId: 'share-program',
resourceType: ShareResourceType.program,
name: 'Programme tirs',
status: ShareInboxStatus.pending,
),
);
final programRepository = _FakeProgramRepository();
final pendingRepository = _FakePendingShareActionRepository();
await tester.pumpWidget(
MaterialApp(
home: ShareInboxScreen(
shareUseCases: _shareUseCases(
tokenStore: _FakeAuthTokenStore(null),
remoteShareApi: remoteShareApi,
inboxRepository: inboxRepository,
pendingActionRepository: pendingRepository,
programRepository: programRepository,
),
),
),
);
await tester.pump();
await tester.pump();
await tester.tap(find.widgetWithText(FilledButton, 'Accepter'));
await tester.pump();
await tester.pump();
expect(remoteShareApi.acceptCalls, 0);
expect(programRepository.saved.single.name, 'Programme tirs');
expect(
programRepository.saved.single.metadata.id,
isNot('share-program'),
);
expect(inboxRepository.items.single.status, ShareInboxStatus.accepted);
expect(
pendingRepository.actions.single.actionType,
PendingShareActionType.accept,
);
expect(find.text('Programme ajouté.'), findsOneWidget);
},
);
}
ShareUseCases _shareUseCases({
_FakeAuthTokenStore? tokenStore,
required _FakeRemoteShareApi remoteShareApi,
required _FakeShareInboxRepository inboxRepository,
_FakePendingShareActionRepository? pendingActionRepository,
_FakeProgramRepository? programRepository,
}) {
return ShareUseCases(
tokenStore: _FakeAuthTokenStore('token-1'),
tokenStore: tokenStore ?? _FakeAuthTokenStore('token-1'),
remoteShareApi: remoteShareApi,
inboxRepository: inboxRepository,
pendingActionRepository: _FakePendingShareActionRepository(),
pendingActionRepository:
pendingActionRepository ?? _FakePendingShareActionRepository(),
localChanges: _FakeLocalSyncChangeRepository(),
programRepository: _FakeProgramRepository(),
programRepository: programRepository ?? _FakeProgramRepository(),
templateRepository: _FakeWorkoutTemplateRepository(),
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}
@ -99,7 +155,17 @@ ShareInboxItem _shareItem({
ShareResourceType.program => {
'name': name,
'exercises': const [
{'setsCount': 3},
{
'exerciseNameSnapshot': 'Sprint',
'setsCount': 3,
'availableTimeSnapshot': false,
'availableRepsSnapshot': true,
'availableScoreSnapshot': false,
'timeEnabled': false,
'repsEnabled': true,
'scoreEnabled': false,
'targetReps': 10,
},
],
},
ShareResourceType.workoutTemplate => {
@ -267,8 +333,12 @@ final class _FakeShareInboxRepository implements ShareInboxRepository {
final class _FakePendingShareActionRepository
implements PendingShareActionRepository {
final actions = <PendingShareAction>[];
@override
Future<void> add(PendingShareAction action) async {}
Future<void> add(PendingShareAction action) async {
actions.add(action);
}
@override
Future<List<PendingShareAction>> listPending() async => const [];
@ -296,6 +366,8 @@ final class _FakeLocalSyncChangeRepository
}
final class _FakeProgramRepository implements ProgramRepository {
final saved = <Program>[];
@override
Future<Program?> findById(String id) async => null;
@ -306,7 +378,9 @@ final class _FakeProgramRepository implements ProgramRepository {
Future<void> replaceExercises(Program program, DateTime deletedAt) async {}
@override
Future<void> save(Program program) async {}
Future<void> save(Program program) async {
saved.add(program);
}
@override
Future<void> saveExercise(ProgramExercise exercise) async {}

View File

@ -537,6 +537,7 @@ ShareUseCases _shareUseCases({
templateRepository: templateRepository,
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
ids: _FakeIds(),
originDeviceId: 'device-1',
);
}