/** * 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 }; }