feat(server): proxy — SEICHE_TRUST_PROXY + SEICHE_COOKIE_SECURE (+4 tests, 30/30)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user