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>
244 lines
13 KiB
Markdown
244 lines
13 KiB
Markdown
---
|
||
title: Backend — Risques & vigilance : Tests
|
||
domain: backend
|
||
bucket: risques
|
||
tags: [tests, vitest, isolation, env-vars, flakiness]
|
||
applies_to: [analysis, implementation, review, debug]
|
||
severity: high
|
||
validated_on: 2026-05-02
|
||
source_projects: [RL799_V2]
|
||
---
|
||
|
||
# Backend — Risques & vigilance : Tests
|
||
|
||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
||
|
||
---
|
||
|
||
<a id="risque-vi-stubenv-sans-restauration"></a>
|
||
## `vi.stubEnv` sans restauration — fuite env vars inter-fichiers
|
||
|
||
### Risques
|
||
|
||
- Un test qui stub une env var dans `beforeAll` sans `vi.unstubAllEnvs()` en `afterAll` affecte silencieusement tous les fichiers exécutés après lui dans le même process
|
||
- En séquentiel (`maxWorkers: 1`), l'ordre est déterministe et la fuite est invisible — la suite passe au vert
|
||
- En passant à `maxWorkers > 1`, les env vars stubbées sont partagées entre workers → tests imprévisibles
|
||
|
||
### Symptômes
|
||
|
||
- Tests qui passent en isolation mais échouent dans la suite complète, ou inversement
|
||
- Comportement d'un endpoint qui dépend d'une env définie dans un fichier qui n'a rien à voir
|
||
- Migration de `maxWorkers: 1` vers `maxWorkers: 4` qui rouge la suite d'un coup
|
||
|
||
### Bonnes pratiques / mitigations
|
||
|
||
```typescript
|
||
beforeAll(() => {
|
||
vi.stubEnv('RESEND_API_KEY', 'test-key');
|
||
});
|
||
afterAll(() => {
|
||
vi.unstubAllEnvs();
|
||
});
|
||
|
||
// Variante encore plus robuste (isolation parfaite par test) :
|
||
beforeEach(() => { vi.stubEnv('X', 'y'); });
|
||
afterEach(() => { vi.unstubAllEnvs(); });
|
||
```
|
||
|
||
- Détection : `rg "vi\.stubEnv\(" __tests__ | wc -l` doit être ≤ `rg "vi\.unstubAllEnvs\(\)" __tests__ | wc -l` regroupé par fichier
|
||
- Avant toute migration vers `maxWorkers > 1` : sweep complet `stubEnv` / `unstubAllEnvs`
|
||
|
||
- Contexte technique : Vitest — RL799_V2 24-04-2026
|
||
|
||
---
|
||
|
||
<a id="risque-maxworkers-1-masque-isolation"></a>
|
||
## `maxWorkers: 1` masque les problèmes d'isolation
|
||
|
||
### Risques
|
||
|
||
- L'exécution séquentielle cache systématiquement tous les bugs d'isolation : `vi.stubEnv` non restaurée, mutations de seed non restaurées, `deleteMany` direct, compteurs globaux non resets
|
||
- La CI actuelle ne peut pas détecter ces problèmes → faux sentiment de sécurité
|
||
- Le jour où on veut paralléliser pour gagner du temps, on découvre une dizaine de bugs d'isolation simultanément
|
||
|
||
### Symptômes
|
||
|
||
- CI verte en `maxWorkers: 1`, rouge dès `maxWorkers > 1`
|
||
- Tests "verts depuis 6 mois" qui rougent soudainement après un changement de config
|
||
- Pas de détection possible avant le passage en parallèle
|
||
|
||
### Bonnes pratiques / mitigations
|
||
|
||
- Tester la parallélisation tôt, même si la suite est petite — passer `maxWorkers: 2` force l'équipe à écrire des tests isolés
|
||
- Si on hérite d'un projet en `maxWorkers: 1`, ne pas migrer d'un coup. Audit ciblé d'abord :
|
||
- `grep "vi\.stubEnv\(" / "vi\.unstubAllEnvs\(" / "deleteMany\(" / "TEST_USER\."` pour repérer les patterns suspects
|
||
- Ajouter un audit "hidden_by_serial_execution" en review de tests : lister les patterns qui marcheraient aujourd'hui mais casseraient en parallèle
|
||
- Heuristique : projet > 200 tests → impératif de passer en parallèle (sinon CI > 15 min = friction dev majeure)
|
||
|
||
- Contexte technique : Vitest — RL799_V2 24-04-2026
|
||
|
||
---
|
||
|
||
<a id="risque-flakiness-inter-fichiers-db-partagee"></a>
|
||
## Flakiness inter-fichiers vitest avec DB partagée
|
||
|
||
### Risques
|
||
|
||
- Un fichier de tests laisse des artefacts résiduels en DB que le fichier suivant ne s'attend pas à voir : audits orphelins, notifications, entries seed mutées, rate-limiters non resets
|
||
- Le pattern se "résout" au 2e run par chance (le `beforeEach` finit par nettoyer par effet de bord), donnant une fausse confiance
|
||
- En CI, un retry automatique masque la vraie cause
|
||
|
||
### Symptômes
|
||
|
||
- 2-4 tests rouges au 1er run, vert au 2e run sans aucune modification
|
||
- `vitest run <fichier-isolé>` vert, suite complète rouge
|
||
- Compteurs `count({ type: 'X' })` qui tombent sur des résidus d'anciens tests
|
||
|
||
### Bonnes pratiques / mitigations
|
||
|
||
**Diagnostic** :
|
||
```bash
|
||
# 1. Run isolé sur le fichier suspect
|
||
pnpm -C apps/api test mon-fichier
|
||
# 2. 2 runs consécutifs de la suite complète
|
||
pnpm -C apps/api test && pnpm -C apps/api test
|
||
# Si 1er rouge / 2e vert → flakiness inter-fichiers
|
||
```
|
||
|
||
**Stratégies par horizon** :
|
||
- **Court terme** : accepter comme dette connue si le 2e run est stable, documenter dans le commit message
|
||
- **Moyen terme** : identifier le fichier qui pollue, ajouter le cleanup manquant dans son `afterEach`
|
||
- **Long terme** : DB-per-worker ou `transactions + rollback` (chantier d'infra dédié, voir `knowledge/backend/patterns/tests.md` pattern template database)
|
||
|
||
**À ne PAS faire** :
|
||
- Ajouter des `setTimeout` pour "attendre que ça se stabilise"
|
||
- Wrapper les assertions dans des try/catch silencieux
|
||
- Marquer les tests `.skip`
|
||
|
||
**Heuristique gravité** :
|
||
- 1 fail intermittent toutes les 5 runs : acceptable temporairement
|
||
- 1+ fail systématique au 1er run, vert au 2e : à diagnostiquer mais pas urgent
|
||
- Fails aléatoires différents à chaque run : urgent (state corruption)
|
||
|
||
- Contexte technique : Vitest / Prisma — RL799_V2 25-04-2026
|
||
|
||
---
|
||
|
||
<a id="risque-test-non-regression-rbac-guards-reels"></a>
|
||
## Test de non-régression d'accès (RBAC) qui ré-encode la table de rôles au lieu d'invoquer les guards réels
|
||
|
||
### Risques
|
||
|
||
- Un test "filet anti-régression" donne un faux sentiment de sécurité s'il teste une projection inerte de la règle au lieu de la règle appliquée
|
||
- Cas vécu RL799 : un test "snapshot d'accès" conçu comme "pour chaque rôle, quels sets de rôles le couvrent" ne dépendait QUE du fichier de définition des sets — il restait vert même si un guard ou un call-site changeait (le vrai risque d'une refonte RBAC). La revue adversariale l'a qualifié de "faux filet"
|
||
|
||
### Symptômes
|
||
|
||
- Le test ne peut PAS échouer si la régression qu'il prétend protéger se produit
|
||
- Question de détection : "ce test peut-il échouer si la régression que je crains arrive ?" — si non, c'est un faux filet
|
||
|
||
### Bonnes pratiques / mitigations
|
||
|
||
- Un test censé protéger contre un changement X doit exercer le chemin où X s'applique RÉELLEMENT, pas une reformulation parallèle de la règle
|
||
- Correctif type : matrice (rôle représentatif × handler représentatif), token forgé par rôle, **appel du handler/guard réel**, assertion `200`/`403`
|
||
- Ne jamais ré-encoder la table de rôles dans le test : invoquer les guards/handlers de prod
|
||
|
||
- Contexte technique : RBAC / API HTTP — RL799_V2
|
||
|
||
---
|
||
|
||
<a id="risque-prefixe-fixture-unique-par-fichier"></a>
|
||
## Préfixe de fixture de test partagé entre fichiers — cleanup qui efface la fixture d'un voisin
|
||
|
||
### Risques
|
||
|
||
- Deux fichiers de tests écrivant dans la même table avec le MÊME préfixe (`RI-TEST-`), dont l'un fait un filet `deleteMany({ ref: { startsWith: 'RI-TEST-' } })` en `afterEach`
|
||
- Vitest pouvant entrelacer deux fichiers sur un même worker, ce filet peut effacer la fixture VIVANTE de l'autre fichier → flakiness intermittente non déterministe
|
||
- Bug LATENT de profil "iceberg" : les deux fichiers passent en isolation et même ensemble la plupart du temps, fail sporadique en CI (ex. 404 sur le merge) difficile à diagnostiquer
|
||
|
||
### Symptômes
|
||
|
||
- 404 / "introuvable" sporadique sur une fixture que le test croyait avoir créée
|
||
- Deux fichiers `grep`-ables sur le même littéral de préfixe
|
||
|
||
### Bonnes pratiques / mitigations
|
||
|
||
- **Préfixe de fixture = UNIQUE par fichier, jamais par domaine/table** : `RI-CRUD-` vs `RI-MERGE-`, pas `RI-TEST-` partagé
|
||
- Règle générale : un cleanup de test ne doit JAMAIS supprimer au-delà de ce que CE fichier a créé — ni `rm` une racine disque partagée, ni `deleteMany` un pattern qu'un autre fichier peut matcher, ni réutiliser un id du seed
|
||
- Détection : `grep -rln "<PREFIX>" <test-dir>` doit retourner UN seul fichier par préfixe
|
||
|
||
- Contexte technique : Vitest / Prisma — RL799_V2 22-06-2026
|
||
|
||
---
|
||
|
||
<a id="risque-test-collision-fichier-versionne"></a>
|
||
## Test qui écrit/supprime un fichier collisionnant avec un artefact versionné
|
||
|
||
### Risques
|
||
|
||
- Un test pose une fixture via `writeFile` puis la supprime dans son `finally`, mais en ciblant le nom d'un fichier SEED VERSIONNÉ dans git (ex. `seed-planches-architecture-apprenti.pdf`)
|
||
- À chaque run de la suite, le fichier seed disparaît du working tree (`git status` → `D`), polluant tous les diffs et risquant d'être commité par erreur
|
||
|
||
### Symptômes
|
||
|
||
- Un fichier suivi par git réapparaît en `D` (supprimé) dans `git status` après l'exécution d'une suite de tests, sans qu'aucun code applicatif ne le touche
|
||
- Déroutant : le coupable est un test, pas le code en cours de dev
|
||
|
||
### Bonnes pratiques / mitigations
|
||
|
||
- Les fixtures posées sur disque portent un nom JETABLE non versionné (préfixe `_fixture-`, `tmp-`, ou sous-dossier `__fixtures__/` git-ignoré), JAMAIS le nom d'un fichier seed/asset commité
|
||
- Vérifier : `git ls-files <dir>` liste les fichiers versionnés — aucun nom de fixture de test ne doit y figurer
|
||
- Détection rapide : si `git status` montre une suppression `D` inattendue d'un seed/asset après un run de tests, `grep` le nom exact dans `__tests__/` → le test qui le référence avec un `rm`/`removeFixture` en `finally` est le coupable
|
||
|
||
- Contexte technique : Vitest / filesystem — RL799_V2 23-06-2026
|
||
|
||
---
|
||
|
||
<a id="risque-test-singleton-module-level-env"></a>
|
||
## Test consommant un singleton module-level dépendant de l'env — passe "par accident" selon l'ordre des fichiers
|
||
|
||
### Risques
|
||
|
||
- Un test qui consomme un singleton module-level configuré par l'environnement (transport SMTP, client API, pool DB mémoïsé) sans stubber son propre env ET reset le singleton passe "par accident" selon l'ordre d'exécution des fichiers
|
||
- Le singleton (`let transport = null` mémoïsé au 1er `getTransport()`) est partagé entre fichiers d'un même worker : un fichier A pose `SMTP_HOST` + construit le transport, un fichier B qui ne configure RIEN réutilise le transport de A → B "marche" tant que A tourne avant lui
|
||
- Corollaire : un reset de singleton (`__resetXxxForTests`) placé dans le SETUP GLOBAL via import statique court-circuite les `vi.mock` des autres fichiers — l'`import` charge le module (et sa chaîne, ex. `smtpTransport → nodemailer`) AVANT que les `vi.mock` propres à chaque fichier soient hoistés → module figé sur la vraie dépendance
|
||
|
||
### Symptômes
|
||
|
||
- Test vert en local, rouge en CI (ou l'inverse) sans changement de code ; `sentCount === 0` au lieu de N, ou appel réseau réel malgré un mock
|
||
- Ajouter un reset "utile" au setup global casse des dizaines de tests sans rapport (`'failed' !== 'sent'`)
|
||
|
||
### Bonnes pratiques / mitigations
|
||
|
||
- Tout fichier qui exerce le singleton doit être AUTO-SUFFISANT : stubber l'env requis dans `beforeAll` (`vi.stubEnv('SMTP_HOST', …)` + `vi.unstubAllEnvs()` en `afterAll`) ET reset le singleton dans `beforeEach` (`__resetXxxForTests()`), au POINT D'USAGE (où son propre `vi.mock` est actif), jamais dans le setup global
|
||
- Ne jamais s'appuyer sur l'état laissé par un fichier voisin
|
||
- Un reset de singleton va dans le `beforeEach` du/des fichier(s) qui en ont besoin, PAS dans le setup global ; si vraiment transverse, l'importer en différé (`await import('@/lib/...')`) au point d'usage
|
||
- Le `dryRun`/mode test d'un service ne dérive PAS forcément de `NODE_ENV === 'test'` — vérifier la VRAIE condition (souvent une var dédiée, ex. `MAIL_DRY_RUN === 'true'`)
|
||
- Après tout changement touchant le fichier de setup, rejouer la suite COMPLÈTE (effet iceberg)
|
||
|
||
- Contexte technique : Vitest — RL799_V2 23-06-2026
|
||
|
||
---
|
||
|
||
<a id="risque-test-rate-limit-rang-hardcode"></a>
|
||
## Test de rate-limit qui hardcode le rang exact de la requête bloquée
|
||
|
||
### Risques
|
||
|
||
- Un test qui hardcode "la Ne requête déclenche le 429" casse silencieusement quand un flag d'environnement (E2E) relève la limite
|
||
- La limite effective dépend d'un flag (`process.env.E2E === '1' ? 1000 : 20`), mais le test fige le nombre d'itérations
|
||
|
||
### Symptômes
|
||
|
||
- `for (i=0;i<21;i++) ...; expect(last.status).toBe(429)` passe en local (limite=20) mais casse sous E2E=1 (limite relevée à 1000) — jamais de 429 atteint, rouge en CI E2E
|
||
- À l'inverse, un test calibré sur la limite E2E serait trop lent/inutile en local
|
||
|
||
### Bonnes pratiques / mitigations
|
||
|
||
- Boucler jusqu'au PREMIER 429 avec un plafond de sécurité couvrant le régime le plus permissif (`SAFETY_CAP > limite E2E`)
|
||
- Asserter (a) qu'un 429 a bien été atteint avant le plafond, (b) qu'au moins une requête est passée avant le blocage
|
||
- Le test reste correct quelle que soit la limite effective et survit à un ajustement de capacité
|
||
- Alternative : exposer la limite (getter) et dériver le nombre d'itérations — mais boucler-jusqu'au-429 évite de changer l'API de prod pour un test
|
||
|
||
- Contexte technique : rate-limit / API HTTP — RL799_V2 23-06-2026
|