import type { FastifyInstance } from "fastify"; import { afterEach, describe, expect, it } from "vitest"; import { buildApp } from "../src/app"; import { hashPassword } from "../src/auth/password"; import { CSRF_COOKIE, SESSION_COOKIE } from "../src/auth/session"; import type { Database } from "../src/db/client"; import { users } from "../src/db/schema"; import { makeConfig, openTestDb } from "./helpers"; // Tests : pas de worker pino-pretty, error-handler en mode production. process.env.NODE_ENV = "production"; process.env.SEICHE_LOG_LEVEL = "silent"; let db: Database; let app!: FastifyInstance; afterEach(async () => { await app.close(); }); async function seedUser(username: string, password: string): Promise { await db.insert(users).values({ username, passwordHash: await hashPassword(password) }); } function setCookies(res: { headers: Record }, name: string): string[] { const raw = res.headers["set-cookie"]; const list = Array.isArray(raw) ? raw : raw === undefined ? [] : [String(raw)]; return list.filter((c): c is string => typeof c === "string" && c.startsWith(`${name}=`)); } async function buildLoginApp( overrides: Parameters[0], ): Promise { db = await openTestDb(); const instance = await buildApp({ config: makeConfig(overrides), db }); await seedUser("alice", "secret"); return instance; } function login(password: string, headers: Record = {}) { return app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "alice", password }, headers, }); } describe("Proxy (SEICHE_TRUST_PROXY / SEICHE_COOKIE_SECURE)", () => { describe("trustProxy", () => { it("activé : X-Forwarded-For est parsé → chaque IP client est limitée séparément", async () => { app = await buildLoginApp({ trustProxy: true }); const ips = [7, 8, 9, 10, 11, 12].map((n) => `203.0.113.${n}`); for (const ip of ips) { const res = await login("wrong", { "x-forwarded-for": ip }); expect(res.statusCode, `l'IP ${ip} a été bloquée à tort`).toBe(401); } }); it("désactivé (défaut) : X-Forwarded-For est ignoré → même IP socket, 6e tentative bloquée", async () => { app = await buildLoginApp({ trustProxy: false }); const codes: number[] = []; for (let i = 7; i <= 12; i++) { const res = await login("wrong", { "x-forwarded-for": `203.0.113.${i}` }); codes.push(res.statusCode); } expect(codes).toEqual([401, 401, 401, 401, 401, 429]); }); }); describe("cookieSecure", () => { it("activé : les cookies de session et CSRF sont marqués Secure", async () => { app = await buildLoginApp({ cookieSecure: true }); const res = await login("secret"); expect(res.statusCode).toBe(200); for (const name of [SESSION_COOKIE, CSRF_COOKIE]) { const cookies = setCookies(res, name); expect(cookies, `cookie ${name} absent`).toHaveLength(1); expect(cookies[0]?.toLowerCase()).toContain("secure"); } }); it("désactivé (défaut) : pas de drapeau Secure", async () => { app = await buildLoginApp({ cookieSecure: false }); const res = await login("secret"); expect(res.statusCode).toBe(200); for (const name of [SESSION_COOKIE, CSRF_COOKIE]) { const cookies = setCookies(res, name); expect(cookies).toHaveLength(1); expect(cookies[0]?.toLowerCase()).not.toContain("secure"); } }); }); });