import { ErrorCode, type SignupInput } from "@batch-cooking/shared"; import { faker } from "@faker-js/faker"; import { expect } from "chai"; import request from "supertest"; import { createApp } from "../src/app.js"; import { prisma } from "../src/db/prisma.js"; import { resetDatabase } from "../test-support/reset-db.js"; /** See `auth.test.ts` — same rationale for generating rather than hardcoding. */ function buildSignupPayload(): SignupInput { const firstName = faker.person.firstName(); const lastName = faker.person.lastName(); return { firstName, lastName, email: faker.internet.email({ firstName, lastName }).toLowerCase(), password: faker.internet.password({ length: 16 }), }; } /** A valid `POST /contact` body — message comfortably over the 10-char minimum. */ function buildContactPayload() { return { type: "bug" as const, message: "Le bouton « Envoyer » ne réagit pas sur la page planning.", pageUrl: "/", appVersion: "0.2.1", }; } describe("Contact", () => { const app = createApp(); beforeEach(async () => { await resetDatabase(); }); after(async () => { await prisma.$disconnect(); }); describe("POST /contact", () => { it("rejects requests without a session with 401 NOT_AUTHENTICATED", async () => { const res = await request(app).post("/contact").send(buildContactPayload()); expect(res.status).to.equal(401); expect(res.body.code).to.equal(ErrorCode.NOT_AUTHENTICATED); expect(await prisma.contactMessage.count()).to.equal(0); }); it("stores the message with its fields, attributed to the caller, and captures the user-agent from the header", async () => { const agent = request.agent(app); const signup = buildSignupPayload(); await agent.post("/auth/signup").send(signup); const author = await prisma.userProfile.findFirstOrThrow({ where: { email: signup.email } }); const res = await agent .post("/contact") .set("User-Agent", "BatchCookingApp/0.2.1 (Android 14)") .send(buildContactPayload()); expect(res.status).to.equal(204); const rows = await prisma.contactMessage.findMany(); expect(rows).to.have.lengthOf(1); const [row] = rows; expect(row.authorId).to.equal(author.id); expect(row.type).to.equal("bug"); expect(row.message).to.equal(buildContactPayload().message); expect(row.pageUrl).to.equal("/"); expect(row.appVersion).to.equal("0.2.1"); // Read from the request header, never the body. expect(row.userAgent).to.equal("BatchCookingApp/0.2.1 (Android 14)"); }); it("ignores any user-agent sent in the body — the header is the only source", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const res = await agent .post("/contact") .set("User-Agent", "real-agent/1.0") .send({ ...buildContactPayload(), userAgent: "spoofed-agent/9.9" }); expect(res.status).to.equal(204); const row = await prisma.contactMessage.findFirstOrThrow(); expect(row.userAgent).to.equal("real-agent/1.0"); }); it("keeps pageUrl / appVersion null when the client omits them", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const res = await agent.post("/contact").send({ type: "suggestion", message: "Ajoutez un mode hors-ligne pour la liste de courses.", }); expect(res.status).to.equal(204); const row = await prisma.contactMessage.findFirstOrThrow(); expect(row.type).to.equal("suggestion"); expect(row.pageUrl).to.equal(null); expect(row.appVersion).to.equal(null); }); it("rejects a message shorter than 10 characters with 400 VALIDATION_ERROR", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); // 5 chars — and the schema trims before counting, so leading/trailing // whitespace can't pad it to the minimum. const res = await agent.post("/contact").send({ type: "bug", message: " court " }); expect(res.status).to.equal(400); expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR); expect(await prisma.contactMessage.count()).to.equal(0); }); it("rejects a message longer than 5000 characters with 400 VALIDATION_ERROR", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const res = await agent.post("/contact").send({ type: "bug", message: "a".repeat(5001) }); expect(res.status).to.equal(400); expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR); expect(await prisma.contactMessage.count()).to.equal(0); }); it("rejects an unknown type with 400 VALIDATION_ERROR", async () => { const agent = request.agent(app); await agent.post("/auth/signup").send(buildSignupPayload()); const res = await agent .post("/contact") .send({ type: "complaint", message: "Ceci est un message de test suffisamment long." }); expect(res.status).to.equal(400); expect(res.body.code).to.equal(ErrorCode.VALIDATION_ERROR); expect(await prisma.contactMessage.count()).to.equal(0); }); }); });