Sécurisation de l'API et refonte des tests backend

- helmet et express-rate-limit sur les routes d'authentification
- CORS restreint aux origines de FRONTEND_URL
- CSP désactivée uniquement sur /api-docs pour Swagger UI
- ajout du test de tirage et simplification du test e2e
- suppression de tests/setup.js devenu inutile

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Emma781227
2026-08-17 00:22:54 +02:00
parent a2543cf4ce
commit 20c865a071
10 changed files with 219 additions and 132 deletions

View File

@@ -0,0 +1,87 @@
const bcrypt = require("bcrypt");
const prisma = require("../src/config/prisma");
// ------------------------
// Vérifie que le pool de participants au tirage du gros lot ne compte
// chaque client qu'une seule fois, quel que soit son nombre de
// participations. On rejoue ici exactement la requête utilisée par
// admin.controller.js::drawWinner, restreinte à des utilisateurs dédiés
// pour ne pas dépendre des autres données déjà présentes en base
// (et surtout ne jamais toucher aux vrais tickets du jeu-concours).
// ------------------------
let userManyParticipations;
let userOneParticipation;
let ticketIds = [];
beforeAll(async () => {
const lot = await prisma.lot.findFirst();
if (!lot) throw new Error("Aucun lot en base : lancez `npx prisma db seed` avant ce test.");
const hashed = await bcrypt.hash("Password123", 10);
userManyParticipations = await prisma.user.create({
data: {
firstname: "Draw",
lastname: "MultiParticip",
email: "draw-test-multi@test.com",
password: hashed,
role: "CLIENT",
isEmailVerified: true,
},
});
userOneParticipation = await prisma.user.create({
data: {
firstname: "Draw",
lastname: "SingleParticip",
email: "draw-test-single@test.com",
password: hashed,
role: "CLIENT",
isEmailVerified: true,
},
});
const codes = ["ZZDRAWTEST1", "ZZDRAWTEST2", "ZZDRAWTEST3", "ZZDRAWTEST4"];
const owners = [
userManyParticipations,
userManyParticipations,
userManyParticipations,
userOneParticipation,
];
for (let i = 0; i < codes.length; i++) {
const ticket = await prisma.ticket.create({
data: { code: codes[i], lotId: lot.id, isUsed: true, usedAt: new Date(), userId: owners[i].id },
});
ticketIds.push(ticket.id);
await prisma.participation.create({
data: { userId: owners[i].id, ticketId: ticket.id, lotId: lot.id },
});
}
});
afterAll(async () => {
await prisma.participation.deleteMany({ where: { ticketId: { in: ticketIds } } });
await prisma.ticket.deleteMany({ where: { id: { in: ticketIds } } });
await prisma.user.deleteMany({
where: { id: { in: [userManyParticipations.id, userOneParticipation.id] } },
});
await prisma.$disconnect();
});
test("un client avec plusieurs participations ne compte qu'une fois dans le pool du tirage", async () => {
const eligible = await prisma.user.findMany({
where: {
role: "CLIENT",
id: { in: [userManyParticipations.id, userOneParticipation.id] },
participations: { some: {} },
},
select: { id: true },
});
expect(eligible).toHaveLength(2);
expect(eligible.map((u) => u.id).sort()).toEqual(
[userManyParticipations.id, userOneParticipation.id].sort()
);
});

View File

