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:
@@ -5,6 +5,7 @@ DATABASE_URL="postgresql://postgres:password@localhost:5432/the_tip_top_db"
|
|||||||
JWT_SECRET="your_secret_key"
|
JWT_SECRET="your_secret_key"
|
||||||
|
|
||||||
APP_URL="http://localhost:5000"
|
APP_URL="http://localhost:5000"
|
||||||
|
FRONTEND_URL="http://localhost:5173"
|
||||||
MAIL_HOST="smtp.gmail.com"
|
MAIL_HOST="smtp.gmail.com"
|
||||||
MAIL_PORT="587"
|
MAIL_PORT="587"
|
||||||
MAIL_USER="your_email@gmail.com"
|
MAIL_USER="your_email@gmail.com"
|
||||||
|
|||||||
48
backend/package-lock.json
generated
48
backend/package-lock.json
generated
@@ -13,6 +13,8 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"express-rate-limit": "^8.6.1",
|
||||||
|
"helmet": "^8.3.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"nodemailer": "^9.0.3",
|
"nodemailer": "^9.0.3",
|
||||||
"prisma": "^6.19.3",
|
"prisma": "^6.19.3",
|
||||||
@@ -605,9 +607,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
"node_modules/@emnapi/wasi-threads": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
|
||||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
"integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -3187,6 +3189,25 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.1.tgz",
|
||||||
|
"integrity": "sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"ip-address": "^10.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/exsolve": {
|
"node_modules/exsolve": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz",
|
||||||
@@ -3635,6 +3656,18 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/helmet": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/EvanHahn"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-escaper": {
|
"node_modules/html-escaper": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
@@ -3743,6 +3776,15 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz",
|
||||||
|
"integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"cors": "^2.8.6",
|
"cors": "^2.8.6",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"express-rate-limit": "^8.6.1",
|
||||||
|
"helmet": "^8.3.0",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"nodemailer": "^9.0.3",
|
"nodemailer": "^9.0.3",
|
||||||
"prisma": "^6.19.3",
|
"prisma": "^6.19.3",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const cors = require("cors");
|
const cors = require("cors");
|
||||||
|
const helmet = require("helmet");
|
||||||
|
const rateLimit = require("express-rate-limit");
|
||||||
require("dotenv").config();
|
require("dotenv").config();
|
||||||
|
|
||||||
const authRoutes = require("./routes/auth.routes");
|
const authRoutes = require("./routes/auth.routes");
|
||||||
@@ -12,9 +14,32 @@ const swaggerSpec = require("./docs/swagger");
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors());
|
const allowedOrigins = (process.env.FRONTEND_URL || "http://localhost:5173").split(",");
|
||||||
|
|
||||||
|
// La CSP par défaut de helmet bloque les scripts inline de Swagger UI :
|
||||||
|
// on l'applique partout sauf sur /api-docs, où une CSP dédiée (désactivée)
|
||||||
|
// est posée juste avant de monter Swagger, plus bas.
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path.startsWith("/api-docs")) return next();
|
||||||
|
return helmet()(req, res, next);
|
||||||
|
});
|
||||||
|
app.use(cors({ origin: allowedOrigins }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Anti-bruteforce sur les routes d'authentification uniquement : 20
|
||||||
|
// tentatives par IP toutes les 15 minutes, sans limiter le reste de l'API
|
||||||
|
// (participation au jeu, consultation des gains, etc.).
|
||||||
|
const authLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
limit: 20,
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
message: { message: "Trop de tentatives, réessayez dans quelques minutes." },
|
||||||
|
});
|
||||||
|
app.use("/api/auth/login", authLimiter);
|
||||||
|
app.use("/api/auth/register", authLimiter);
|
||||||
|
app.use("/api/auth/forgot-password", authLimiter);
|
||||||
|
|
||||||
// routes
|
// routes
|
||||||
app.use("/api/auth", authRoutes);
|
app.use("/api/auth", authRoutes);
|
||||||
app.use("/api/game", gameRoutes);
|
app.use("/api/game", gameRoutes);
|
||||||
@@ -22,7 +47,7 @@ app.use("/api/employee", employeeRoutes);
|
|||||||
app.use("/api/admin", adminRoutes);
|
app.use("/api/admin", adminRoutes);
|
||||||
|
|
||||||
// swagger
|
// swagger
|
||||||
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
app.use("/api-docs", helmet({ contentSecurityPolicy: false }), swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||||
|
|
||||||
// test route
|
// test route
|
||||||
app.get("/", (req, res) => {
|
app.get("/", (req, res) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const bcrypt = require("bcrypt");
|
const bcrypt = require("bcrypt");
|
||||||
|
const crypto = require("crypto");
|
||||||
const prisma = require("../config/prisma");
|
const prisma = require("../config/prisma");
|
||||||
const { getGameWindow, expireOverdueClaims } = require("../utils/gameWindow");
|
const { getGameWindow, expireOverdueClaims } = require("../utils/gameWindow");
|
||||||
|
|
||||||
@@ -234,6 +235,10 @@ const drawWinner = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Un utilisateur = une entrée dans le pool, quel que soit son nombre de
|
||||||
|
// participations. On sélectionne des CLIENT distincts (pas des
|
||||||
|
// participations), donc avoir utilisé 10 tickets ou 1 seul ne change
|
||||||
|
// strictement rien aux chances de gagner le gros lot.
|
||||||
const participants = await prisma.user.findMany({
|
const participants = await prisma.user.findMany({
|
||||||
where: {
|
where: {
|
||||||
role: "CLIENT",
|
role: "CLIENT",
|
||||||
@@ -255,7 +260,11 @@ const drawWinner = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomIndex = Math.floor(Math.random() * participants.length);
|
// Tirage purement aléatoire et équiprobable parmi les participants
|
||||||
|
// distincts. crypto.randomInt (générateur cryptographiquement sûr,
|
||||||
|
// sans biais) plutôt que Math.random(), pour un tirage défendable
|
||||||
|
// devant l'huissier qui le supervise.
|
||||||
|
const randomIndex = crypto.randomInt(participants.length);
|
||||||
const winner = participants[randomIndex];
|
const winner = participants[randomIndex];
|
||||||
|
|
||||||
const savedWinner = await prisma.grandPrizeWinner.create({
|
const savedWinner = await prisma.grandPrizeWinner.create({
|
||||||
@@ -302,14 +311,8 @@ const getGrandPrizeWinner = async (req, res) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!winner) {
|
|
||||||
return res.status(404).json({
|
|
||||||
message: "Aucun gagnant du gros lot n'a encore été tiré.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
winner,
|
winner: winner ?? null,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur récupération gagnant:", error);
|
console.error("Erreur récupération gagnant:", error);
|
||||||
|
|||||||
@@ -105,9 +105,7 @@ router.post("/draw", drawWinner);
|
|||||||
* - bearerAuth: []
|
* - bearerAuth: []
|
||||||
* responses:
|
* responses:
|
||||||
* 200:
|
* 200:
|
||||||
* description: Gagnant du gros lot
|
* description: Gagnant du gros lot, ou { winner - null } si aucun tirage n'a encore eu lieu
|
||||||
* 404:
|
|
||||||
* description: Aucun gagnant encore tiré
|
|
||||||
*/
|
*/
|
||||||
router.get("/draw", getGrandPrizeWinner);
|
router.get("/draw", getGrandPrizeWinner);
|
||||||
|
|
||||||
|
|||||||
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 () => {
|
beforeAll(async () => {
|
||||||
await prisma.claim.deleteMany();
|
await prisma.user.deleteMany({ where: { email: testEmail } });
|
||||||
await prisma.participation.deleteMany();
|
await prisma.ticket.deleteMany({ where: { code: ticketCode } });
|
||||||
await prisma.ticket.deleteMany();
|
|
||||||
await prisma.user.deleteMany();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
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();
|
await prisma.$disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("THE TIP TOP - FULL E2E FLOW", () => {
|
describe("THE TIP TOP - FULL E2E FLOW", () => {
|
||||||
let token;
|
|
||||||
let ticketCode = "ABC123";
|
|
||||||
|
|
||||||
// ------------------------
|
|
||||||
// 1. REGISTER
|
|
||||||
// ------------------------
|
|
||||||
test("Register user", async () => {
|
test("Register user", async () => {
|
||||||
const res = await request(app)
|
const res = await request(app).post("/api/auth/register").send({
|
||||||
.post("/api/auth/register")
|
firstname: "E2E",
|
||||||
.send({
|
lastname: "Test",
|
||||||
firstname: "Test",
|
email: testEmail,
|
||||||
lastname: "User",
|
password: "Password123",
|
||||||
email: "test@test.com",
|
});
|
||||||
password: "Password123",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect([201, 409]).toContain(res.statusCode);
|
expect(res.statusCode).toBe(201);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------
|
|
||||||
// 2. LOGIN
|
|
||||||
// ------------------------
|
|
||||||
test("Login user", async () => {
|
test("Login user", async () => {
|
||||||
// Validation du user pour bypasser vérification email
|
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: { email: "test@test.com" },
|
where: { email: testEmail },
|
||||||
data: { isEmailVerified: true },
|
data: { isEmailVerified: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await request(app)
|
const res = await request(app).post("/api/auth/login").send({
|
||||||
.post("/api/auth/login")
|
email: testEmail,
|
||||||
.send({
|
password: "Password123",
|
||||||
email: "test@test.com",
|
});
|
||||||
password: "Password123",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
expect(res.body.token).toBeDefined();
|
expect(res.body.token).toBeDefined();
|
||||||
token = res.body.token;
|
token = res.body.token;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------
|
test("Create a dedicated ticket for this test", async () => {
|
||||||
// 3. PARTICIPATE GAME
|
const lot = await prisma.lot.findFirst();
|
||||||
// ------------------------
|
expect(lot).not.toBeNull();
|
||||||
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
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const ticket = await prisma.ticket.create({
|
const ticket = await prisma.ticket.create({
|
||||||
data: {
|
data: { code: ticketCode, lotId: lot.id },
|
||||||
code: ticketCode,
|
|
||||||
lotId: lot.id
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
createdTicketId = ticket.id;
|
||||||
|
|
||||||
expect(ticket).toBeDefined();
|
|
||||||
expect(ticket.code).toBe(ticketCode);
|
expect(ticket.code).toBe(ticketCode);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ------------------------
|
|
||||||
// 4. PARTICIPATE GAME
|
|
||||||
// ------------------------
|
|
||||||
test("Participate to game", async () => {
|
test("Participate to game", async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post("/api/game/participate")
|
.post("/api/game/participate")
|
||||||
.set("Authorization", `Bearer ${token}`)
|
.set("Authorization", `Bearer ${token}`)
|
||||||
.send({ code: ticketCode });
|
.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 () => {
|
test("Get my gains", async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get("/api/game/my-gains")
|
.get("/api/game/my-gains")
|
||||||
@@ -136,11 +99,9 @@ describe("THE TIP TOP - FULL E2E FLOW", () => {
|
|||||||
|
|
||||||
expect(res.statusCode).toBe(200);
|
expect(res.statusCode).toBe(200);
|
||||||
expect(res.body.gains).toBeDefined();
|
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 () => {
|
test("Admin stats - CLIENT access denied", async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.get("/api/admin/stats")
|
.get("/api/admin/stats")
|
||||||
@@ -149,4 +110,4 @@ describe("THE TIP TOP - FULL E2E FLOW", () => {
|
|||||||
expect(res.statusCode).toBe(403);
|
expect(res.statusCode).toBe(403);
|
||||||
expect(res.body.message || res.body).toBeDefined();
|
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();
|
|
||||||
});
|
|
||||||
@@ -41,7 +41,6 @@ export async function getEmailingContactsApi() {
|
|||||||
|
|
||||||
export async function getWinnerApi() {
|
export async function getWinnerApi() {
|
||||||
const res = await fetch('/api/admin/draw', { headers: authHeaders() })
|
const res = await fetch('/api/admin/draw', { headers: authHeaders() })
|
||||||
if (res.status === 404) return null
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (!res.ok) throw apiError(data.message || 'Erreur gagnant.', res.status)
|
if (!res.ok) throw apiError(data.message || 'Erreur gagnant.', res.status)
|
||||||
return data
|
return data
|
||||||
|
|||||||
Reference in New Issue
Block a user