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:
87
backend/tests/draw.test.js
Normal file
87
backend/tests/draw.test.js
Normal 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()
|
||||
);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user