@@ -10,125 +10,88 @@ jest.mock("../src/config/mailer", () => ({
}));
// ------------------------
// Réinitialisation DB avant tests
// Ce test tourne sur la même base que le reste du dev (500 000 tickets
// réels du jeu-concours). On ne touche donc JAMAIS les tables globales
// (pas de deleteMany sur claim/participation/ticket/user/lot) : tout ce
// que ce fichier crée est identifiable de façon unique et nettoyé dans
// afterAll, rien d'autre.
// ------------------------
const testEmail = "e2e-flow-test@test.com";
const ticketCode = "ZZE2ETEST1";
let token;
let createdTicketId;
beforeAll(async () => {
await prisma.claim.deleteMany();
await prisma.participation.deleteMany();
await prisma.ticket.deleteMany();
await prisma.user.deleteMany();
await prisma.user.deleteMany({ where: { email: testEmail } });
await prisma.ticket.deleteMany({ where: { code: ticketCode } });
});
afterAll(async () => {
const user = await prisma.user.findUnique({ where: { email: testEmail } });
if (user) {
await prisma.claim.deleteMany({ where: { participation: { userId: user.id } } });
await prisma.participation.deleteMany({ where: { userId: user.id } });
}
if (createdTicketId) {
await prisma.ticket.delete({ where: { id: createdTicketId } }).catch(() => {});
}
if (user) {
await prisma.user.delete({ where: { id: user.id } });
}
await prisma.$disconnect();
});
describe("THE TIP TOP - FULL E2E FLOW", () => {
let token;
let ticketCode = "ABC123";
// ------------------------
// 1. REGISTER
// ------------------------
test("Register user", async () => {
const res = await request(app)
.post("/api/auth/register")
.send({
firstname: "Test",
lastname: "User",
email: "test@test.com",
password: "Password123",
});
const res = await request(app).post("/api/auth/register").send({
firstname: "E2E",
lastname: "Test",
email: testEmail,
password: "Password123",
});
expect([201, 409]).toContain(res.statusCode);
expect(res.statusCode).toBe(201);
});
// ------------------------
// 2. LOGIN
// ------------------------
test("Login user", async () => {
// Validation du user pour bypasser vérification email
await prisma.user.update({
where: { email: "test@test.com" },
where: { email: testEmail },
data: { isEmailVerified: true },
});
const res = await request(app)
.post("/api/auth/login")
.send({
email: "test@test.com",
password: "Password123",
});
const res = await request(app).post("/api/auth/login").send({
email: testEmail,
password: "Password123",
});
expect(res.statusCode).toBe(200);
expect(res.body.token).toBeDefined();
token = res.body.token;
});
// ------------------------
// 3. PARTICIPATE GAME
// ------------------------
test("Participate to game", async () => {
const res = await request(app)
.post("/api/game/participate")
.set("Authorization", `Bearer ${token}`)
.send({ code: ticketCode });
expect([200, 201, 404, 409, 400]).toContain(res.statusCode);
console.log("Ticket in DB before participate:", ticketCode);
});
// ------------------------
// 4. MY GAINS
// ------------------------
test("Get my gains", async () => {
const res = await request(app)
.get("/api/game/my-gains")
.set("Authorization", `Bearer ${token}`);
expect(res.statusCode).toBe(200);
expect(res.body.gains).toBeDefined();
});
// ------------------------
// 3. GENERER UN TICKET
// ------------------------
test("Create a ticket for testing", async () => {
const lot = await prisma.lot.create({
data: {
name: "Lot Test",
description: "Lot pour test E2E",
value: 10,
percentage: 100
},
});
test("Create a dedicated ticket for this test", async () => {
const lot = await prisma.lot.findFirst();
expect(lot).not.toBeNull();
const ticket = await prisma.ticket.create({
data: {
code: ticketCode,
lotId: lot.id
}
data: { code: ticketCode, lotId: lot.id },
});
createdTicketId = ticket.id;
expect(ticket).toBeDefined();
expect(ticket.code).toBe(ticketCode);
});
// ------------------------
// 4. PARTICIPATE GAME
// ------------------------
test("Participate to game", async () => {
const res = await request(app)
.post("/api/game/participate")
.set("Authorization", `Bearer ${token}`)
.send({ code: ticketCode });
expect([200, 201, 404, 409, 400]).toContain(res.statusCode);
expect(res.statusCode).toBe(201);
expect(res.body.gain).toBeDefined();
});
// ------------------------
// 5. MY GAINS
// ------------------------
test("Get my gains", async () => {
const res = await request(app)
.get("/api/game/my-gains")
@@ -136,11 +99,9 @@ describe("THE TIP TOP - FULL E2E FLOW", () => {
expect(res.statusCode).toBe(200);
expect(res.body.gains).toBeDefined();
expect(res.body.gains.some((g) => g.ticketCode === ticketCode)).toBe(true);
});
// ------------------------
// 6. ADMIN STATS (CLIENT access denied)
// ------------------------
test("Admin stats - CLIENT access denied", async () => {
const res = await request(app)
.get("/api/admin/stats")
@@ -149,4 +110,4 @@ describe("THE TIP TOP - FULL E2E FLOW", () => {
expect(res.statusCode).toBe(403);
expect(res.body.message || res.body).toBeDefined();
});
});
});

View File

@@ -1,31 +0,0 @@
const prisma = require("../src/config/prisma");
beforeAll(async () => {
await prisma.claim.deleteMany();
await prisma.participation.deleteMany();
await prisma.ticket.deleteMany();
await prisma.user.deleteMany();
// Création de lots
await prisma.lot.createMany({
data: [
{ name: "Infuseur", value: 10, percentage: 60, description: "Infuseur à thé" },
{ name: "Thé Détox", value: 15, percentage: 20, description: "Thé détox bio" },
{ name: "Thé Signature", value: 20, percentage: 10, description: "Thé signature" },
{ name: "Coffret 39€", value: 39, percentage: 6, description: "Coffret découverte" },
{ name: "Coffret 69€", value: 69, percentage: 4, description: "Coffret premium" },
]
});
// Création d'un ticket
await prisma.ticket.create({
data: {
code: "ABC123",
lotId: 1
}
});
});
afterAll(async () => {
await prisma.$disconnect();
});