95 lines
3.5 KiB
TypeScript
95 lines
3.5 KiB
TypeScript
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<void> {
|
|
await db.insert(users).values({ username, passwordHash: await hashPassword(password) });
|
|
}
|
|
|
|
function setCookies(res: { headers: Record<string, unknown> }, 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<typeof makeConfig>[0],
|
|
): Promise<FastifyInstance> {
|
|
db = await openTestDb();
|
|
const instance = await buildApp({ config: makeConfig(overrides), db });
|
|
await seedUser("alice", "secret");
|
|
return instance;
|
|
}
|
|
|
|
function login(password: string, headers: Record<string, string> = {}) {
|
|
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");
|
|
}
|
|
});
|
|
});
|
|
});
|