/** * Serveur de test pour les parcours Playwright. * * Reproduit la topologie de production : nginx sert les fichiers statiques du * dashboard et proxifie `/admin/api/*` et `/admin/auth/*` vers l'API (voir * apps/admin/nginx.conf). Ici, un petit serveur HTTP joue le rôle de nginx et * transmet à la VRAIE application Fastify — routage, cookies, JWT, bcrypt, * validation et génération SQL sont donc ceux de production — avec un faux pool * à la place de Postgres. * * Ce qui est réellement exercé : les parcours d'interface, l'authentification, * les états de chargement et d'erreur, l'accessibilité au clavier. * Ce qui ne l'est pas : la validité du SQL pour Postgres et le comportement de * PostGIS — affaire de tests d'intégration, pas de tests d'interface. */ import { createServer } from "node:http"; import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { buildServer } from "../../../api/src/app.js"; import { makeFakePool, rows } from "../../../api/test/helpers/fakePool.js"; import { seedHandlers } from "./fixtures.mjs"; const SITE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../site"); // Secret de session propre aux tests. Sans lui, signAdminSession() lève et la // connexion renvoie un 500 opaque — exactement le piège documenté dans // infra/.env.example pour la production. process.env.ADMIN_JWT_SECRET ||= "secret-e2e-suffisamment-long-pour-passer-32"; const MIME = { ".html": "text/html; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml", ".ico": "image/x-icon", ".png": "image/png", }; /** Les mêmes préfixes que ceux proxifiés par nginx.conf. */ const API_PREFIXES = ["/admin/api/", "/admin/auth/", "/api/"]; export async function startTestServer({ port = 0, handlers = seedHandlers() } = {}) { const pool = makeFakePool(handlers); const app = await buildServer({ pool, logger: false, importToken: "jeton-e2e", corsOrigins: ["http://127.0.0.1"], seedAdmin: false, // Les parcours enchaînent beaucoup d'appels ; les plafonds réels sont // vérifiés séparément dans apps/api/test/rateLimit.test.js. globalRateLimitMax: 1e9, adminRateLimitMax: 1e9, authRateLimitMax: 1e9, }); const front = createServer(async (req, res) => { const url = req.url || "/"; if (API_PREFIXES.some((p) => url.startsWith(p))) { const chunks = []; for await (const c of req) chunks.push(c); const payload = chunks.length ? Buffer.concat(chunks) : undefined; // `inject()` recalcule content-length à partir du payload : retransmettre // celui du client donne un corps tronqué, donc un 400 « champ requis ». const headers = { ...req.headers }; delete headers["content-length"]; delete headers["transfer-encoding"]; // inject() traverse tout le cycle de vie Fastify (hooks, plugins, // sérialisation) : c'est la même exécution qu'une requête réseau. const reply = await app.inject({ method: req.method, url, headers, payload }); res.writeHead(reply.statusCode, reply.headers); res.end(reply.rawPayload); return; } const rel = url.split("?")[0].replace(/^\/+/, "") || "index.html"; const file = path.resolve(SITE, rel); if (!file.startsWith(SITE)) { res.writeHead(403).end("forbidden"); return; } try { const buf = await readFile(file); res.writeHead(200, { "content-type": MIME[path.extname(file)] || "application/octet-stream" }); res.end(buf); } catch { // Application monopage : toute route inconnue retombe sur index.html res.writeHead(200, { "content-type": MIME[".html"] }); res.end(await readFile(path.join(SITE, "index.html"))); } }); await new Promise((resolve) => front.listen(port, "127.0.0.1", resolve)); return { app, pool, url: `http://127.0.0.1:${front.address().port}`, /** Reprogramme les réponses de la base entre deux scénarios. */ setHandlers: (h) => pool.reset(h), close: async () => { await new Promise((r) => front.close(r)); await app.close(); }, }; } export { rows }; // Démarrage autonome, utilisé par `webServer` dans playwright.config.js. if (process.env.E2E_STANDALONE === "1") { const srv = await startTestServer({ port: Number(process.env.E2E_PORT || 4319) }); console.log(`e2e server: ${srv.url}`); }