import { faker } from "@faker-js/faker"; import { expect } from "chai"; import request from "supertest"; import { createApp } from "../src/app.js"; import { env } from "../src/config/env.js"; import { prisma } from "../src/db/prisma.js"; import { hashAdminPassword } from "../src/modules/admin/admin-auth.service.js"; import { resetDatabase } from "../test-support/reset-db.js"; async function seedAdmin(): Promise<{ email: string; password: string }> { const email = faker.internet.email().toLowerCase(); const password = faker.internet.password({ length: 16 }); await prisma.adminUser.create({ data: { email, name: faker.person.fullName(), passwordHash: await hashAdminPassword(password) }, }); return { email, password }; } /** Creates a `UserProfile` to attribute contact messages to. */ async function seedUser() { return prisma.userProfile.create({ data: { firstName: faker.person.firstName(), lastName: faker.person.lastName(), email: `${faker.string.uuid()}@example.test`, passwordHash: "x", }, }); } /** One `ContactMessage`, `createdAt` overridable so ordering is deterministic. */ async function seedMessage(overrides: { authorId: number | null; type?: string; message?: string; createdAt?: Date; }) { return prisma.contactMessage.create({ data: { authorId: overrides.authorId, type: overrides.type ?? "bug", message: overrides.message ?? faker.lorem.sentence(), pageUrl: "/", userAgent: "test-agent/1.0", appVersion: "0.2.1", ...(overrides.createdAt ? { createdAt: overrides.createdAt } : {}), }, }); } const adminSecretConfigured = env.ADMIN_JWT_SECRET !== undefined; describe("Admin contact messages", () => { const app = createApp(); beforeEach(async () => { await resetDatabase(); }); after(async () => { await prisma.$disconnect(); }); async function adminAgent() { const { email, password } = await seedAdmin(); const agent = request.agent(app); await agent.post("/admin/api/auth/login").send({ email, password }); return agent; } it("rejects the route without an admin session", async () => { const res = await request(app).get("/admin/api/contact"); expect(res.status).to.equal(401); }); it("returns messages newest first, with the author resolved", async function () { if (!adminSecretConfigured) { // biome-ignore lint/suspicious/noExplicitAny: mocha's `this.skip()` isn't typed here. (this as any).skip(); return; } const user = await seedUser(); await seedMessage({ authorId: user.id, message: "le plus ancien", createdAt: new Date("2026-01-01T00:00:00.000Z"), }); await seedMessage({ authorId: user.id, message: "le plus récent", createdAt: new Date("2026-06-01T00:00:00.000Z"), }); const agent = await adminAgent(); const res = await agent.get("/admin/api/contact"); expect(res.status).to.equal(200); expect(res.body.map((row: { message: string }) => row.message)).to.deep.equal([ "le plus récent", "le plus ancien", ]); expect(res.body[0].author).to.deep.equal({ id: user.id, firstName: user.firstName, lastName: user.lastName, email: user.email, }); }); it("filters by type", async function () { if (!adminSecretConfigured) { // biome-ignore lint/suspicious/noExplicitAny: mocha's `this.skip()` isn't typed here. (this as any).skip(); return; } const user = await seedUser(); await seedMessage({ authorId: user.id, type: "bug" }); await seedMessage({ authorId: user.id, type: "suggestion" }); await seedMessage({ authorId: user.id, type: "suggestion" }); const agent = await adminAgent(); const res = await agent.get("/admin/api/contact").query({ type: "suggestion" }); expect(res.status).to.equal(200); expect(res.body).to.have.lengthOf(2); expect(res.body.every((row: { type: string }) => row.type === "suggestion")).to.equal(true); }); it("rejects an unknown type filter with 400", async function () { if (!adminSecretConfigured) { // biome-ignore lint/suspicious/noExplicitAny: mocha's `this.skip()` isn't typed here. (this as any).skip(); return; } const agent = await adminAgent(); const res = await agent.get("/admin/api/contact").query({ type: "complaint" }); expect(res.status).to.equal(400); }); it("caps the page at 200 rows", async function () { if (!adminSecretConfigured) { // biome-ignore lint/suspicious/noExplicitAny: mocha's `this.skip()` isn't typed here. (this as any).skip(); return; } const user = await seedUser(); await prisma.contactMessage.createMany({ data: Array.from({ length: 205 }, () => ({ authorId: user.id, type: "bug", message: faker.lorem.sentence(), })), }); const agent = await adminAgent(); const res = await agent.get("/admin/api/contact"); expect(res.status).to.equal(200); expect(res.body).to.have.lengthOf(200); }); it("reports author: null once the sender's account is deleted", async function () { if (!adminSecretConfigured) { // biome-ignore lint/suspicious/noExplicitAny: mocha's `this.skip()` isn't typed here. (this as any).skip(); return; } const user = await seedUser(); await seedMessage({ authorId: user.id, message: "signalé puis compte supprimé" }); await prisma.userProfile.delete({ where: { id: user.id } }); const agent = await adminAgent(); const res = await agent.get("/admin/api/contact"); expect(res.status).to.equal(200); expect(res.body).to.have.lengthOf(1); expect(res.body[0].message).to.equal("signalé puis compte supprimé"); expect(res.body[0].author).to.equal(null); }); });