# Qualité, tests et CI/CD ## Le modèle en une phrase Coolify redéploie sur webhook à **chaque** push (`dev` → dev.metalfrom.eu, `main` → metalfrom.eu). Rien ne s'interpose. La porte de qualité est donc **locale**, dans le hook `pre-push`. ``` git push ──▶ [hook pre-push : npm run check] ──▶ Forgejo ──▶ webhook ──▶ Coolify redeploy ↑ ~15 s, bloquant ``` ## Installation (une fois par clone) ```bash npm install npm run hooks:install # active .githooks/pre-push pip install -r requirements-dev.txt ``` ## Commandes et temps mesurés | Commande | Ce que ça fait | Froid | Incrémental | |---|---|---|---| | `npm run check` | **La porte** : ESLint + Ruff + tsc + tous les tests | ~15 s | — | | `npm test` | 401 tests JS | 3,5 s | — | | `npm run test:py` | 43 tests Python | 1 s | — | | `npm run lint` / `lint:py` | ESLint / Ruff | 3 s / 1 s | — | | `npm run typecheck` | `tsc --checkJs` sur l'API | 6 s | — | | `npm run test:mutation` | Mutation, logique pure (352 mutants) | 34 s | ~5 s | | `npm run test:mutation:full` | Mutation, toute l'API (1511 mutants) | 5 min | 13 s | | `npm run check:full` | `check` + audits de dépendances + mutation complète | — | ~1 min | ### Où passe le temps, et pourquoi c'est acceptable La commande de la boucle de développement, c'est `npm run check` : **17 s**, et elle inclut déjà les 401 tests, ESLint, Ruff et tsc. C'est elle qui tourne cent fois par jour. La mutation n'est pas une commande de boucle courte. Son coût se lit ainsi : - `test:mutation` (352 mutants) : 34 s à froid, ~5 s ensuite. Assez rapide pour être lancée avant chaque commit qui touche la validation ou l'auth. - `test:mutation:full` (1511 mutants) : 5 min **une seule fois** (clone neuf ou après avoir modifié les quatre fichiers mutés d'un coup). Après une édition normale d'un seul fichier : **13 s**, mesuré. Le fichier incrémental vit dans `reports/`, qui est gitignoré : un clone neuf paie donc les 5 minutes une fois. C'est assumé — c'est un audit, pas un test. **Ce qui n'a délibérément pas été fait pour aller plus vite** : réduire encore le périmètre muté. Descendre sous ~1500 mutants sur l'API reviendrait à ne plus mesurer grand-chose, et un score de mutation flatteur obtenu en retirant les mutants gênants est pire qu'une absence de score. ## Ce que couvrent les tests | Batterie | Où | Approche | |---|---|---| | **Unitaires** | `apps/api/test/validate.test.js` | Validation pure : bbox, limit/offset, coordonnées, années, recherche | | **Routes / intégration** | `publicRoutes*.test.js`, `adminRoutes*.test.js` | `fastify.inject()` + faux pool. Gating d'auth, allowlists de colonnes, forme du SQL, transactions | | **Sécurité** | `adminAuth.test.js`, `rateLimit.test.js` | JWT forgé, `alg:none`, expiration, anti-énumération, verrouillage de compte, plafonds de débit par IP et par jeton | | **Annulation** | `cancellation.test.js` + `apps/crawler/tests/test_cancellation.py` | Les deux moitiés du protocole coopératif | | **Accessibilité** | `apps/web/test/a11y.test.js` | axe-core + jsdom sur le HTML statique | | **Frontend** | `apps/web/test/pure.test.js` | Filtrage, tri, échappement HTML | | **Méta** | `apps/api/test/harness.test.js` | Vérifie le faux pool lui-même | | **Python** | `apps/geocoder/tests/`, `apps/crawler/tests/` | Parseur de localisations, annulation | | **Mutation** | `stryker*.config.json` | 82 % (logique pure) / 65 % (API complète) | ## Performance : les décisions et leurs mesures Le principe : **aucun test ne monte de conteneur, ne compile, ni ne touche le réseau.** Quatre optimisations, toutes mesurées (les intuitions non vérifiées se sont révélées fausses au moins une fois) : 1. **Une app Fastify par fichier de test, pas par cas.** `buildServer()` coûte 14 ms. À raison d'une construction par `it()`, ces 14 ms étaient multipliés par ~12 000 exécutions pendant un run de mutation. Les tests partagent une app et reprogramment un pool via `pool.reset()` (`test/helpers/testApp.js`). 2. **Config Vitest dédiée à la mutation.** Stryker recharge les fichiers de test à chaque mutant. En ne déclarant que ceux qui couvrent le code muté (et surtout pas le test a11y, qui initialise jsdom en ~2 s), le profil pur est passé de **1 min 23 à 34 s**. 3. **Mutants `StringLiteral` exclus.** Les handlers sont à ~80 % du SQL en template literals. Muter le contenu d'une chaîne SQL ne mesure rien. Les retirer a fait passer le profil complet de 7 min 50 à 5 min — et le score de 42 % à 65 %, parce que le bruit disparaissait du dénominateur. 4. **Plafonds de débit paramétrables.** Conséquence du point 1 : une app partagée accumule l'état du limiteur. Les valeurs réelles de production sont vérifiées séparément dans `rateLimit.test.js`, pour que la paramétrisation ne crée pas d'angle mort. Ce qui a été **essayé et rejeté sur mesure** : le pool Vitest `threads` avec `isolate: false`, réputé plus rapide, donne **18,8 s contre 3,5 s** ici (la mise en place de jsdom est pénalisée). Le défaut (`forks`) est conservé. ## Pourquoi deux profils de mutation La mutation prouve qu'un test **échoue** quand le code casse — la couverture ne dit que « cette ligne a été exécutée ». - `test:mutation` (validate.js, adminAuth.js) : **82 %**. Logique pure, score interprétable, assez rapide pour être lancé souvent. - `test:mutation:full` (+ adminRoutes.js, app.js) : **65 %**. Les handlers de route plafonnent structurellement plus bas — beaucoup de mutants portent sur des messages de log ou des branches `catch` dont l'observable exact n'a pas d'importance. Utile en audit, pas en boucle courte. Les seuils d'échec (70 % et 55 %) sont des cliquets anti-régression, pas des objectifs. ### Ce qui n'est pas couvert - Règles a11y exigeant un moteur de rendu (contraste, cibles tactiles) : jsdom ne calcule pas de styles. À compléter par un audit Lighthouse manuel. - `migrate.js` : s'exécute au démarrage du conteneur et appelle `process.exit`. - Crawler et workers de géocodage : seuls le parseur et l'annulation sont couverts, le reste est de l'I/O réseau et base. - `apps/web/site/pure.js` est testé mais **exclu de la mutation** : il est chargé via `fs` + `vm` (comme le fait le navigateur), donc l'instrumentation Stryker ne l'atteindrait pas et afficherait un score faussement parfait. ## Forgejo Actions `.forgejo/workflows/ci.yml` existe mais **ne tourne pas** : aucun runner `act_runner` n'est enregistré sur le VPS. Le fichier est prêt si tu en ajoutes un. Il est volontairement informatif — il ne bloque pas le déploiement.