PD-84 — Acceptabilité¶
Prérequis acceptabilité¶
- Tests CI : 17 suites, 133 tests, 0 failures (module freemium isolé — Jest)
- Coverage : 92.52% (seuil : 80%) — Global projet, module freemium ~100% unitaire
- TODO non tracés : aucun (grep
// TODO|// STUB|// DEV ONLY= 0 résultat) - Code DEV ONLY : aucun (PlanStubGuard protégé par ENABLE_PLAN_STUB=true, triple verrou)
Phase 1 — Reviews automatisées¶
| Check | Résultat | Détails |
|---|---|---|
| ESLint | OK | 0 issues (lint-staged + prettier) |
| Prettier (format) | OK | Tous les fichiers TS conformes |
| TypeScript (tsc --noEmit) | OK | 0 erreurs après fix isolatedModules (export type) |
| Tests Jest | OK | 17/17 suites, 133/133 tests PASS |
| Coverage | 92.52% | Au-dessus du seuil 80% |
Corrections Phase 1 : - 4 erreurs TS isolatedModules corrigées : export type pour ExportCta, FolderCapabilities, FreemiumErrorCode, FreemiumErrorResponse dans les barrel files index.ts - Tests KO détectés hors périmètre PD-84 (modules pre-existants) — non bloquants
Phase 1.5 — Analyse Sonar¶
- Quality Gate : NON EXÉCUTABLE (Docker indisponible, sonar-scanner non installé localement)
- Mitigation : Sonar sera exécuté dans le pipeline CI/CD post-merge. ESLint + TypeScript strict couvrent les catégories Sonar principales (bugs, code smells, security hotspots) en local.
- Note : Aucune vulnérabilité ESLint
security/*restante (2detect-object-injectionjustifiées par eslint-disable avec commentaire)
Phase 2 — Review Code (développeur senior NestJS/TypeScript)¶
Verdict : CONFORME¶
Architecture et patterns NestJS¶
| Critère | Évaluation |
|---|---|
| Injection de dépendances (DI) | Correct — @Injectable(), @InjectRepository(), constructeur injection partout |
| Module encapsulation | FreemiumModule importe TypeOrmModule.forFeature + AuditModule, exporte les 4 services |
| Guards | FolderOwnerGuard (ownership check app-level) + PlanStubGuard (env gate) — defense-in-depth |
| DTOs + validation | class-validator sur CreateFolderDto + ChangePlanDto — @IsEnum, @IsNotEmpty, @MaxLength |
| Controller thin / service fat | Controllers délèguent à services — aucune logique métier dans les controllers |
Qualité du code¶
| Aspect | Observation |
|---|---|
| Nommage | Cohérent (camelCase TS, snake_case DB/API) — suit le pattern du projet |
| Lisibilité | Fichiers courts (max 205 lignes), une responsabilité par service |
| Gestion d'erreurs | FreemiumException centralisée, 8 codes typés, mapping HTTP explicite, messages i18n FR |
| Logging | Logger NestJS dans services et guards — warn pour les refus, error pour SLA breach |
| Audit | Non-blocking via AuditLogService.logAsync() — 5 event types (INV-84-15) |
Points forts¶
- Atomicité transactionnelle :
pg_advisory_xact_lockpour sérialiser les opérations concurrent-sensibles (quota check + création). Pattern solide. - Idempotence :
closeFolder()retourne 409 FOLDER_ALREADY_CLOSED si déjà fermé (ECT-12) - Transition unidirectionnelle : ACTIVE → CLOSED_READ_ONLY uniquement (INV-84-08). L'enum
FolderStatusn'a que 2 valeurs. - Upgrade CTA structuré :
FreemiumExceptionajoute automatiquementupgrade_ctapour les erreurs quota/premium (INV-84-07) - Triple verrou stub :
PlanStubGuard+ TC-10-bis + CI check empêchent l'exposition de PUT /account/plan en production
Écarts identifiés¶
| ID | Sévérité | Description | Preuve |
|---|---|---|---|
| EC-R1 | MINEUR | AddDocumentDto déclaré inline dans folder-document.controller.ts (L21-23) au lieu d'un fichier DTO séparé | folder-document.controller.ts:21 — pattern non standard vs les autres DTOs |
| EC-R2 | MINEUR | hashToInt() dupliqué entre folder.service.ts:191 et folder-document.service.ts:154 | Deux implémentations identiques — pourrait être un utilitaire partagé |
| EC-R3 | MINEUR (suggestion) | FolderController.create() fait un findOneOrFail pour le user puis passe dbUser.plan — le plan pourrait être passé via le service directement | folder.controller.ts:40-42 — double requête user (guard + controller) |
Aucun écart BLOQUANT ni MAJEUR.
Phase 2 — Review Tests (QA engineer senior Jest)¶
Verdict : CONFORME¶
Couverture des cas contractuels¶
| TC-ID | Description | Couvert | Fichier test |
|---|---|---|---|
| TC-01 | Create folder FREE under quota | folder.service.spec.ts | Nominal + audit |
| TC-02 | Quota folder limit → 422 + CTA | folder.service.spec.ts | Exception code + HTTP status + upgrade_cta |
| TC-03 | Close ACTIVE → CLOSED_READ_ONLY | folder.service.spec.ts | Status + closedAt + save |
| TC-04 | Close already closed → 409 | folder.service.spec.ts | FOLDER_ALREADY_CLOSED + 409 |
| TC-05 | List folders active + closed | folder.service.spec.ts | 2 folders + DTO mapping |
| TC-06 | Add document under quota | folder-document.service.spec.ts | Result structure + seal ref |
| TC-07 | Document quota limit → 422 + CTA | folder-document.service.spec.ts | Exception + upgrade_cta |
| TC-08 | Add doc to closed folder → 422 | folder-document.service.spec.ts | FOLDER_CLOSED_READ_ONLY |
| TC-12 | No monthly reset — cumulative | freemium-scenarios.spec.ts | Count-based not time-based |
| TC-12-bis | closed_at immutable | freemium-scenarios.spec.ts | 409 + save not called |
| TC-13 | 5 audit events | freemium-scenarios.spec.ts | All 5 AuditActionType verified |
| TC-14 | Nominal flow 5 steps | freemium-scenarios.spec.ts | Create → add × 2 → close → verify |
| TC-15 | Universal plan regardless of role | freemium-scenarios.spec.ts | 3 roles × 2 plans (parametrized) |
| TC-18 | Capability recomputed after downgrade | freemium-scenarios.spec.ts | Before/after/direct check |
| TC-19 | Downgrade conserves data | freemium-scenarios.spec.ts | 3 folders preserved + quotas re-applied |
| TC-INV-01 | Same pipeline FREE/PREMIUM | folder-document.service.spec.ts | Same keys structure |
| TC-LIM-01 | pg_advisory_xact_lock folders | folder.service.spec.ts | Advisory lock call verified |
| TC-LIM-02 | pg_advisory_xact_lock documents | folder-document.service.spec.ts | Advisory lock call verified |
| TC-LIM-03 | Close + slot reallocation | freemium-scenarios.spec.ts | Close → count drops → new creation OK |
| TC-LIM-04 | Upgrade removes export restriction | freemium-scenarios.spec.ts | FREE → PREMIUM → capabilities true |
| TC-SLA-01 | 50 plan changes p95 < 1s | freemium-scenarios.spec.ts | Performance with mocks |
| TC-AUD-01 to 04 | Audit trail coverage | Multiple specs | All audit calls verified with expect.objectContaining |
| TC-NR-01 to 04 | Non-regression invariants | freemium-nonreg.spec.ts | Enum stability, error codes, export_cta, status count |
Qualité des tests¶
| Aspect | Évaluation |
|---|---|
| Isolation | Chaque test crée ses propres mocks dans beforeEach — pas de couplage |
| Assertions | Précises — expect.objectContaining() pour les audits, .getCode() / .getStatus() pour les exceptions |
| Edge cases | Boundary testing présent : ⅔ folders (OK), 3/3 (KO), 99/100 docs (OK), 100/100 (KO) |
| Mocking | DataSource.transaction mockée correctement avec callback. Manager mocks par opération. |
| Paramétrisé | it.each() pour TC-15 (3 rôles × 2 plans) |
Écarts identifiés¶
| ID | Sévérité | Description |
|---|---|---|
| EC-T1 | MINEUR | Pas de tests e2e (integration HTTP) — les controllers sont testés unitairement via leurs spec respectives. Tests e2e seraient redondants en absence de DB réelle. |
| EC-T2 | MINEUR (suggestion) | TC-02 et TC-07 appellent le service deux fois (une pour rejects.toThrow, une pour catch-inspect) — pattern verbeux mais fonctionnel |
Aucun écart BLOQUANT ni MAJEUR.
Phase 2 — Review Sécurité (pentester adversarial)¶
Verdict : CONFORME¶
Surface d'attaque analysée¶
| Vecteur | Évaluation | Détail |
|---|---|---|
| Injection SQL | PROTÉGÉ | TypeORM parameterized queries. pg_advisory_xact_lock($1) avec paramètre bindé. |
| Bypass auth | PROTÉGÉ | @UseGuards(OidcJwtAuthGuard) sur chaque controller (class-level). Token OIDC validé. |
| Bypass ownership | PROTÉGÉ | FolderOwnerGuard vérifie ownerUserId === user.sub au niveau app. RLS PostgreSQL au niveau DB (defense-in-depth). |
| IDOR | PROTÉGÉ | ParseUUIDPipe valide le format UUID. FolderOwnerGuard empêche l'accès cross-user. |
| Enum injection | PROTÉGÉ | @IsEnum(FolderCategory) sur CreateFolderDto. @IsEnum(PlanType) sur ChangePlanDto. Valeurs invalides rejetées 400. |
| Mass assignment | PROTÉGÉ | DTOs explicites avec class-validator. Pas de spread objet user input → entity. |
| Plan manipulation | PROTÉGÉ | PlanStubGuard + ENABLE_PLAN_STUB=true requis. Production = endpoint invisible (404). |
| Timing attack | PROTÉGÉ | pg_advisory_xact_lock sérialise — pas de race condition entre quota check et création. |
| Information leakage | PROTÉGÉ | Messages d'erreur en français, pas de stack trace. FOLDER_NOT_FOUND pour user absent aussi (pas de "user not found"). |
Invariants de sécurité vérifiés¶
| Invariant | Status |
|---|---|
| INV-84-01/02 : Même pipeline sealing FREE/PREMIUM | CONFORME — aucune branche plan dans le code de sealing |
| INV-84-04 : Quotas atomiques | CONFORME — pg_advisory_xact_lock + transaction |
| INV-84-08 : Transition unidirectionnelle | CONFORME — enum 2 valeurs, check status avant transition |
| INV-84-12 : Pas de colonne contenu | CONFORME — entity n'a aucun champ content/blob |
| INV-84-14 : Export refusé FREE | CONFORME — ExportController check plan, throw PREMIUM_REQUIRED |
| SEC-84-02 : Tous endpoints protégés | CONFORME — OidcJwtAuthGuard class-level sur 5 controllers |
Forbidden patterns vérifiés¶
| Pattern interdit | Trouvé ? |
|---|---|
Math.random() | Non — randomUUID() de node:crypto utilisé |
| SQL string concatenation | Non — parameterized queries uniquement |
eval() / Function() | Non |
| Credentials en dur | Non |
any cast dangereux | Non — types stricts, casts as string justifiés pour enum comparison |
Écarts identifiés¶
| ID | Sévérité | Description |
|---|---|---|
| EC-S1 | MINEUR | FolderOwnerGuard loggue userId et folderId dans les warn messages — acceptable pour debug mais à surveiller si les UUID sont considérés PII dans le contexte RGPD. L'audit module gère séparément. |
| EC-S2 | MINEUR (suggestion) | ExportController fait un findOneOrFail sur User — en cas de user supprimé entre auth et DB call, l'exception TypeORM non-customisée pourrait leaker. Mitigation : le JWT est validé en amont, donc user absent = race condition extrêmement improbable. |
Aucun écart BLOQUANT ni MAJEUR. Aucune vulnérabilité exploitable identifiée.
Synthèse¶
Résultat global : CONFORME¶
| Review | Verdict | Écarts BLOQUANT | Écarts MAJEUR | Écarts MINEUR |
|---|---|---|---|---|
| Reviews auto (lint/format/tsc/tests) | OK | 0 | 0 | 0 |
| Sonar | NON EXÉCUTÉ | - | - | - |
| Review Code | CONFORME | 0 | 0 | 3 |
| Review Tests | CONFORME | 0 | 0 | 2 |
| Review Sécurité | CONFORME | 0 | 0 | 2 |
| TOTAL | CONFORME | 0 | 0 | 7 |
Écarts MINEUR consolidés¶
| ID | Review | Description |
|---|---|---|
| EC-R1 | Code | AddDocumentDto inline au lieu de fichier séparé |
| EC-R2 | Code | hashToInt() dupliqué entre 2 services |
| EC-R3 | Code | Double requête user (guard + controller) |
| EC-T1 | Tests | Pas de tests e2e HTTP |
| EC-T2 | Tests | Pattern double-call dans TC-02/TC-07 |
| EC-S1 | Sécurité | UUID loggés dans FolderOwnerGuard |
| EC-S2 | Sécurité | findOneOrFail sans custom exception dans ExportController |
Aucun de ces écarts ne constitue un risque pour la conformité, la sécurité ou le fonctionnement. Tous classifiés MINEUR/suggestion.
Couverture invariants¶
| Invariant | Code | Tests | Sécurité |
|---|---|---|---|
| INV-84-01/02 (même pipeline) | CONFORME | TC-INV-01 | CONFORME |
| INV-84-04 (quotas atomiques) | CONFORME | TC-LIM-01/02 | CONFORME |
| INV-84-05 (upgrade immédiat) | CONFORME | TC-LIM-04 | CONFORME |
| INV-84-06 (capabilities computed) | CONFORME | TC-18 | CONFORME |
| INV-84-07 (upgrade CTA) | CONFORME | TC-NR-03 | CONFORME |
| INV-84-08 (unidirectionnel) | CONFORME | TC-03/04/NR-04 | CONFORME |
| INV-84-09 (cumulative) | CONFORME | TC-12 | N/A |
| INV-84-10 (universal plan) | CONFORME | TC-15 | N/A |
| INV-84-11 (blocked at 100) | CONFORME | TC-07 | N/A |
| INV-84-12 (no content) | CONFORME | N/A | CONFORME |
| INV-84-14 (export FREE refusé) | CONFORME | TC-05 export | CONFORME |
| INV-84-15 (audit complet) | CONFORME | TC-13/AUD-01-04 | CONFORME |