Le dépôt n'avait aucun test, aucun linter, aucune vérification de types. Outillage - ESLint 9 (flat config) sur api + les deux frontends, Ruff sur le Python - tsc --checkJs sur l'API (pas de TypeScript, juste la vérification) - Vitest : 401 tests JS ; pytest : 43 tests Python - Tests de mutation (Stryker), deux profils : logique pure et API complète - Hook pre-push `npm run check` (~17 s) — le déploiement Coolify est sur webhook, c'est donc la seule porte de qualité avant la mise en ligne - Workflow Forgejo Actions prêt (inerte tant qu'aucun runner n'est enregistré) Sécurité - Injection SQL authentifiée dans resolve-conflict : `field` était interpolé dans le SET sans allowlist - timingSafeEqual levait sur un jeton multi-octets (500 au lieu de 401) - setErrorHandler écrasait tous les 4xx en 500 - .env.example : ADMIN_JWT_SECRET et ADMIN_SEED_* n'étaient documentés nulle part alors que leur absence casse toute connexion admin Annulation réelle des crawl_run (migration 014) - L'API posait status='error' sans que le crawler en sache rien : le process continuait, et son UPDATE final ne matchait plus (run réussi affiché en erreur) - Protocole coopératif : drapeau cancel_requested lu à chaque lot, le crawler écrit lui-même status='cancelled' Cohérence géographique (migration 014) - Le trigger 013 supprimait les band_locations sans purger le point dénormalisé - L'édition admin de lat/lon n'atteignait jamais band_locations : la carte ignorait la correction. Override step_order = -1, dans une transaction Corrections - limit/offset NaN → 500 au lieu de 400 - OPTIONS sans `return reply` (Fastify poursuivait le cycle de vie) - listen() sans catch, cast ::text en dur sur les colonnes numériques - /admin/api/logs ne renvoyait pas sa pagination - a11y : sélecteur de langue annoncé comme liste vide (role=option manquant) Nettoyage - apps/web/quizz-site supprimé (sans rapport avec le projet) - Code mort : openModal(), LANG_NAMES, double import, variables inutilisées - .dockerignore ajoutés ; node_modules racine n'était pas gitignoré Co-Authored-By: Claude <noreply@anthropic.com>
134 lines
6.6 KiB
Markdown
134 lines
6.6 KiB
Markdown
# 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.
|