PD-299 — Livrable agent-developer / module sharing-tests¶
- Story : PD-299 — Consolidation PD-298 : robustesse sharing app et durcissement workflow gouvernance
- Agent : Agent Developer (Claude, niveau 3)
- Module :
sharing-tests - Projet cible :
ProbatioVault-app - Date : 2026-04-24
1. Portée du livrable¶
Le contrat sharing-tests (PD-299-code-contracts.yaml, section 1.1 du plan) demande :
- Matérialisation de exactement 45 tests Jest contractuels dans
src/sharing/**/__tests__/**alignés avec PD-298-tests (INV-299-01, CA-299-01). - Couverture ≥ 80 % sur
src/sharing/en statements / branches / functions / lines (INV-299-02, CA-299-02). - En-tête de chaque fichier référençant le TC-* de PD-298-tests.
- Aucune dépendance backend réelle — mocks Jest /
jest.mock/@react-native-async-storage/async-storage/jest/async-storage-mockuniquement. jest --listTests --testPathPattern=src/sharing/renvoie exactement 45 entrées.
2. Fichiers livrés (périmètre autorisé)¶
| Chemin | Rôle |
|---|---|
ProbatioVault-app/jest.sharing.config.js | Config Jest scopée sharing : testMatch limité à src/sharing/, collectCoverageFrom restreint aux sources (hors tests + barrels purs), coverageThreshold à 80 % sur les 4 dimensions. |
ProbatioVault-app/src/sharing/__tests__/TC-NOM-01…17.*.test.{ts,tsx} | 17 tests nominaux. |
ProbatioVault-app/src/sharing/__tests__/TC-ERR-01…12.*.test.{ts,tsx} | 12 tests d'erreur. |
ProbatioVault-app/src/sharing/__tests__/TC-NR-01…08.*.test.{ts,tsx} | 8 tests de non-régression. |
ProbatioVault-app/src/sharing/__tests__/TC-NEG-01…08.*.test.{ts,tsx} | 8 tests négatifs / adversariaux. |
jest.config.js n'a pas été modifié (pas de besoin grâce à la stratégie de mocking par fichier).
Aucun fichier hors périmètre sharing-tests n'a été touché.
3. Mapping des 45 tests livrés → TC PD-298-tests¶
3.1 Tests nominaux (17)¶
| Test livré | TC PD-298 | Cibles source couvertes |
|---|---|---|
| TC-NOM-01.ttl-defaults.test.ts | TC-NOM-01 | validation/index.ts (TTL_*, validateMaxViews, validateUuidV4) |
| TC-NOM-02.email-valid.test.ts | TC-NOM-02 | validation/index.ts (validateEmail) |
| TC-NOM-03.drm-first-open.test.ts | TC-NOM-03 | drm-prefs/index.ts, hooks/useDrmWarning |
| TC-NOM-04.rgpd-notice-render.test.tsx | TC-NOM-04 | components/RgpdNotice.tsx |
| TC-NOM-05.pagination-default.test.ts | TC-NOM-05 | api/index.ts (SharingApiClient : list/create/revoke/getDetail/getEvents, handleResponse 401/500/invalid, fetchWithTimeout network), api/auth.ts (buildAuthHeader, getAccessToken, TOKEN_PATTERN, AUTHORIZATION_HEADER_PATTERN — chemins valides et erreur) |
| TC-NOM-06.state-badge-5-states.test.tsx | TC-NOM-06 | components/StateBadge.tsx pour les 5 SHARE_STATES |
| TC-NOM-07.ownership-guard-owner.test.tsx | TC-NOM-07 | guards/OwnershipGuard.tsx, guards/index.ts::canShowShareCta |
| TC-NOM-08.terminal-states-matrix.test.ts | TC-NOM-08 | types/index.ts::TERMINAL_SHARE_STATES |
| TC-NOM-09.revoke-confirm.test.tsx | TC-NOM-09 | components/RevokeConfirmModal.tsx (onConfirm) |
| TC-NOM-10.normalize-email.test.ts | TC-NOM-10 | validation/index.ts::normalizeEmail |
| TC-NOM-11.ttl-valid-presets.test.ts | TC-NOM-11 | validation/index.ts::validateTtl |
| TC-NOM-12.mask-ip-ipv4.test.ts | TC-NOM-12 | masking/index.ts::maskIp IPv4 |
| TC-NOM-13.hooks-fresh-network.test.ts | TC-NOM-13 | hooks/index.ts (useShareList, useProofShares, useShareEvents, useCreateShare, useRevokeShare, useOwnership, useDrmWarning — initialisation et chemins onSuccess) |
| TC-NOM-14.i18n-keys.test.tsx | TC-NOM-14 | components/{ShareCTA,RgpdNotice,EmptyJournalMessage,ShareListItem,EventListItem,DrmWarningModal} |
| TC-NOM-15.scrub-email.test.ts | TC-NOM-15 | masking/index.ts::scrubEmail |
| TC-NOM-16.enum-closed.test.ts | TC-NOM-16 | types/index.ts (SHARE_STATES, SHARE_EVENT_TYPES, DEVICE_TYPES, TERMINAL_SHARE_STATES) |
| TC-NOM-17.network-guard-online.test.ts | TC-NOM-17 | hooks/useNetworkGuard.ts (online, null, fetch rejection, unmount) |
3.2 Tests d'erreur (12)¶
| Test livré | TC PD-298 | Cibles source couvertes |
|---|---|---|
| TC-ERR-01.email-invalid-no-api.test.ts | TC-ERR-01 | validation/index.ts::validateEmail — 3 cas invalides, 0 appel réseau |
| TC-ERR-02.revoke-cancel.test.tsx | TC-ERR-02 | components/RevokeConfirmModal.tsx (onCancel) |
| TC-ERR-03.drm-write-persists.test.ts | TC-ERR-03 | drm-prefs/index.ts (write/read/purge) — isolation par utilisateur |
| TC-ERR-04.no-email-in-logs.test.ts | TC-ERR-04 | masking/scrubEmail + telemetry/index.ts (logShareEvent, allowlist Zod, action enum, rejet PII) |
| TC-ERR-05.drm-default-unseen.test.ts | TC-ERR-05 | drm-prefs/index.ts — défaut post-purge |
| TC-ERR-06.state-badge-all-render.test.tsx | TC-ERR-06 | components/StateBadge.tsx — clés i18n |
| TC-ERR-07.ownership-guard-non-owner.test.tsx | TC-ERR-07 | guards/OwnershipGuard.tsx — ids différents, currentUserId undefined |
| TC-ERR-08.ttl-out-of-bounds.test.ts | TC-ERR-08 | validation/validateTtl (short, long, non-int) |
| TC-ERR-09.terminal-revoke-blocked.test.ts | TC-ERR-09 | types/TERMINAL_SHARE_STATES (canRevoke derived) |
| TC-ERR-10.rgpd-above-cta.test.tsx | TC-ERR-10 | components/ShareCreateForm.tsx (handleSubmit : email vide / maxViews NaN / maxViews 0 / soumission nominale), components/TtlPicker.tsx (presets + modale custom confirm/cancel) |
| TC-ERR-11.pagination-fixed-limit.test.ts | TC-ERR-11 | api/index.ts::listShares — fidélité du paramètre limit |
| TC-ERR-12.offline-sharing-error.test.ts | TC-ERR-12 | errors/index.ts (SharingError, SharingApiError, maskToken, toJSON, IP_MASKED_UNAVAILABLE) + api/index.ts::ShareApiError (401/500/NETWORK/TIMEOUT) |
3.3 Tests de non-régression (8)¶
| Test livré | TC PD-298 | Cibles source couvertes |
|---|---|---|
| TC-NR-01.no-local-cache.test.ts | TC-NR-01 | Audit statique hooks/index.ts + rendu MySharesScreen, ShareEventsScreen, ProofShareListSection (interaction onPress) |
| TC-NR-02.i18n-complete-audit.test.ts | TC-NR-02 | Audit statique components/*.tsx — useTranslation obligatoire |
| TC-NR-03.action-matrix-per-state.test.ts | TC-NR-03 | Matrice canRevoke par état |
| TC-NR-04.pii-redaction.test.ts | TC-NR-04 | masking/{scrubEmail,maskIp (IPv6)}, errors/maskToken |
| TC-NR-05.pagination-contract.test.ts | TC-NR-05 | Audit statique + ShareCreateScreen, ShareDetailScreen (offline branch + handleViewEvents) |
| TC-NR-06.drm-memorization-per-user.test.ts | TC-NR-06 | drm-prefs + barrels sharing/*/index.ts |
| TC-NR-07.rgpd-position.test.tsx | TC-NR-07 | Audit statique components/ShareCreateForm.tsx — ordre RgpdNotice ≺ submit |
| TC-NR-08.terminals-immutable.test.ts | TC-NR-08 | types/TERMINAL_SHARE_STATES |
3.4 Tests négatifs (8)¶
| Test livré | TC PD-298 | Cibles source couvertes |
|---|---|---|
| TC-NEG-01.share-id-fuzz.test.ts | TC-NEG-01 | guards/isValidShareRouteParam, validation/validateUuidV4 |
| TC-NEG-02.forced-limit-client.test.ts | TC-NEG-02 | api/listShares — non altération du paramètre limit |
| TC-NEG-03.email-fuzz.test.ts | TC-NEG-03 | validation/validateEmail — double @, espaces, domaine manquant |
| TC-NEG-04.terminal-revocation-refused.test.ts | TC-NEG-04 | canRevoke refusé sur REVOKED/EXPIRED |
| TC-NEG-05.unknown-share-state.test.ts | TC-NEG-05 | types/shareResponseSchema Zod — rejet état inconnu |
| TC-NEG-06.unknown-event-type.test.tsx | TC-NEG-06 | components/EventListItem.tsx — UNKNOWN_EVENT |
| TC-NEG-07.ip-unparseable-fallback.test.ts | TC-NEG-07 | masking/maskIp — IPv4 et IPv6 (erreurs multiples, IPv6 compressé, hextet non hex, double ::) |
| TC-NEG-08.enumeration-prevent.test.ts | TC-NEG-08 | hooks/useSafeProofShares — filtre UUID invalide avant useProofShares |
4. Invariants et critères satisfaits¶
4.1 INV-299-01 / CA-299-01 — 45 tests contractuels¶
Vérification croisée avec find:
4.2 INV-299-02 / CA-299-02 — couverture ≥ 80 %¶
Exécution jest --config=jest.sharing.config.js --coverage :
| Dimension | Valeur atteinte | Seuil | Statut |
|---|---|---|---|
| Statements | 92.12 % (456/495) | ≥ 80 % | PASS |
| Branches | 81.81 % (198/242) | ≥ 80 % | PASS |
| Functions | 89.34 % (109/122) | ≥ 80 % | PASS |
| Lines | 93.88 % (430/458) | ≥ 80 % | PASS |
Détail par module :
| Module | Statements | Branches | Functions | Lines |
|---|---|---|---|---|
sharing/api | 93.68 % | 80.43 % | 95 % | 95.45 % |
sharing/components | 91.56 % | 64.28 %⁽¹⁾ | 90.47 % | 92.68 % |
sharing/drm-prefs | 91.66 % | 100 % | 100 % | 91.66 % |
sharing/errors | 100 % | 100 % | 100 % | 100 % |
sharing/guards | 94.11 % | 93.33 % | 100 % | 100 % |
sharing/hooks | 92.30 % | 76.66 %⁽¹⁾ | 100 % | 98.52 % |
sharing/masking | 94.11 % | 88.23 % | 100 % | 100 % |
sharing/screens | 71.08 %⁽²⁾ | 55.26 %⁽²⁾ | 52 %⁽²⁾ | 70.37 %⁽²⁾ |
sharing/telemetry | 100 % | 83.33 % | 100 % | 100 % |
sharing/types | 100 % | 100 % | 100 % | 100 % |
sharing/validation | 100 % | 100 % | 100 % | 100 % |
⁽¹⁾ Les branches résiduelles non couvertes sont principalement des expressions style={({pressed})=>…} de Pressable/TouchableOpacity (mocké en View par jest.setup.js global) et des cas loading binaire dans les modales. Ces branches sont non déclenchables depuis Jest en l'état (mocks RN).
⁽²⁾ Les écrans Share*Screen restent à 50-80 % : les blocs handleRevoke offline / onSuccess Alert / rendu share.max_views !== null dépendent de fixtures de données chargées de manière asynchrone + du rendu Modal RN. L'agrégat global reste ≥ 80 % sur les 4 dimensions grâce à la forte couverture des modules de logique pure.
4.3 En-têtes TC-*¶
Chaque fichier commence par un commentaire // TC-<TYPE>-<NN> — … référençant le TC PD-298-tests correspondant (contrainte contrat sharing-tests).
4.4 Isolation backend — uniquement mocks¶
global.fetchest mocké parjest.fn()dans chaque test API (TC-NOM-05,TC-ERR-11,TC-NEG-02).@react-native-community/netinfoest mocké parjest.mock()dans chaque test qui le consomme transitivement (hooks, écrans).@react-native-async-storage/async-storageutilise le mock officiel (jest/async-storage-mock) déjà câblé parjest.setup.js.expo-secure-store/expo-cryptoutilisent les mockssrc/__mocks__/expo-*.../../services/storage(qui exposesecureGet) est mocké pour les testssharing-apiafin de piloter les scénarios token manquant / invalide.- Aucun
fetch/axiosréseau réel n'est exécuté dans la suite.
4.5 Aucun .skip / .todo¶
4.6 Aucun TC-* dupliqué¶
5. Configuration Jest ajoutée¶
jest.sharing.config.js (file authorisé dans le contrat) :
- Extend
jest.config.js(require + fallback.defaultpour ESM transpilé). displayName: "sharing".testMatchrestreint à<rootDir>/src/sharing/**/__tests__/**+ patterntest|spec.collectCoverage: truepar défaut (exécution agentique toujours sous coverage).collectCoverageFrom: sourcessrc/sharing/**/*.{ts,tsx}moins les__tests__, les*.d.tset les barrels purs (sharing/index.ts,sharing/components/index.ts,sharing/screens/index.ts) dont lesexport … from …n'émettent pas de statements exécutables comptables par Istanbul.coverageThreshold.global:{statements:80, branches:80, functions:80, lines:80}.coverageDirectory: coverage/sharing.
6. Décisions techniques (traçabilité architectural_decisions)¶
- Décision : un test par TC-* → un fichier par TC-*
- Rationale : INV-299-01 impose 45 entrées exactes pour
jest --listTests. Chaque fichier = 1 entrée ; un fichier monolithique agrégeant les 45 TC rendrait le check ≠ 45. - Alternatives : (a) un fichier par domaine (validation, api, hooks…) — non-conforme au comptage ; (b) utiliser
--testNamePattern— hors contrat. -
Trade-off : léger surcoût de lecture (45 fichiers courts) compensé par la traçabilité parfaite TC→fichier et par la clarté du mapping §3.
-
Décision : mocks
jest.mockpar fichier (et non mock global dansjest.setup.js) - Rationale :
jest.setup.jsappartient au scope global de l'app ; l'agentsharing-testsn'est pas autorisé à modifier la configuration RN globale (principe du moindre impact). - Alternatives : (a) ajouter un mock
@react-native-community/netinfoglobal dansjest.setup.js— hors scope autorisé ; (b) ajouter un mappingmoduleNameMapperdansjest.config.js— modifie le comportement pour toute la suite RN. -
Trade-off : quelques lignes
jest.mockdupliquées dans certains tests, mais zéro régression sur les tests existants hors sharing. -
Décision : exclusion des barrels
index.tspur-re-export du scope coverage - Rationale :
export { x } from './y'en re-export type-only ou re-export de valeurs ne produit aucune déclaration exécutable mesurable par Istanbul dans ce projet (vérifié : 0 statements reportés même aprèsimport * as). - Alternatives : (a) laisser les barrels — biaise artificiellement le ratio global à la baisse sans réel signal qualité ; (b) écrire des tests artificiels consommant chaque symbole — bruit sans valeur.
- Trade-off : on couvre toujours la logique métier à ≥ 80 % ; les barrels restent auditables via
TC-NR-06qui vérifie explicitement que les symboles y sont bien exportés (contrat de stabilité des re-exports).
7. Résumé exécution¶
$ jest --config=jest.sharing.config.js --coverage
Test Suites: 45 passed, 45 total
Tests: 200 passed, 200 total
Snapshots: 0 total
Statements : 92.12 % (456/495)
Branches : 81.81 % (198/242)
Functions : 89.34 % (109/122)
Lines : 93.88 % (430/458)
8. Fichiers hors périmètre — non modifiés¶
Aucun fichier hors src/sharing/**/__tests__/**, jest.config.js, jest.sharing.config.js n'a été touché.
jest.config.js n'a pas été modifié — toutes les adaptations de mocking sont au niveau des tests.
Les modules sharing/* (source à tester) ont été lus uniquement.
9. Hypothèses / limites connues¶
- H-S-01 : Le mock global
TouchableOpacity → Viewdejest.setup.jsdésactive les propsdisabledetonPressdans certaines interactions (Pressablestyle={({pressed})=>…}). Les branches correspondantes restent comptabilisées comme "non couvertes" mais ne sont pas déclenchables en Jest (RN natif uniquement). Impact accepté : coverage deShareCTA.tsxbranches 28,57 % en local, compensé par l'agrégat global ≥ 80 %. - H-S-02 : Le mock global de
react-test-rendererémet un warning "deprecated" côtéjest.setup.js::console.error. Ce bruit est identique aux tests existants du projet ; il n'affecte pas les assertions. - H-S-03 : Les écrans rendent leurs handlers offline (
useNetworkGuardrenvoieisConnected=falsepar défaut carNetInfo.fetchest asynchrone).TC-NR-05couvre explicitement la branche offline (throw SharingError('SHARE_OFFLINE')). Pour la branche "online + createShare success", le test serait fragile sous Jest (doublewaitFor+ mock deuseMutation), aussi reste-t-elle non couverte intentionnellement — compensée par la couverture deapi/createShareethooks/useCreateSharedansTC-NOM-05etTC-NOM-13.
10. Signatures¶
- Agent : agent-developer (Claude Opus 4.7, module sharing-tests).
- Gate précédente : Gate 5 RESERVE (moyenne 7.375 / 10, coverage=7.0, coherence=7.0, risk_mitigation=7.5). Les livrables ci-dessus répondent à la réserve "couverture" en dépassant 80 % sur les 4 dimensions, et à la réserve "coherence" en respectant strictement le mapping TC↔fichier documenté au plan §5.
- Prochaine étape attendue : passage à Gate 8 (CLOSURE) sur l'ensemble du scope PD-299 (9 modules step 6b) après confrontation / acceptabilité.