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>
110 lines
3.6 KiB
JavaScript
110 lines
3.6 KiB
JavaScript
/**
|
|
* Faux `pg.Pool` pour les tests de routes.
|
|
*
|
|
* Objectif : tester le routage, l'authentification, la validation d'entrée et
|
|
* la *forme* du SQL généré (allowlists de colonnes, casts, transactions) — sans
|
|
* démarrer de conteneur Postgres. Un test qui monte un conteneur met des
|
|
* dizaines de secondes ; celui-ci tourne en millisecondes.
|
|
*
|
|
* `handlers` associe un fragment de SQL (sous-chaîne ou RegExp) à un résultat.
|
|
* Toutes les requêtes exécutées sont enregistrées dans `pool.calls`.
|
|
*
|
|
* Le faux client rend les transactions observables : `pool.committed` et
|
|
* `pool.rolledBack` permettent d'affirmer qu'une route a bien annulé son travail
|
|
* sur erreur, et `pool.released` qu'elle n'a pas fui de connexion.
|
|
*/
|
|
/**
|
|
* @typedef {object} Handler
|
|
* @property {string|RegExp} match fragment de SQL à reconnaître
|
|
* @property {any} [result] résultat pg, ou fonction (sql, values) => résultat
|
|
* @property {Error|string} [throws] fait échouer la requête (chemins d'erreur)
|
|
*
|
|
* @param {Handler[]} [handlers]
|
|
*/
|
|
export function makeFakePool(handlers = []) {
|
|
const calls = [];
|
|
const state = { committed: false, rolledBack: false, released: 0, connections: 0 };
|
|
let active = handlers;
|
|
|
|
async function query(text, values) {
|
|
const sql = typeof text === "string" ? text : text.text;
|
|
calls.push({ sql, values: values ?? [] });
|
|
|
|
if (sql === "COMMIT") state.committed = true;
|
|
if (sql === "ROLLBACK") state.rolledBack = true;
|
|
|
|
for (const { match, result, throws } of active) {
|
|
const hit = match instanceof RegExp ? match.test(sql) : sql.includes(match);
|
|
if (hit) {
|
|
if (throws) throw throws instanceof Error ? throws : new Error(String(throws));
|
|
return typeof result === "function" ? result(sql, values) : result;
|
|
}
|
|
}
|
|
return { rows: [], rowCount: 0 };
|
|
}
|
|
|
|
const pool = {
|
|
calls,
|
|
query,
|
|
|
|
/**
|
|
* Reconfigure le pool sans en recréer un.
|
|
*
|
|
* Permet de construire l'app Fastify UNE fois par fichier de test (14 ms
|
|
* par construction, multipliées par ~12 000 exécutions pendant un run de
|
|
* mutation) au lieu d'une fois par cas de test.
|
|
*/
|
|
reset(newHandlers = []) {
|
|
active = newHandlers;
|
|
calls.length = 0;
|
|
state.committed = false;
|
|
state.rolledBack = false;
|
|
state.released = 0;
|
|
state.connections = 0;
|
|
return pool;
|
|
},
|
|
|
|
async connect() {
|
|
state.connections++;
|
|
return {
|
|
query,
|
|
release() {
|
|
state.released++;
|
|
},
|
|
};
|
|
},
|
|
|
|
get committed() { return state.committed; },
|
|
get rolledBack() { return state.rolledBack; },
|
|
get released() { return state.released; },
|
|
get connections() { return state.connections; },
|
|
|
|
/** Retrouve la première requête exécutée contenant `fragment`. */
|
|
find(fragment) {
|
|
return calls.find((c) =>
|
|
fragment instanceof RegExp ? fragment.test(c.sql) : c.sql.includes(fragment)
|
|
);
|
|
},
|
|
|
|
/** Toutes les requêtes contenant `fragment`. */
|
|
findAll(fragment) {
|
|
return calls.filter((c) =>
|
|
fragment instanceof RegExp ? fragment.test(c.sql) : c.sql.includes(fragment)
|
|
);
|
|
},
|
|
|
|
/** Ordre d'exécution des requêtes correspondant à l'un des fragments. */
|
|
sequence(fragments) {
|
|
return calls
|
|
.map((c) => fragments.find((f) => (f instanceof RegExp ? f.test(c.sql) : c.sql.includes(f))))
|
|
.filter(Boolean);
|
|
},
|
|
};
|
|
|
|
return pool;
|
|
}
|
|
|
|
/** Raccourci : un résultat pg minimal. */
|
|
export function rows(...items) {
|
|
return { rows: items, rowCount: items.length };
|
|
}
|