mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +02:00
f1b783407a
Triage et intégration des propositions backend du buffer 95_a_capitaliser.md (lot local RL799_V2 + app-alexandrie, mai-juin 2026), distinct de la capitalisation remote antérieure (triage 2026-05-02). ~73 entrées intégrées sur knowledge/backend/, dont : - patterns/auth.md : série "membrane d'auth fédérée BFF/OIDC" (9 patterns) + jose algo whitelist - patterns/prisma.md : recette fusionnée "Migration String/Int → enum" (backfill + Cas A/B/C), row réactivable, endpoint replace atomique, updateMany conditionnel, etc. - risques/general.md : 19 risques (epoch s vs ms, keepAliveTimeout=0, upsert+filtre liste, fail-safe catch-all, retrait asymétrique front/back, anti-énumération rate-limit, etc.) - patterns/general, async, nestjs, contracts, tests + risques/auth, contracts, prisma, redis, stripe, tests - compléments d'entrées existantes (authorize-after-fetch, P3014, cursor opaque, DI swc, Stripe v20...) - README patterns/risques mis à jour Doublons internes corrigés en relecture (suppression-champ .map() → general seul ; e2e DB-based → tests.md seul). Doublons hors backend / entrées projet / rejets non intégrés. Source 95_a_capitaliser.md non purgée à ce stade (purge en fin de capitalisation complète). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
533 lines
24 KiB
Markdown
533 lines
24 KiB
Markdown
---
|
||
title: Backend — Patterns : Tests
|
||
domain: backend
|
||
bucket: patterns
|
||
tags: [tests, vitest, prisma, integration, isolation]
|
||
applies_to: [implementation, review, debug]
|
||
severity: high
|
||
validated_on: 2026-05-02
|
||
source_projects: [RL799_V2]
|
||
---
|
||
|
||
# Backend — Patterns : Tests
|
||
|
||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||
|
||
---
|
||
|
||
<a id="pattern-cleanup-track-tests-integration-db"></a>
|
||
## Pattern : `cleanup.track()` LIFO pour tests d'intégration DB
|
||
|
||
- Objectif : nettoyer proprement les artefacts créés par chaque test sans recourir à un `TRUNCATE CASCADE` qui casserait les transactions du SUT.
|
||
- Contexte : tests qui écrivent dans une vraie base (pas mockée), en cohabitation avec un seed durable et des tables FK-liées.
|
||
- Quand l'utiliser : dès qu'un test crée des rows à la volée et qu'on veut éviter la pollution inter-tests.
|
||
- Quand l'éviter : si le projet utilise `transactions + rollback par test` (déjà isolé), ou si le SUT n'écrit jamais en DB.
|
||
- Avantage :
|
||
- ordre LIFO = ordre FK-safe (les enfants tombent avant les parents)
|
||
- résistant aux throws intermédiaires (le `afterEach` tourne quand même)
|
||
- pas de `TRUNCATE` qui casserait les transactions ouvertes par le SUT
|
||
- Limites / vigilance :
|
||
- convention "100 % via `cleanup.track`" : ne pas mélanger avec des `prisma.*.deleteMany` directs
|
||
- la queue est pour le teardown, pas pour le setup : ne jamais tracker une création
|
||
- Validé le : 24-04-2026
|
||
- Contexte technique : Vitest / Prisma / Postgres — RL799_V2
|
||
|
||
### Implémentation (helper minimal)
|
||
|
||
```typescript
|
||
// __tests__/helpers/db.ts
|
||
type CleanupFn = () => Promise<void> | void;
|
||
|
||
export function createCleanup() {
|
||
const queue: CleanupFn[] = [];
|
||
return {
|
||
track(fn: CleanupFn) { queue.push(fn); },
|
||
async run() {
|
||
while (queue.length > 0) {
|
||
const fn = queue.pop()!;
|
||
try { await fn(); }
|
||
catch (err) { console.warn('cleanup.run error:', err); }
|
||
}
|
||
},
|
||
};
|
||
}
|
||
```
|
||
|
||
### Usage typique
|
||
|
||
```typescript
|
||
const cleanup = createCleanup();
|
||
afterEach(async () => { await cleanup.run(); });
|
||
|
||
test('crée une tenue', async () => {
|
||
const tenue = await prisma.tenue.create({ data: { /* … */ } });
|
||
cleanup.track(async () => {
|
||
await prisma.tenue.deleteMany({ where: { id: tenue.id } });
|
||
});
|
||
// … assertions
|
||
});
|
||
```
|
||
|
||
### Checklist
|
||
|
||
- [ ] Ordre LIFO respecté (FK-safe)
|
||
- [ ] Aucun `prisma.*.deleteMany` direct hors `cleanup.track` dans les fichiers concernés
|
||
- [ ] Cleanup défensif (`try/catch + warn`) — pas de partial leak
|
||
- [ ] Aucune création (seulement des suppressions/restores) dans la queue
|
||
|
||
---
|
||
|
||
<a id="pattern-globalsetup-vitest-purge-residus"></a>
|
||
## Pattern : `globalSetup` vitest pour purger les résidus DB inter-runs
|
||
|
||
- Objectif : repartir d'une DB propre en début de suite quand des cleanups intra-test sont incomplets et que les artefacts s'accumulent entre sessions.
|
||
- Contexte : projet vitest avec `maxWorkers: 1` (DB partagée), tables 100 % alimentées par les tests (notifications, audit logs, payments éphémères).
|
||
- Quand l'utiliser : symptômes de "tests verts en isolation, rouges en suite complète après plusieurs jours" liés à des `findFirst({ type })` qui tombent sur des résidus.
|
||
- Quand l'éviter : si la suite tourne déjà avec une stratégie `transactions + rollback` ou DB-per-worker.
|
||
- Avantage :
|
||
- hook standard vitest, pas de cron externe ni de `prisma:reset` manuel entre sessions
|
||
- 1 seule passe au démarrage de la suite, coût négligeable
|
||
- Limites / vigilance :
|
||
- **NE PAS purger les tables seed** (users, soirees seed, profiles, etc.) — réservé aux tables 100 % "test-only"
|
||
- ne corrige pas la flakiness intra-run entre fichiers consécutifs
|
||
- Validé le : 25-04-2026
|
||
- Contexte technique : Vitest / Prisma — RL799_V2
|
||
|
||
### Implémentation
|
||
|
||
```typescript
|
||
// vitest.config.ts
|
||
test: {
|
||
maxWorkers: 1,
|
||
globalSetup: ['./src/__tests__/globalSetup.ts'],
|
||
}
|
||
|
||
// src/__tests__/globalSetup.ts
|
||
export default async function globalSetup() {
|
||
await prisma.notification.deleteMany();
|
||
await prisma.auditLog.deleteMany();
|
||
await prisma.cotisationPayment.deleteMany();
|
||
await prisma.$disconnect();
|
||
}
|
||
```
|
||
|
||
### Vérification avant d'ajouter une table
|
||
|
||
```bash
|
||
# Tables référencées par un snapshot/seed → NE PAS purger
|
||
grep -rln "prisma.<entity>.findMany\|seedSnapshot" src/__tests__/
|
||
```
|
||
|
||
### Checklist
|
||
|
||
- [ ] Aucune table seed dans la liste des `deleteMany`
|
||
- [ ] Hook référencé dans `vitest.config.ts`
|
||
- [ ] Justifier en commentaire pourquoi chaque table listée est test-only
|
||
|
||
---
|
||
|
||
<a id="pattern-template-database-isolation-fichiers"></a>
|
||
## Pattern : Template database Postgres pour isoler les fichiers de tests
|
||
|
||
- Objectif : éliminer la flakiness inter-fichiers en garantissant que chaque fichier de tests démarre avec une DB seedée pristine.
|
||
- Contexte : suite vitest qui partage une DB Postgres unique (cas typique : `maxWorkers: 1` ou DB unique côté CI).
|
||
- Quand l'utiliser : projet avec ≥50 fichiers de tests partageant une DB, présence de tests qui mutent les données seed sans restaurer systématiquement.
|
||
- Quand l'éviter : si les tests utilisent transactions+rollback (déjà isolés), DB-per-worker, ou si le rôle Postgres n'a pas le droit `CREATEDB`.
|
||
- Avantage :
|
||
- flakiness inter-fichiers éliminée par construction
|
||
- les tests ne polluent plus la DB de dev partagée
|
||
- réutilisation entre sessions : ~27 s au 1er run, < 1 s aux suivants
|
||
- Limites / vigilance :
|
||
- surcoût ~50 s sur 134 fichiers (cycle `drop + create FROM TEMPLATE` ~380 ms par fichier)
|
||
- les sub-processes (`prisma db seed`) doivent recevoir les envs critiques explicitement (`APP_BASE_URL`, `JWT_SECRET`, `ENCRYPTION_KEY`)
|
||
- Validé le : 01-05-2026
|
||
- Contexte technique : Prisma 7 / pg 8 / Postgres 17 / vitest 4 — RL799_V2
|
||
|
||
### Mécanique en 3 lots
|
||
|
||
**Lot 1 — DSN + primitives admin SQL** (`dbUrls.ts`, `dbAdmin.ts`) : `getAdminDsn()`, `getTemplateDsn()`, `getTestDsn()`, `databaseExists`, `dropDatabase` (`pg_terminate_backend` + `DROP IF EXISTS`), `createDatabase(name, { template })`. Whitelist stricte des noms de DB (les SQL admin ne supportent pas `$1`).
|
||
|
||
**Lot 2 — Bootstrap idempotent du template** (`bootstrapTemplate.ts`) :
|
||
|
||
```typescript
|
||
export async function ensureTemplateReady() {
|
||
if (await databaseExists(TEMPLATE)) return; // no-op si déjà là
|
||
await createDatabase(TEMPLATE);
|
||
spawnSync('pnpm', ['-C', 'apps/api', 'exec', 'prisma', 'migrate', 'deploy'], {
|
||
env: { ...process.env, DB_URL: getTemplateDsn() },
|
||
});
|
||
spawnSync('pnpm', ['-C', 'apps/api', 'exec', 'prisma', 'db', 'seed'], {
|
||
env: { ...process.env, DB_URL: getTemplateDsn() },
|
||
});
|
||
}
|
||
```
|
||
|
||
**Lot 3 — Câblage vitest** :
|
||
|
||
```typescript
|
||
// globalSetup.ts
|
||
export default async function globalSetup() {
|
||
await ensureTemplateReady();
|
||
if (await databaseExists(TEST)) await dropDatabase(TEST);
|
||
return async () => {
|
||
if (await databaseExists(TEST)) await dropDatabase(TEST);
|
||
};
|
||
}
|
||
|
||
// setupFile.ts (avant chaque fichier)
|
||
if (await databaseExists(TEST)) await dropDatabase(TEST);
|
||
await createDatabase(TEST, { template: TEMPLATE });
|
||
```
|
||
|
||
### Pièges anticipés
|
||
|
||
- **`resolveDbUrl()` qui force le pathname `/test`** : doit préserver le DSN s'il pointe déjà sur la template (sinon le seed sub-process écrit dans la mauvaise DB).
|
||
- **`prisma db seed` sub-process** : les envs critiques chargées par `lib/prisma.ts` côté app doivent être propagées au sub-process (`loadEnv.ts` importé en tête de `globalSetup.ts`).
|
||
- **`globalForPrisma` cache un PrismaClient** : c'est le **contenu** de la DB qui change (drop+create), pas le DSN — pas de `$disconnect()` nécessaire grâce à `pg_terminate_backend`.
|
||
|
||
### Checklist
|
||
|
||
- [ ] Whitelist stricte des noms de DB autorisés (anti-injection)
|
||
- [ ] Bootstrap idempotent (réutilise la template entre sessions de dev)
|
||
- [ ] Sub-processes du bootstrap reçoivent les envs critiques
|
||
- [ ] Drop + create FROM TEMPLATE au début de chaque fichier
|
||
- [ ] Test de flakiness avant/après pour valider le gain
|
||
|
||
---
|
||
|
||
<a id="pattern-helper-waitfor-fire-and-forget"></a>
|
||
## Pattern : Helper `waitForX()` polling-borné pour les attentes fire-and-forget
|
||
|
||
- Objectif : remplacer les `setTimeout(50) + findFirst` dispersés par un helper centralisé robuste à la charge CPU et auto-documenté.
|
||
- Contexte : tests qui valident un side-effect async (audit log, notification, mail log) déclenché par `setImmediate` / `Promise.resolve().then(...)` côté SUT.
|
||
- Quand l'utiliser : tout test qui attend qu'un side-effect non-awaited apparaisse en DB.
|
||
- Quand l'éviter : pour vérifier l'**absence** d'un event — là il faut un délai fixe puis assertion d'absence (le polling jusqu'au timeout ne prouve rien).
|
||
- Avantage :
|
||
- robuste aux variations de charge (pas de durée arbitraire)
|
||
- fail rapide si l'event n'arrive pas (timeout 1500 ms par défaut)
|
||
- lisibilité : intention claire (`waitForX` vs `setTimeout` + `findFirst`)
|
||
- Limites / vigilance :
|
||
- le polling consomme des requêtes DB (1 toutes les 50 ms) — négligeable en `maxWorkers: 1`
|
||
- **NE PAS** utiliser pour vérifier l'absence d'un event
|
||
- Validé le : 25-04-2026
|
||
- Contexte technique : Vitest / Prisma — RL799_V2
|
||
|
||
### Implémentation
|
||
|
||
```typescript
|
||
// __tests__/helpers/asyncWait.ts
|
||
export const waitForAudit = async (
|
||
query: { action: string; targetId?: string; userId?: string },
|
||
options: { timeoutMs?: number; intervalMs?: number } = {},
|
||
) => {
|
||
const timeout = options.timeoutMs ?? 1500;
|
||
const interval = options.intervalMs ?? 50;
|
||
const deadline = Date.now() + timeout;
|
||
while (Date.now() < deadline) {
|
||
const audit = await prisma.auditLog.findFirst({ where: query });
|
||
if (audit) return audit;
|
||
await new Promise((r) => setTimeout(r, interval));
|
||
}
|
||
return null;
|
||
};
|
||
```
|
||
|
||
### Anti-pattern
|
||
|
||
```typescript
|
||
// ❌ délai arbitraire et fragile
|
||
await new Promise((r) => setTimeout(r, 50));
|
||
const notif = await prisma.notification.findFirst({ where: { ... } });
|
||
|
||
// ✅ polling borné, intention claire
|
||
const notif = await waitForNotification({ type: 'X', recipientId: userId });
|
||
```
|
||
|
||
### Checklist
|
||
|
||
- [ ] Filtre exhaustif (au minimum `recipientId` ou `targetId` pour éviter de matcher les artefacts d'un autre test)
|
||
- [ ] Timeout par défaut court (1500 ms)
|
||
- [ ] Migration progressive — pas tous les tests d'un coup
|
||
|
||
### Pattern symétrique pour vérifier l'ABSENCE d'un side-effect
|
||
|
||
`waitForX` ne convient pas pour prouver qu'**aucun** event n'apparaît : le polling jusqu'au timeout ne distingue pas "aucun event" de "event arrivé après le timeout". Le remplacer par `setTimeout(100) + count()` souffre d'une double fragilité (trop court en CI lent → faux négatif si l'event fuyant arrive après ; trop long → temps gaspillé).
|
||
|
||
Le helper symétrique correct est un polling-borné **fail-fast** (`assertCountStable`) : il poll-vérifie que le compteur reste à la valeur attendue sur une fenêtre courte, et abandonne **dès** qu'un compteur surnuméraire est observé.
|
||
|
||
```typescript
|
||
// __tests__/helpers/asyncWait.ts
|
||
export const assertNotificationCountStable = async (
|
||
query: { type: NotificationType; recipientId?: string },
|
||
expected: number,
|
||
options: { windowMs?: number; intervalMs?: number } = {},
|
||
): Promise<number> => {
|
||
const window = options.windowMs ?? 200;
|
||
const interval = options.intervalMs ?? 50;
|
||
const deadline = Date.now() + window;
|
||
|
||
let count = await prisma.notification.count({ where: query });
|
||
if (count > expected) return count; // fail-fast immédiat
|
||
|
||
while (Date.now() < deadline) {
|
||
await new Promise((r) => setTimeout(r, interval));
|
||
count = await prisma.notification.count({ where: query });
|
||
if (count > expected) return count; // fail-fast
|
||
}
|
||
return count;
|
||
};
|
||
```
|
||
|
||
```typescript
|
||
// ❌ délai arbitraire — trop court en CI lent (faux négatif), trop long = temps perdu
|
||
await new Promise((r) => setTimeout(r, 100));
|
||
expect(await prisma.notification.count({ where: { /* … */ } })).toBe(1);
|
||
|
||
// ✅ polling borné, fail-fast si une notif fuyante apparaît
|
||
const count = await assertNotificationCountStable({ type: 'X', recipientId: userId }, 1);
|
||
expect(count).toBe(1);
|
||
```
|
||
|
||
Checklist (absence) :
|
||
|
||
- [ ] Fenêtre courte (200 ms par défaut — durée typique d'un fire-and-forget non-désiré, pas un timeout)
|
||
- [ ] Filtre exhaustif — `recipientId` ou `targetId` au minimum
|
||
- [ ] Le test affirme `count === expected` APRÈS le helper, pas pendant
|
||
|
||
Cas vécu : `PATCH/DELETE payments → aucune nouvelle notif` (`cotisationsPayments.test.ts`), 05-05-2026.
|
||
|
||
---
|
||
|
||
<a id="pattern-injection-dependance-hooks-module-level"></a>
|
||
## Pattern : Injection de dépendance testable via hooks module-level `__setXForTests` / `__resetXForTests`
|
||
|
||
- Objectif : surcharger en test une dépendance d'un module de prod (getter de clés JWKS, sub-resolver DB, …) SANS polluer la signature publique (param de DI) ni recourir à `vi.mock` (fragile sur les imports transitifs, hoisting capricieux).
|
||
- Contexte : module dont une dépendance interne doit varier en test mais dont la signature publique est un invariant ("signatures intouchables").
|
||
- Quand l'utiliser : la dépendance est interne et `vi.mock` se révèle fragile (chaîne d'imports transitifs).
|
||
- Quand l'éviter : la dépendance est déjà un paramètre explicite de la fonction (param-threading suffit) ; ou un `vi.mock` simple et stable fait l'affaire.
|
||
- Avantage :
|
||
- pas de param de DI dans le contrat public, pas de `vi.mock` fragile
|
||
- bonus diagnostic : injecter un fake qui **throw si appelé** prouve qu'un chemin n'est PAS pris (ex. "le sub-resolver Keycloak ne doit pas être appelé quand le JWT-maison gagne")
|
||
- Limites / vigilance :
|
||
- **reset systématique dans `setupFile.ts` avant chaque fichier** (même garde-fou que les rate-limiters in-memory : un fichier qui injecte ne doit pas polluer le suivant → flakiness inter-fichiers évitée)
|
||
- le défaut de PRODUCTION reste le comportement réel (ex. stub fail-closed `() => null` tant que la vraie impl n'existe pas)
|
||
- Validé le : 14-06-2026
|
||
- Contexte technique : Vitest — RL799_V2 (rate-limiters, puis Keycloak K1.1)
|
||
|
||
### Implémentation
|
||
|
||
```typescript
|
||
// module de prod
|
||
let active = productionDefault;
|
||
|
||
export function __setXForTests(v: typeof productionDefault): void { active = v; }
|
||
export function __resetXForTests(): void { active = productionDefault; }
|
||
```
|
||
|
||
```typescript
|
||
// setupFile.ts — reset AVANT chaque fichier
|
||
beforeEach(() => {
|
||
__resetXForTests();
|
||
__resetAllRateLimitersForTests();
|
||
});
|
||
```
|
||
|
||
Cas vécus : `__resetAllRateLimitersForTests` (`rateLimiter.ts`), puis `__setKeycloakKeyGetterForTests` / `__setSubResolverForTests` (K1.1).
|
||
|
||
---
|
||
|
||
<a id="pattern-e2e-db-based-nestjs-prisma-v7"></a>
|
||
## Pattern : e2e DB-based NestJS + Prisma v7 (Jest + @swc/jest)
|
||
|
||
- Objectif : exécuter des e2e Jest contre une vraie base Postgres (sans mocker `PrismaClient`) dans un projet NestJS utilisant Prisma v7.
|
||
- Contexte : Prisma v7 charge dynamiquement un runtime WASM (`await import('....mjs')`, `import.meta.url`) → incompatible avec la config Jest historique `ts-jest` + `moduleNameMapper` qui mock le client.
|
||
- Quand l'utiliser : projet NestJS + Prisma v7 voulant des e2e avec persistance réelle (validation de contracts Zod réels, fixtures partagées avec le seed, intégrité cross-modules).
|
||
- Quand l'éviter : suites qui n'ont pas besoin d'une vraie DB → garder le pipeline mocké (plus rapide, plus isolé).
|
||
- Avantage :
|
||
- exploite des builders typés contre une vraie base
|
||
- pattern inter-projet : tout NestJS + Prisma v7 rencontre le même problème
|
||
- Limites / vigilance :
|
||
- garde-fou anti-truncate destructeur obligatoire (refuser si `DATABASE_URL` ne contient pas "test")
|
||
- **PAS** de mock global `argon2` (sinon le hash est stub et les tests d'auth ne valident rien)
|
||
- Validé le : 27-05-2026
|
||
- Contexte technique : NestJS / Jest / @swc/jest / Prisma v7 / Postgres — app-alexandrie
|
||
|
||
### Config Jest e2e DB-based minimale
|
||
|
||
```json
|
||
{
|
||
"moduleFileExtensions": ["js", "mjs", "json", "ts"],
|
||
"testRegex": ".e2e-db-spec.ts$",
|
||
"setupFiles": ["<rootDir>/test-env-db.ts"],
|
||
"transformIgnorePatterns": ["/node_modules/(?!(@prisma)/)"],
|
||
"transform": {
|
||
"^.+\\.(t|j|mj)s$": ["@swc/jest", {
|
||
"jsc": {
|
||
"target": "es2023",
|
||
"parser": { "syntax": "typescript", "decorators": true, "dynamicImport": true },
|
||
"transform": { "legacyDecorator": true, "decoratorMetadata": true },
|
||
"keepClassNames": true
|
||
},
|
||
"module": { "type": "commonjs" }
|
||
}]
|
||
},
|
||
"moduleNameMapper": { "^(\\.{1,2}/.*)\\.js$": "$1" }
|
||
}
|
||
```
|
||
|
||
Points-clés :
|
||
|
||
- **`@swc/jest`** au lieu de `ts-jest` : gère `import.meta.url`, `await import()` dynamique, et respecte `decoratorMetadata` (DI Nest).
|
||
- **`transformIgnorePatterns`** ouvert pour `/node_modules/@prisma/` : les `.mjs` du runtime WASM doivent passer dans le transformer.
|
||
- **`moduleNameMapper`** strip les extensions `.js` : Prisma v7 écrit ses imports en ESM strict (`./internal/class.js`), Jest CJS résout `.ts`.
|
||
- **PAS** de `moduleNameMapper` mappant `PrismaClient` vers un mock (sinon on tape le mock, pas la vraie DB).
|
||
|
||
### Helper e2e avec garde-fou anti-truncate
|
||
|
||
```typescript
|
||
// _helpers/e2e-db.ts
|
||
export async function truncateE2EDb(prisma: PrismaClient): Promise<void> {
|
||
const url = process.env.DATABASE_URL ?? '';
|
||
if (!url.includes('test')) {
|
||
throw new Error('[e2e-db] truncate refusé : DATABASE_URL ne contient pas "test"');
|
||
}
|
||
await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE;`);
|
||
}
|
||
```
|
||
|
||
Infra (à provisionner une fois) : DB dédiée avec "test" dans le nom + `DATABASE_URL=...test pnpm exec prisma migrate deploy`.
|
||
|
||
### Cohabitation avec les e2e mockés historiques
|
||
|
||
- Garder le pipeline mocké pour les suites sans besoin de vraie DB.
|
||
- Migrer progressivement vers le DB-based les e2e qui bénéficient d'une vraie persistance.
|
||
- Convention de nommage : `*.e2e-spec.ts` (mockés) vs `*.e2e-db-spec.ts` (DB-based) — extension distinctive captée par les `testRegex` respectifs.
|
||
|
||
---
|
||
|
||
<a id="pattern-test-atomicite-transaction"></a>
|
||
## Pattern : Tester l'atomicité d'une transaction (audit + mutation)
|
||
|
||
- Objectif : prouver par un test que la promesse "X est garanti par une transaction" tient — sans ce test, un refactor "fire-and-forget" passe en review sans alarme.
|
||
- Contexte : code de service qui revendique `mutation persistée ⇔ audit log existe` via `prisma.$transaction(async tx => { await tx.X.update(...); await logActionSync(tx, ...); })`.
|
||
- Quand l'utiliser : sur tout chemin critique où l'audit ou un side-effect transactionnel est un livrable métier.
|
||
- Quand l'éviter : pour les audits purement informatifs (statistiques d'usage) où le fire-and-forget est acceptable.
|
||
- Avantage :
|
||
- le test fait office de contrat exécutable du pattern transactionnel
|
||
- une régression silencieuse (passer `prisma` au lieu de `tx`) casse le test immédiatement
|
||
- Limites / vigilance :
|
||
- le mock doit cibler le seul appel ciblé (pas un mock global qui fait tout throw)
|
||
- Validé le : 27-04-2026
|
||
- Contexte technique : Vitest / Prisma — RL799_V2
|
||
|
||
### Implémentation (recipe vitest + spyOn)
|
||
|
||
```typescript
|
||
import * as auditRepository from '../repositories/admin/auditRepository';
|
||
|
||
test('mutation rollback si audit throw', async () => {
|
||
vi.spyOn(auditRepository, 'createAuditLog').mockImplementation(async (data) => {
|
||
if (data.action === 'X.cross_soiree_update') {
|
||
throw new Error('audit-failure-simulee');
|
||
}
|
||
return null as never;
|
||
});
|
||
|
||
const res = await POST_HANDLER(...);
|
||
assert.ok(res.status >= 500);
|
||
assert.equal(await prisma.X.findFirst(...), null); // rollback
|
||
assert.equal(await prisma.auditLog.count(...), 0);
|
||
});
|
||
```
|
||
|
||
### Checklist
|
||
|
||
- [ ] Mock chirurgical sur l'action ciblée, pas un mock global
|
||
- [ ] Assertions sur **trois** invariants : status HTTP 5xx, donnée non persistée, audit non écrit
|
||
- [ ] Restaurer le mock après le test (`vi.restoreAllMocks()` en `afterEach`)
|
||
|
||
---
|
||
|
||
<a id="pattern-describe-convention-tests-integration"></a>
|
||
## Pattern : Convention `describe()` minimale pour tests d'intégration
|
||
|
||
- Objectif : rendre les fichiers de tests > 150 LOC navigables, regroupables par cause d'échec et lisibles dans le reporter.
|
||
- Contexte : tests d'intégration API où les assertions s'enchaînent sans regroupement, ou fichiers historiquement plats.
|
||
- Quand l'utiliser : tout fichier > 150 LOC ou > 5 tests.
|
||
- Quand l'éviter : fichier court (< 50 LOC, < 3 tests) où le `describe` ajoute du bruit.
|
||
- Avantage :
|
||
- le reporter vitest groupe les échecs logiquement (toutes les erreurs d'auth ensemble)
|
||
- navigation facilitée par recherche (`describe('Auth'`)
|
||
- un groupe devient candidat naturel à l'extraction en fichier dédié
|
||
- Limites / vigilance :
|
||
- 2 niveaux maximum — au-delà ça devient illisible
|
||
- Validé le : 24-04-2026
|
||
- Contexte technique : Vitest — RL799_V2
|
||
|
||
### Convention recommandée (2 niveaux)
|
||
|
||
```typescript
|
||
describe('POST /api/tenues', () => {
|
||
describe('Auth', () => {
|
||
test('refuse un user non-admin → 403', async () => { /* … */ });
|
||
test('refuse un token invalide → 401', async () => { /* … */ });
|
||
});
|
||
describe('Validation', () => {
|
||
test('refuse un body sans date → 400', async () => { /* … */ });
|
||
});
|
||
describe('Happy path', () => {
|
||
test('crée une tenue valide → 201', async () => { /* … */ });
|
||
});
|
||
});
|
||
```
|
||
|
||
### Seuils
|
||
|
||
- < 50 LOC et < 3 tests : optionnel
|
||
- 50–150 LOC : 1 niveau de contexte
|
||
- \> 150 LOC ou > 5 tests : 2 niveaux obligatoires
|
||
|
||
### Niveau 2 — vocabulaire stable
|
||
|
||
`Auth`, `Validation`, `Happy path`, `Edge cases`, `Error handling`, `Integration`. Ne pas inventer un nom local — la cohérence inter-fichiers vaut plus que l'expressivité ponctuelle.
|
||
|
||
---
|
||
|
||
<a id="pattern-refactor-iteratif-tests-monolithe"></a>
|
||
## Pattern : Refactor itératif d'un fichier de tests monolithe
|
||
|
||
- Objectif : découper un fichier > 1000 LOC mêlant plusieurs domaines métier en fichiers thématiques sans introduire de régression.
|
||
- Contexte : fichier de tests historique qui partage un `beforeEach` lourd, n'a pas de `describe`, et accueille tous les nouveaux tests par défaut.
|
||
- Quand l'utiliser : > 1000 LOC, plusieurs domaines mélangés, navigation pénible.
|
||
- Quand l'éviter : fichier ciblé, déjà cohérent même volumineux.
|
||
- Avantage :
|
||
- 1 commit par domaine extrait → review possible commit par commit
|
||
- helpers réutilisables émergent naturellement
|
||
- reporter vitest groupe les échecs par domaine
|
||
- Limites / vigilance :
|
||
- flakiness inter-fichiers possible pendant la transition (cleanups incomplets temporaires)
|
||
- tolérer 1-2 fails au 1er run pendant le chantier, investiguer après
|
||
- Validé le : 25-04-2026
|
||
- Contexte technique : Vitest — RL799_V2
|
||
|
||
### Étapes (ordre obligatoire)
|
||
|
||
1. **Analyse** : `grep -nE "^test|^// ---" fichier.test.ts` → carte des sections + repérage des dépendances closure.
|
||
2. **Extraction des helpers d'abord** : `helpers/<domaine>.ts` avec `setup<Domaine>Fixtures(cleanup)` qui retourne explicitement les IDs (pas de closure module-level).
|
||
3. **POC sur le domaine le plus simple** (4-7 tests, peu de dépendances). Commit + push dès que vert.
|
||
4. **Itération domaine par domaine** par ordre croissant de complexité. Une seule règle : ne jamais commencer un nouveau domaine sans avoir commit le précédent.
|
||
5. **Suppression finale du fichier monolithe** quand le dernier domaine est extrait.
|
||
|
||
### Pièges à éviter
|
||
|
||
- **Ne jamais dupliquer le fichier** pour le scinder : toujours **déplacer** (extraction → suppression dans l'original → commit).
|
||
- **Closure variables partagées** : si les tests utilisent `let X = ''` au niveau module, les ids doivent passer par le retour de `setup<Domaine>`.
|
||
- **Snapshot/restore seed** : si le fichier original prenait un snapshot d'entries seed, **chaque** nouveau fichier doit le faire (sinon le 1er fichier qui tourne snapshot un état déjà pollué).
|
||
|
||
### Checklist
|
||
|
||
- [ ] Helpers extraits **avant** le 1er domaine
|
||
- [ ] 1 commit par domaine
|
||
- [ ] `wc -l` du fichier original baisse à chaque commit (preuve de progrès)
|
||
- [ ] Suite verte à chaque commit
|