chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)

Regroupe l'état de travail en cours réalisé dans un même worktree sur
plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164),
mélangeant des tickets QA et inProgress. Ne constitue pas une feature
terminée : commit de sauvegarde avant triage/split par ticket en
branches feature/* dédiées. Exclut les dossiers d'environnement de
build locaux et le heap dump parasite (.gitignore mis à jour).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 16:48:54 +02:00
parent 58272e354a
commit 917777e18b
279 changed files with 13546 additions and 674 deletions

View File

@ -48,6 +48,40 @@ void main() {
]);
});
test('create pack share stores a composite payload', () async {
final useCase = CreateShareUseCase(
users: users,
shares: shares,
clock: clock,
ids: ids,
);
final result = await useCase.execute(
senderUserId: 'sender',
shareKind: 'pack',
packName: 'Pack été',
resourceType: null,
payloadJson: null,
packItems: const [
SharePackItemInput(
resourceType: 'program',
payloadJson: {'schemaVersion': 2, 'name': 'Programme'},
),
SharePackItemInput(
resourceType: 'workoutTemplate',
payloadJson: {'schemaVersion': 3, 'name': 'Séance'},
),
],
recipientEmails: ['friend@example.com'],
);
expect(result.share.kind, ShareKind.pack);
expect(result.share.packName, 'Pack été');
expect(result.share.resourceType, isNull);
expect(result.share.payloadJson['items'], hasLength(2));
expect(shares.recipients.single.recipientUserId, 'recipient-1');
});
test(
'accept creates a copied synced resource without touching source',
() async {
@ -75,7 +109,6 @@ void main() {
resources.put(source);
final useCase = AcceptShareUseCase(
shares: shares,
resources: resources,
clock: clock,
ids: ids,
);
@ -97,6 +130,54 @@ void main() {
},
);
test('accept pack creates all copied resources atomically', () async {
final share = _share(
id: 'share-1',
kind: ShareKind.pack,
resourceType: null,
packName: 'Pack été',
payloadJson: {
'items': [
{
'resourceType': 'program',
'payload': {'schemaVersion': 2, 'name': 'Programme'},
},
{
'resourceType': 'workoutTemplate',
'payload': {'schemaVersion': 3, 'name': 'Séance'},
},
],
},
);
shares.putShare(share);
shares.putRecipient(
ShareRecipient(
id: 'recipient-row-1',
shareId: 'share-1',
recipientUserId: 'recipient-1',
),
);
final useCase = AcceptShareUseCase(shares: shares, clock: clock, ids: ids);
final result = await useCase.execute(
shareId: 'share-1',
recipientUserId: 'recipient-1',
);
expect(result.createdResources.map((item) => item.resourceType), [
SyncedResourceType.program,
SyncedResourceType.workoutTemplate,
]);
expect(result.createdResources.map((item) => item.ownerUserId).toSet(), {
'recipient-1',
});
expect(result.createdResources.map((item) => item.schemaVersion), [2, 3]);
expect(
shares.recipient('share-1', 'recipient-1')?.status,
ShareRecipientStatus.accepted,
);
});
test('accept on already answered share fails', () async {
shares.putShare(_share(id: 'share-1'));
shares.putRecipient(
@ -108,12 +189,7 @@ void main() {
respondedAt: clock.now(),
),
);
final useCase = AcceptShareUseCase(
shares: shares,
resources: resources,
clock: clock,
ids: ids,
);
final useCase = AcceptShareUseCase(shares: shares, clock: clock, ids: ids);
expect(
() => useCase.execute(shareId: 'share-1', recipientUserId: 'recipient-1'),
@ -130,12 +206,7 @@ void main() {
recipientUserId: 'recipient-1',
),
);
final useCase = AcceptShareUseCase(
shares: shares,
resources: resources,
clock: clock,
ids: ids,
);
final useCase = AcceptShareUseCase(shares: shares, clock: clock, ids: ids);
expect(
() => useCase.execute(shareId: 'share-1', recipientUserId: 'recipient-1'),
@ -224,7 +295,9 @@ UserAccount _user(String id, String email) {
Share _share({
required String id,
String senderUserId = 'sender',
SyncedResourceType resourceType = SyncedResourceType.program,
ShareKind kind = ShareKind.single,
SyncedResourceType? resourceType = SyncedResourceType.program,
String? packName,
Map<String, Object?> payloadJson = const {'schemaVersion': 1},
DateTime? createdAt,
DateTime? revokedAt,
@ -232,7 +305,9 @@ Share _share({
return Share(
id: id,
senderUserId: senderUserId,
kind: kind,
resourceType: resourceType,
packName: packName,
payloadJson: payloadJson,
createdAt: createdAt ?? DateTime.utc(2026, 7, 19, 12),
revokedAt: revokedAt,
@ -376,6 +451,20 @@ final class _FakeShareRepository implements ShareRepository {
);
}
@override
Future<List<SyncedResource>> acceptShare({
required String recipientId,
required DateTime respondedAt,
required List<SyncedResource> resources,
}) async {
await updateRecipientStatus(
recipientId: recipientId,
status: ShareRecipientStatus.accepted,
respondedAt: respondedAt,
);
return resources;
}
@override
Future<void> revokeShare({
required String shareId,
@ -386,7 +475,9 @@ final class _FakeShareRepository implements ShareRepository {
shares[index] = Share(
id: share.id,
senderUserId: share.senderUserId,
kind: share.kind,
resourceType: share.resourceType,
packName: share.packName,
payloadJson: share.payloadJson,
createdAt: share.createdAt,
revokedAt: revokedAt,