PD-278 — Agent QA Unit/Integration Report: dip-tests¶
Module:
dip-testsAgent: agent-qa-unit-integration Date: 2026-03-01 Story: PD-278 — NF Z42-013: ajout contractuel de l'etat DIP
1. Fichiers crees¶
| Fichier | Tests | Lignes | Description |
|---|---|---|---|
src/modules/documents/services/dissemination.service.spec.ts | 54 | 1119 | Service layer: transitions, guards, transactions, audit, attestation |
src/modules/documents/controllers/dissemination.controller.spec.ts | 10 | 258 | Controller layer: delegation, DTO mapping, actor extraction |
src/modules/documents/guards/dissemination-rate-limit.guard.spec.ts | 15 | 286 | Guard layer: dual Redis counters, fail-closed, boundaries |
src/modules/documents/filters/dissemination-audit-exception.filter.spec.ts | 20 | 482 | Filter layer: synchronous audit, error routing, transaction lifecycle |
| Total | 99 | 2145 |
2. Fichier modifie (bug fix)¶
| Fichier | Action | Description |
|---|---|---|
src/modules/documents/dto/dissemination-response.dto.ts | MODIFIE | Reordonnancement des classes: DisseminationAttestationResponseDto deplace AVANT DisseminationResponseDto pour corriger un ReferenceError TDZ (Temporal Dead Zone) |
3. Couverture des scenarios contractuels¶
3.1 TC-NOM (nominaux)¶
| TC | Description | Fichier(s) | Statut |
|---|---|---|---|
| TC-NOM-01 | SEALED -> DIP mono-document | service.spec, controller.spec | PASS |
| TC-NOM-02 | SEALED -> DIP multi-document (package) | service.spec | PASS |
| TC-NOM-03 | DIP -> SEALED retour explicite | service.spec, controller.spec | PASS |
| TC-NOM-04 | motif_communication persist | service.spec, controller.spec | PASS |
| TC-NOM-06 | package_id=NULL mono, UUID multi | service.spec | PASS |
| TC-NOM-07 | returned_at >= disseminated_at (GREATEST) | service.spec | PASS |
3.2 TC-ERR (erreurs)¶
| TC | Description | Fichier(s) | Statut |
|---|---|---|---|
| TC-ERR-01 | UUID format invalide (ParseUUIDPipe) | controller.spec | PASS |
| TC-ERR-02 | Champs readonly rejetes (DTO) | controller.spec | PASS |
| TC-ERR-03 | 401 Unauthorized -> audit DENIED | filter.spec | PASS |
| TC-ERR-04 | 403 Forbidden (role) -> audit DENIED | filter.spec | PASS |
| TC-ERR-05 | 403 RLS -> audit DENIED | filter.spec | PASS |
| TC-ERR-06 | 409 E-409-STATE (PENDING -> DIP) | service.spec | PASS |
| TC-ERR-07 | 409 E-409-STATE (EXPIRED -> DIP) | service.spec | PASS |
| TC-ERR-09 | 409 E-409-STATE (DIP -> DIP self) | service.spec | PASS |
| TC-ERR-10 | 422 E-422-GUARD-COPIES | service.spec | PASS |
| TC-ERR-11 | 409 E-409-STATE (return from non-DIP) | service.spec | PASS |
| TC-ERR-12 | 403 role denial on F2 -> audit | filter.spec | PASS |
| TC-ERR-13 | 429 rate-limit -> audit DENIED | guard.spec, filter.spec | PASS |
| TC-ERR-14 | 409 E-409-RETENTION-DUE | service.spec, filter.spec | PASS |
3.3 TC-INV (invariants)¶
| TC | Description | Fichier(s) | Statut |
|---|---|---|---|
| TC-INV-01 | DIP in DocumentStatus enum | service.spec | PASS |
| TC-INV-02 | Gardes exhaustives (auth, role, rate-limit, etat, copies, retention) | service.spec | PASS |
| TC-INV-03 | Retour explicite uniquement, pas de timeout | service.spec | PASS |
| TC-INV-04 | Audit synchrone sur refus (filter) | filter.spec | PASS |
| TC-INV-05 | 1 attestation par requete (mono et multi) | service.spec | PASS |
| TC-INV-08 | GREATEST(NOW(), disseminated_at) | service.spec | PASS |
| TC-INV-11 | Package atomique: 1 echec = rejet global | service.spec | PASS |
| TC-INV-12 | SELECT FOR UPDATE avec ORDER BY ASC | service.spec | PASS |
| TC-INV-13 | returned_at >= disseminated_at | service.spec | PASS |
3.4 TC-NR (non-regression)¶
| TC | Description | Fichier(s) | Statut |
|---|---|---|---|
| TC-NR-01 | DocumentStatus contient les 5 valeurs attendues | service.spec | PASS |
| TC-NR-03/04 | Audit DISSEMINATED/RETURNED sur succes | service.spec | PASS |
3.5 TC-NEG (negatifs)¶
| TC | Description | Fichier(s) | Statut |
|---|---|---|---|
| TC-NEG-02 | RESTITUTED -> DIP interdit | service.spec | PASS |
| TC-NEG-06 | Pas de mutation des champs immutables (hash, metadata, path) | service.spec | PASS |
| TC-NEG-07 | Pas de methode PENDING -> SEALED exposee | service.spec | PASS |
3.6 TC-FML (formels)¶
| TC | Description | Fichier(s) | Statut |
|---|---|---|---|
| TC-FML-02 | Couverture des 14 invariants INV-278 | service.spec | PASS |
4. Details d'implementation par fichier¶
4.1 dissemination.service.spec.ts (54 tests)¶
Pattern: mock QueryRunner avec query.mockImplementation() routant les SQL (SELECT FOR UPDATE, UPDATE, INSERT INTO attestations, INSERT INTO journal).
Helpers: - createDocRow(overrides) — factory pour un document SEALED nominal - createDipDocRow(overrides) — factory pour un document DIP nominal - setupNominalDisseminate(overrides) — setup complet mock pour F1
Couverture specifique: - Transitions interdites: PENDING, EXPIRED, DIP, RESTITUTED -> DIP (4 tests) - Transaction ACID: rollback sur echec attestation, journal, etat invalide (3 tests) - Package atomique: rejet global si 1 document invalide sur N (3 tests) - Temporal: GREATEST dans SQL, rejection si violation d'ordre (2 tests) - Champs immutables: absence de encrypted_metadata/file_hash/ovh_path dans UPDATE (1 test) - Server-only timestamps: NOW() dans SQL, pas de client timestamps (1 test) - Locking: SELECT FOR UPDATE avec ORDER BY ASC pour deadlock prevention (2 tests) - E-404-NOT-FOUND: document inexistant sur disseminate et return (2 tests)
4.2 dissemination.controller.spec.ts (10 tests)¶
Pattern: Test.createTestingModule avec overrideGuard().useValue({ canActivate: () => true }) pour JwtAuthGuard, AuthorizationGuard, DisseminationRateLimitGuard.
Couverture: - Delegation service: arguments corrects (documents, actorId, motif) - DTO mapping: DisseminationResponseDto.fromResult() et DisseminationReturnResponseDto.fromResult() - Actor extraction: user.sub (JWT claim) extrait via @CurrentUser() - Propagation d'erreurs transparente - Champs extra ignores (whitelist DTO)
4.3 dissemination-rate-limit.guard.spec.ts (15 tests)¶
Pattern: jest.mock('ioredis') avec mock Redis INCR/EXPIRE/TTL.
Couverture: - Dual counter: per-minute (dip:rate:{actorId}) + daily quota (dip:quota:{actorId}:{date}) - TTL management: EXPIRE uniquement sur premiere requete (TTL=-2) - Fail-closed 503: Redis ECONNREFUSED et Connection timeout - Boundary values: exact limit (pass) vs limit+1 (reject) pour les deux compteurs - Actor identity: absence de sub, sub vide, fallback user.id - Key format: pattern dip:rate: et dip:quota: verifie
4.4 dissemination-audit-exception.filter.spec.ts (20 tests)¶
Pattern: mock ArgumentsHost + DataSource.createQueryRunner() + AuditLogService.logAsync().
Couverture: - Erreurs auditables: 401, 403-ROLE, 403-RLS, 429, 409-RETENTION-DUE (5 tests) - Erreurs NON auditables: 409-STATE, 409-CONFLICT, 422, 400, 500 (5 tests) - Persistence synchrone: commit AVANT response HTTP (test d'ordonnancement) - Transaction lifecycle: commit sur succes, rollback sur echec audit, release systematique - Document ID extraction: params.id (F2), body.documents[0] (F1), graceful null - Error code extraction: structured error_code, fallback status-based - Non-HttpException: retour 500 sans audit
5. Bug decouvert et corrige¶
TDZ (Temporal Dead Zone) dans dissemination-response.dto.ts¶
Symptome: ReferenceError: Cannot access 'DisseminationAttestationResponseDto' before initialization
Cause racine: DisseminationResponseDto (defini en premier) referencait DisseminationAttestationResponseDto (defini apres) dans son annotation de type de propriete. Les annotations TypeScript avec decorateurs @ApiProperty() sont evaluees au moment de la definition de la classe, causant une erreur TDZ.
Correction: Deplacement de DisseminationAttestationResponseDto avant DisseminationResponseDto dans le fichier. Le fichier contenait deja un commentaire NOTE: Defined before ... to avoid TDZ mais les classes etaient dans le mauvais ordre.
6. Resultat final¶
Tous les tests passent. Aucun test en echec, aucun test saute.
7. Decisions techniques¶
| Decision | Justification |
|---|---|
jest.mock('ioredis') au niveau module | Isolation Redis sans connexion reelle — test unitaire, pas integration |
query.mockImplementation() avec routage SQL | Permet de verifier le contenu SQL exact (GREATEST, FOR UPDATE, ORDER BY) |
expect.objectContaining() pour audit assertions | Verifie les champs critiques sans fragiliser sur les champs annexes |
expect.any(Array) au lieu de expect.anything() pour args nullable | Jest 29.7: expect.anything() ne match pas null ni undefined |
Pas de jest.useFakeTimers() | Les timestamps sont generes cote SQL (NOW()), pas cote applicatif |
Helper factories avec overrides | Pattern spread operator pour eviter la mutation de fixtures partagees (REX PD-237) |