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>
13 KiB
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.mdpour l'index complet.
vi.stubEnv sans restauration — fuite env vars inter-fichiers
Risques
- Un test qui stub une env var dans
beforeAllsansvi.unstubAllEnvs()enafterAllaffecte 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: 1versmaxWorkers: 4qui rouge la suite d'un coup
Bonnes pratiques / mitigations
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 -ldoit être ≤rg "vi\.unstubAllEnvs\(\)" __tests__ | wc -lregroupé par fichier -
Avant toute migration vers
maxWorkers > 1: sweep completstubEnv/unstubAllEnvs -
Contexte technique : Vitest — RL799_V2 24-04-2026
maxWorkers: 1 masque les problèmes d'isolation
Risques
- L'exécution séquentielle cache systématiquement tous les bugs d'isolation :
vi.stubEnvnon restaurée, mutations de seed non restaurées,deleteManydirect, 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èsmaxWorkers > 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: 2force 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
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
beforeEachfinit 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 :
# 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é, voirknowledge/backend/patterns/tests.mdpattern template database)
À ne PAS faire :
- Ajouter des
setTimeoutpour "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
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
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 filetdeleteMany({ ref: { startsWith: 'RI-TEST-' } })enafterEach - 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-vsRI-MERGE-, pasRI-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
rmune racine disque partagée, nideleteManyun 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
Test qui écrit/supprime un fichier collisionnant avec un artefact versionné
Risques
- Un test pose une fixture via
writeFilepuis la supprime dans sonfinally, 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é) dansgit statusaprè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 statusmontre une suppressionDinattendue d'un seed/asset après un run de tests,greple nom exact dans__tests__/→ le test qui le référence avec unrm/removeFixtureenfinallyest le coupable -
Contexte technique : Vitest / filesystem — RL799_V2 23-06-2026
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 = nullmémoïsé au 1ergetTransport()) est partagé entre fichiers d'un même worker : un fichier A poseSMTP_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 lesvi.mockdes autres fichiers — l'importcharge le module (et sa chaîne, ex.smtpTransport → nodemailer) AVANT que lesvi.mockpropres à 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 === 0au 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()enafterAll) ET reset le singleton dansbeforeEach(__resetXxxForTests()), au POINT D'USAGE (où son proprevi.mockest 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
beforeEachdu/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 deNODE_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
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