spb/phi-conseil
Public
1import { beforeEach, describe, expect, it, vi } from "vitest";2import { POST } from "@/app/api/contact/route";3import { setMailerForTests, type Mailer } from "@/lib/mail";4import { resetEnvCache } from "@/lib/env";56const sent: unknown[] = [];7const fakeMailer: Mailer = {8 async send(data) {9 sent.push(data);10 return { delivered: true, id: "fake-id" };11 },12};1314function req(body: unknown, headers: Record<string, string> = {}) {15 return new Request("http://localhost/api/contact", {16 method: "POST",17 headers: {18 "content-type": "application/json",19 "x-forwarded-for": headers.ip ?? "198.51.100.1",20 ...headers,21 },22 body: typeof body === "string" ? body : JSON.stringify(body),23 });24}2526const valid = {27 name: "Jean Roy",28 email: "jean@exemple.com",29 organization: "OBNL",30 subject: "Reddition de comptes",31 message: "Nous devons produire un rapport final pour une subvention reçue cette année.",32 consent: true,33 website: "",34 startedAt: Date.now() - 10_000,35};3637describe("POST /api/contact", () => {38 beforeEach(() => {39 sent.length = 0;40 process.env.CONTACT_TRANSPORT = "log";41 process.env.CONTACT_RATE_LIMIT = "100";42 resetEnvCache();43 setMailerForTests(fakeMailer);44 vi.spyOn(console, "info").mockImplementation(() => {});45 });4647 it("accepte une soumission valide et appelle le mailer", async () => {48 const res = await POST(req({ ...valid, message: valid.message + " A" }));49 expect(res.status).toBe(200);50 expect(await res.json()).toMatchObject({ ok: true, delivered: true });51 expect(sent).toHaveLength(1);52 });5354 it("renvoie 400 avec les champs fautifs", async () => {55 const res = await POST(req({ ...valid, email: "pas-un-courriel", message: "B" }));56 expect(res.status).toBe(400);57 const body = await res.json();58 expect(body.ok).toBe(false);59 expect(Object.keys(body.fields)).toEqual(expect.arrayContaining(["email", "message"]));60 expect(sent).toHaveLength(0);61 });6263 it("répond succès silencieux au honeypot sans envoyer", async () => {64 const res = await POST(req({ ...valid, website: "http://bot", message: valid.message + " C" }));65 expect(res.status).toBe(200);66 expect(await res.json()).toMatchObject({ ok: true, delivered: false });67 expect(sent).toHaveLength(0);68 });6970 it("ignore une soumission instantanée (robot)", async () => {71 const res = await POST(req({ ...valid, startedAt: Date.now(), message: valid.message + " D" }));72 expect(await res.json()).toMatchObject({ ok: true, delivered: false });73 expect(sent).toHaveLength(0);74 });7576 it("refuse un JSON invalide et un mauvais content-type", async () => {77 expect((await POST(req("{oops"))).status).toBe(400);78 const r = await POST(79 new Request("http://localhost/api/contact", {80 method: "POST",81 headers: { "content-type": "text/plain" },82 body: "x",83 }),84 );85 expect(r.status).toBe(415);86 });8788 it("dédoublonne un même message", async () => {89 const body = { ...valid, message: valid.message + " E" };90 await POST(req(body));91 const res = await POST(req(body));92 expect(await res.json()).toMatchObject({ ok: true, duplicate: true });93 expect(sent).toHaveLength(1);94 });9596 it("renvoie 502 si le mailer échoue, sans fuite de détail", async () => {97 vi.spyOn(console, "error").mockImplementation(() => {});98 setMailerForTests({99 async send() {100 throw new Error("SMTP down: password=secret");101 },102 });103 const res = await POST(req({ ...valid, message: valid.message + " F" }));104 expect(res.status).toBe(502);105 const txt = JSON.stringify(await res.json());106 expect(txt).not.toContain("secret");107 });108});109