import { beforeEach, describe, expect, it, vi } from "vitest"; import { POST } from "@/app/api/contact/route"; import { setMailerForTests, type Mailer } from "@/lib/mail"; import { resetEnvCache } from "@/lib/env"; const sent: unknown[] = []; const fakeMailer: Mailer = { async send(data) { sent.push(data); return { delivered: true, id: "fake-id" }; }, }; function req(body: unknown, headers: Record = {}) { return new Request("http://localhost/api/contact", { method: "POST", headers: { "content-type": "application/json", "x-forwarded-for": headers.ip ?? "198.51.100.1", ...headers, }, body: typeof body === "string" ? body : JSON.stringify(body), }); } const valid = { name: "Jean Roy", email: "jean@exemple.com", organization: "OBNL", subject: "Reddition de comptes", message: "Nous devons produire un rapport final pour une subvention reçue cette année.", consent: true, website: "", startedAt: Date.now() - 10_000, }; describe("POST /api/contact", () => { beforeEach(() => { sent.length = 0; process.env.CONTACT_TRANSPORT = "log"; process.env.CONTACT_RATE_LIMIT = "100"; resetEnvCache(); setMailerForTests(fakeMailer); vi.spyOn(console, "info").mockImplementation(() => {}); }); it("accepte une soumission valide et appelle le mailer", async () => { const res = await POST(req({ ...valid, message: valid.message + " A" })); expect(res.status).toBe(200); expect(await res.json()).toMatchObject({ ok: true, delivered: true }); expect(sent).toHaveLength(1); }); it("renvoie 400 avec les champs fautifs", async () => { const res = await POST(req({ ...valid, email: "pas-un-courriel", message: "B" })); expect(res.status).toBe(400); const body = await res.json(); expect(body.ok).toBe(false); expect(Object.keys(body.fields)).toEqual(expect.arrayContaining(["email", "message"])); expect(sent).toHaveLength(0); }); it("répond succès silencieux au honeypot sans envoyer", async () => { const res = await POST(req({ ...valid, website: "http://bot", message: valid.message + " C" })); expect(res.status).toBe(200); expect(await res.json()).toMatchObject({ ok: true, delivered: false }); expect(sent).toHaveLength(0); }); it("ignore une soumission instantanée (robot)", async () => { const res = await POST(req({ ...valid, startedAt: Date.now(), message: valid.message + " D" })); expect(await res.json()).toMatchObject({ ok: true, delivered: false }); expect(sent).toHaveLength(0); }); it("refuse un JSON invalide et un mauvais content-type", async () => { expect((await POST(req("{oops"))).status).toBe(400); const r = await POST( new Request("http://localhost/api/contact", { method: "POST", headers: { "content-type": "text/plain" }, body: "x", }), ); expect(r.status).toBe(415); }); it("dédoublonne un même message", async () => { const body = { ...valid, message: valid.message + " E" }; await POST(req(body)); const res = await POST(req(body)); expect(await res.json()).toMatchObject({ ok: true, duplicate: true }); expect(sent).toHaveLength(1); }); it("renvoie 502 si le mailer échoue, sans fuite de détail", async () => { vi.spyOn(console, "error").mockImplementation(() => {}); setMailerForTests({ async send() { throw new Error("SMTP down: password=secret"); }, }); const res = await POST(req({ ...valid, message: valid.message + " F" })); expect(res.status).toBe(502); const txt = JSON.stringify(await res.json()); expect(txt).not.toContain("secret"); }); });