ajout des champs date de naissance et gender sur l'inscription, modification de la permition de lancer le jeu concour par l'administrateur, envoie des mail a chaque lot remporter

This commit is contained in:
Emma781227
2026-08-18 15:06:51 +02:00
parent 8adf780da2
commit fa21d9af2a
19 changed files with 1198 additions and 72 deletions

View File

@@ -1,7 +1,8 @@
const bcrypt = require("bcrypt");
const crypto = require("crypto");
const prisma = require("../config/prisma");
const { getGameWindow, expireOverdueClaims } = require("../utils/gameWindow");
const transporter = require("../config/mailer");
const { getGameWindow } = require("../utils/gameWindow");
const AGE_BRACKETS = ["-18", "18-24", "25-34", "35-44", "45-54", "55-64", "65+"];
@@ -34,7 +35,6 @@ function computeAgeStats(users) {
const getStats = async (req, res) => {
try {
await expireOverdueClaims();
const gameWindow = await getGameWindow();
const [
@@ -80,9 +80,11 @@ const getStats = async (req, res) => {
ticketsUsed,
ticketsRemaining: ticketsTotal - ticketsUsed,
participations,
claimsPending,
// L'expiration n'est pas écrite en base : une fois la fenêtre de retrait
// fermée, les PENDING restants comptent comme annulés.
claimsPending: gameWindow.isOpen ? claimsPending : 0,
claimsDelivered,
claimsCancelled,
claimsCancelled: gameWindow.isOpen ? claimsCancelled : claimsCancelled + claimsPending,
users,
gameWindow,
ageStats: computeAgeStats(clientsWithBirthdate),
@@ -215,6 +217,31 @@ const createEmployee = async (req, res) => {
const drawWinner = async (req, res) => {
try {
// Le grand gagnant est tiré parmi tous les participants : le tirage n'a de
// sens qu'une fois la période de participation close. Avant l'ouverture,
// il n'existe aucun participant à départager.
const gameWindow = await getGameWindow();
if (!gameWindow.configured) {
return res.status(409).json({
message: "Le jeu-concours n'a pas encore été lancé : aucun tirage au sort n'est possible.",
});
}
if (!gameWindow.hasStarted) {
return res.status(409).json({
message: `Le jeu-concours n'a pas encore commencé. Il ouvre le ${gameWindow.startDate.toLocaleDateString("fr-FR")}.`,
startDate: gameWindow.startDate,
});
}
if (gameWindow.isParticipationOpen) {
return res.status(409).json({
message: `Le tirage au sort ne peut avoir lieu qu'après la fin des participations, le ${gameWindow.participationDeadline.toLocaleDateString("fr-FR")}.`,
participationDeadline: gameWindow.participationDeadline,
});
}
const existingWinner = await prisma.grandPrizeWinner.findFirst({
include: {
user: {
@@ -283,9 +310,41 @@ const drawWinner = async (req, res) => {
},
});
// Le tirage est un acte irréversible réalisé sous contrôle d'huissier :
// un email qui ne part pas ne doit surtout pas l'invalider. On signale
// simplement l'échec pour que l'administrateur prévienne autrement.
let emailSent = true;
try {
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: savedWinner.user.email,
subject: "Vous avez gagné le gros lot Thé Tip Top !",
html: `
<div style="font-family:sans-serif;max-width:480px;margin:auto">
<h2 style="color:#006B6B">Félicitations ${savedWinner.user.firstname} !</h2>
<p>Le tirage au sort du jeu-concours « 100% Gagnant » vient d'avoir lieu,
et c'est vous qui remportez le gros lot :</p>
<div style="font-size:20px;font-weight:bold;color:#006B6B;text-align:center;padding:20px;background:#F0F0F0;border-radius:12px">
${savedWinner.prize}
</div>
<p>Notre équipe vous recontactera très prochainement pour convenir des
modalités de remise de votre lot.</p>
<p style="color:#888;font-size:13px">Merci de votre participation et à bientôt chez Thé Tip Top.</p>
</div>
`,
});
} catch (mailError) {
emailSent = false;
console.error("Erreur envoi email au grand gagnant:", mailError);
}
return res.status(201).json({
message: "Tirage au sort effectué avec succès.",
message: emailSent
? "Tirage au sort effectué avec succès. Le gagnant a été prévenu par email."
: "Tirage au sort effectué avec succès, mais l'email n'a pas pu être envoyé au gagnant. Merci de le contacter directement.",
totalEligibleParticipants: participants.length,
emailSent,
winner: savedWinner,
});
} catch (error) {
@@ -296,6 +355,52 @@ const drawWinner = async (req, res) => {
}
};
// Lancement du jeu-concours : fixe (ou corrige) la date de début, dont
// découlent les deux échéances. Une seule ligne de configuration existe, on
// la met donc à jour si elle est déjà présente.
const setGameSettings = async (req, res) => {
try {
const { startDate } = req.body;
const parsedStartDate = new Date(startDate);
const existing = await prisma.gameSettings.findFirst({ orderBy: { id: "asc" } });
if (existing) {
await prisma.gameSettings.update({
where: { id: existing.id },
data: { startDate: parsedStartDate },
});
} else {
await prisma.gameSettings.create({ data: { startDate: parsedStartDate } });
}
const gameWindow = await getGameWindow();
return res.json({
message: existing
? "Dates du jeu-concours mises à jour."
: "Jeu-concours lancé avec succès.",
gameWindow,
});
} catch (error) {
console.error("Erreur configuration du jeu:", error);
return res.status(500).json({
message: "Erreur serveur lors de la configuration du jeu-concours.",
});
}
};
const getGameSettings = async (req, res) => {
try {
return res.json({ gameWindow: await getGameWindow() });
} catch (error) {
console.error("Erreur lecture configuration du jeu:", error);
return res.status(500).json({
message: "Erreur serveur lors de la lecture de la configuration.",
});
}
};
const getGrandPrizeWinner = async (req, res) => {
try {
const winner = await prisma.grandPrizeWinner.findFirst({
@@ -329,4 +434,6 @@ module.exports = {
createEmployee,
drawWinner,
getGrandPrizeWinner,
setGameSettings,
getGameSettings,
};

View File

@@ -62,11 +62,17 @@ const register = async (req, res) => {
},
});
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: email,
subject: "Votre code de validation Thé Tip Top",
html: `
// Le code de validation est le seul moyen d'activer le compte. Si l'envoi
// échoue, laisser l'utilisateur en base le bloquerait définitivement : il
// ne pourrait ni se connecter (email non vérifié) ni se réinscrire (email
// déjà pris). On annule donc la création pour que la tentative reste
// rejouable.
try {
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: email,
subject: "Votre code de validation Thé Tip Top",
html: `
<div style="font-family:sans-serif;max-width:480px;margin:auto">
<h2 style="color:#006B6B">Bienvenue ${firstname} !</h2>
<p>Merci pour votre inscription sur Thé Tip Top.</p>
@@ -77,7 +83,21 @@ const register = async (req, res) => {
<p style="color:#888;font-size:13px">Ce code expire dans <strong>15 minutes</strong>.</p>
</div>
`,
});
});
} catch (mailError) {
console.error("Erreur envoi du code de validation:", mailError);
try {
await prisma.user.delete({ where: { id: user.id } });
} catch (rollbackError) {
console.error("Echec de l'annulation de l'inscription:", rollbackError);
}
return res.status(502).json({
message:
"Votre compte n'a pas pu être créé : l'email de validation n'a pas pu être envoyé. Merci de réessayer dans quelques instants.",
});
}
return res.status(201).json({
message: "Inscription réussie. Un code de validation a été envoyé par email.",
@@ -435,10 +455,13 @@ const deleteAccount = async (req, res) => {
// 4. Supprimer les participations
await tx.participation.deleteMany({ where: { userId } });
// 5. Libérer les tickets utilisés par ce user
// 5. Détacher les tickets du compte sans les remettre en jeu : un code
// figure sur un ticket de caisse physique et n'est jouable qu'une
// fois. Le réinitialiser permettrait de rejouer indéfiniment le même
// code en supprimant puis recréant un compte.
await tx.ticket.updateMany({
where: { userId },
data: { userId: null, isUsed: false, usedAt: null },
data: { userId: null },
});
// 6. Supprimer l'utilisateur

View File

@@ -1,11 +1,11 @@
const prisma = require("../config/prisma");
const { expireOverdueClaims } = require("../utils/gameWindow");
const { getGameWindow, effectiveClaimStatus } = require("../utils/gameWindow");
const getGainByTicketCode = async (req, res) => {
try {
const { code } = req.params;
await expireOverdueClaims();
const gameWindow = await getGameWindow();
const ticket = await prisma.ticket.findUnique({
where: { code: code.toUpperCase() },
@@ -52,7 +52,7 @@ const getGainByTicketCode = async (req, res) => {
},
claim: {
id: ticket.participation.claim.id,
status: ticket.participation.claim.status,
status: effectiveClaimStatus(ticket.participation.claim.status, gameWindow),
claimedAt: ticket.participation.claim.claimedAt,
},
});
@@ -69,7 +69,7 @@ const deliverClaim = async (req, res) => {
const { id } = req.params;
const employeeId = req.user.id;
await expireOverdueClaims();
const gameWindow = await getGameWindow();
const claim = await prisma.claim.findUnique({
where: { id: Number(id) },
@@ -87,7 +87,7 @@ const deliverClaim = async (req, res) => {
});
}
if (claim.status === "CANCELLED") {
if (effectiveClaimStatus(claim.status, gameWindow) === "CANCELLED") {
return res.status(400).json({
message: "Ce gain est expiré et ne peut plus être remis.",
});

View File

@@ -1,5 +1,13 @@
const prisma = require("../config/prisma");
const { getGameWindow, expireOverdueClaims } = require("../utils/gameWindow");
const transporter = require("../config/mailer");
const {
getGameWindow,
effectiveClaimStatus,
participationClosedMessage,
} = require("../utils/gameWindow");
// Levée par la transaction quand le ticket a été consommé entre-temps.
class TicketDejaUtiliseError extends Error {}
const participate = async (req, res) => {
try {
@@ -7,9 +15,13 @@ const participate = async (req, res) => {
const userId = req.user.id;
const gameWindow = await getGameWindow();
if (!gameWindow.isOpen) {
// Un code n'est jouable que pendant les 30 jours de jeu : ni avant
// l'ouverture, ni pendant la prolongation réservée aux retraits.
if (!gameWindow.isParticipationOpen) {
return res.status(410).json({
message: "Le jeu-concours est terminé. Vous ne pouvez plus tester de code ni réclamer de lot.",
message: participationClosedMessage(gameWindow),
phase: gameWindow.phase,
});
}
@@ -33,16 +45,24 @@ const participate = async (req, res) => {
}
const result = await prisma.$transaction(async (tx) => {
const updatedTicket = await tx.ticket.update({
where: { id: ticket.id },
// Le contrôle isUsed ci-dessus ne suffit pas : deux requêtes simultanées
// peuvent le franchir ensemble. On repasse donc par un update
// conditionnel, atomique, qui ne touche le ticket que s'il est encore
// libre — le perdant de la course obtient 0 ligne modifiée.
const { count } = await tx.ticket.updateMany({
where: { id: ticket.id, isUsed: false },
data: {
isUsed: true,
usedAt: new Date(),
userId,
},
include: {
lot: true,
},
});
if (count === 0) throw new TicketDejaUtiliseError();
const updatedTicket = await tx.ticket.findUnique({
where: { id: ticket.id },
include: { lot: true },
});
const participation = await tx.participation.create({
@@ -67,8 +87,41 @@ const participate = async (req, res) => {
};
});
// La participation est déjà enregistrée et consultable dans « Mes gains » :
// un email qui ne part pas ne doit pas l'annuler ni consommer le ticket
// pour rien. On se contente de signaler l'échec.
let emailSent = true;
try {
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: req.user.email,
subject: `Vous avez gagné : ${result.ticket.lot.name} !`,
html: `
<div style="font-family:sans-serif;max-width:480px;margin:auto">
<h2 style="color:#006B6B">Bravo ${req.user.firstname} !</h2>
<p>Votre code <strong>${result.ticket.code}</strong> est gagnant. Vous remportez :</p>
<div style="font-size:20px;font-weight:bold;color:#006B6B;text-align:center;padding:20px;background:#F0F0F0;border-radius:12px">
${result.ticket.lot.name}
</div>
${result.ticket.lot.description ? `<p style="text-align:center;color:#666">${result.ticket.lot.description}</p>` : ""}
<p>Présentez-vous en boutique avec ce code pour retirer votre lot${
gameWindow.claimDeadline
? `, avant le <strong>${gameWindow.claimDeadline.toLocaleDateString("fr-FR")}</strong>`
: ""
}.</p>
<p style="color:#888;font-size:13px">Retrouvez à tout moment vos gains dans votre espace « Mes gains ».</p>
</div>
`,
});
} catch (mailError) {
emailSent = false;
console.error("Erreur envoi email de gain:", mailError);
}
return res.status(201).json({
message: "Participation enregistrée avec succès.",
emailSent,
gain: {
name: result.ticket.lot.name,
description: result.ticket.lot.description,
@@ -80,6 +133,12 @@ const participate = async (req, res) => {
},
});
} catch (error) {
if (error instanceof TicketDejaUtiliseError) {
return res.status(409).json({
message: "Ce code a déjà été utilisé.",
});
}
console.error("Erreur participation:", error);
return res.status(500).json({
message: "Erreur serveur lors de la participation.",
@@ -91,7 +150,7 @@ const myGains = async (req, res) => {
try {
const userId = req.user.id;
await expireOverdueClaims();
const gameWindow = await getGameWindow();
const participations = await prisma.participation.findMany({
where: { userId },
@@ -112,7 +171,9 @@ const myGains = async (req, res) => {
lot: participation.lot.name,
description: participation.lot.description,
value: participation.lot.value,
claimStatus: participation.claim?.status,
claimStatus: participation.claim
? effectiveClaimStatus(participation.claim.status, gameWindow)
: undefined,
wonAt: participation.createdAt,
})),
});

View File

@@ -6,12 +6,14 @@ const {
createEmployee,
drawWinner,
getGrandPrizeWinner,
setGameSettings,
getGameSettings,
} = require("../controllers/admin.controller");
const authMiddleware = require("../middlewares/auth.middleware");
const roleMiddleware = require("../middlewares/role.middleware");
const validate = require("../middlewares/validate.middleware");
const { createEmployeeSchema } = require("../validations/admin.validation");
const { createEmployeeSchema, gameSettingsSchema } = require("../validations/admin.validation");
const router = express.Router();
@@ -109,4 +111,44 @@ router.post("/draw", drawWinner);
*/
router.get("/draw", getGrandPrizeWinner);
/**
* @swagger
* /api/admin/game-settings:
* get:
* summary: Fenêtre du jeu-concours (début, fin des participations, fin des retraits)
* tags:
* - Admin
* security:
* - bearerAuth: []
* responses:
* 200:
* description: Configuration courante du jeu-concours
* put:
* summary: Lance le jeu-concours ou corrige sa date de début
* tags:
* - Admin
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - startDate
* properties:
* startDate:
* type: string
* format: date-time
* example: 2026-09-01T00:00:00.000Z
* responses:
* 200:
* description: Jeu-concours lancé ou mis à jour
* 400:
* description: Date de début invalide
*/
router.get("/game-settings", getGameSettings);
router.put("/game-settings", validate(gameSettingsSchema), setGameSettings);
module.exports = router;

View File

@@ -1,6 +1,7 @@
const express = require("express");
const { participate, myGains } = require("../controllers/game.controller");
const authMiddleware = require("../middlewares/auth.middleware");
const roleMiddleware = require("../middlewares/role.middleware");
const validate = require("../middlewares/validate.middleware");
const { participateSchema } = require("../validations/game.validation");
@@ -20,7 +21,9 @@ const router = express.Router();
* description: Participation enregistrée
*/
router.post("/participate", authMiddleware, validate(participateSchema), participate);
// Le jeu est reserve aux clients : un ADMIN ou un EMPLOYEE qui joue
// consommerait des tickets et fausserait les statistiques.
router.post("/participate", authMiddleware, roleMiddleware("CLIENT"), validate(participateSchema), participate);
/**
* @swagger
@@ -36,6 +39,6 @@ router.post("/participate", authMiddleware, validate(participateSchema), partici
* description: Liste des gains
*/
router.get("/my-gains", authMiddleware, myGains);
router.get("/my-gains", authMiddleware, roleMiddleware("CLIENT"), myGains);
module.exports = router;

View File

@@ -1,23 +1,41 @@
const prisma = require("../config/prisma");
// Le concours traverse cinq états, et non deux : confondre « pas encore
// ouvert » avec « terminé » produit des messages faux côté joueur et laisse
// passer le tirage au sort avant le début du jeu.
const PHASES = {
NOT_CONFIGURED: "NOT_CONFIGURED",
SCHEDULED: "SCHEDULED",
OPEN: "OPEN",
CLAIM_ONLY: "CLAIM_ONLY",
CLOSED: "CLOSED",
};
const GAME_DURATION_DAYS = 30;
const CLAIM_EXTENSION_DAYS = 30;
const DAY_MS = 24 * 60 * 60 * 1000;
// Fenêtre pendant laquelle un joueur peut à la fois tester son code et
// réclamer son lot : 30 jours de jeu + 30 jours supplémentaires à compter
// de startDate. Tant qu'aucune GameSettings n'existe, le jeu est considéré
// comme non démarré et donc ouvert (comportement de dev par défaut).
async function getGameWindow() {
// Deux échéances distinctes à partir de startDate :
// - participationDeadline (30 j) : dernier moment pour tester un code ;
// - claimDeadline (60 j) : dernier moment pour retirer un lot déjà gagné.
// Les 30 jours supplémentaires servent uniquement au retrait, pas au jeu.
// Tant qu'aucune GameSettings n'existe, le jeu est considéré comme non
// configuré et donc ouvert (comportement de dev par défaut).
async function getGameWindow(now = new Date()) {
const settings = await prisma.gameSettings.findFirst({ orderBy: { id: "asc" } });
// Tant que l'administrateur n'a pas fixé de date, le concours n'existe pas :
// rien n'est jouable et aucun tirage n'est possible. Ouvrir par défaut
// reviendrait à faire gagner des lots dans un jeu qui n'a jamais été lancé.
if (!settings) {
return {
configured: false,
phase: PHASES.NOT_CONFIGURED,
startDate: null,
participationDeadline: null,
claimDeadline: null,
isOpen: true,
isParticipationOpen: false,
isOpen: false,
};
}
@@ -26,26 +44,57 @@ async function getGameWindow() {
settings.startDate.getTime() + (GAME_DURATION_DAYS + CLAIM_EXTENSION_DAYS) * DAY_MS
);
const hasStarted = now >= settings.startDate;
const isParticipationOpen = hasStarted && now <= participationDeadline;
const isOpen = hasStarted && now <= claimDeadline;
let phase;
if (!hasStarted) phase = PHASES.SCHEDULED;
else if (isParticipationOpen) phase = PHASES.OPEN;
else if (isOpen) phase = PHASES.CLAIM_ONLY;
else phase = PHASES.CLOSED;
return {
configured: true,
phase,
startDate: settings.startDate,
participationDeadline,
claimDeadline,
isOpen: new Date() <= claimDeadline,
hasStarted,
isParticipationOpen,
isOpen,
};
}
// Bascule en CANCELLED les gains PENDING non réclamés une fois la fenêtre
// fermée. Appelé au fil de l'eau (pas de cron) avant toute lecture/écriture
// de claims, ce qui suffit pour ce projet.
async function expireOverdueClaims() {
const window = await getGameWindow();
if (window.isOpen) return;
await prisma.claim.updateMany({
where: { status: "PENDING" },
data: { status: "CANCELLED" },
});
// L'expiration d'un gain est *dérivée* de la fenêtre de jeu, jamais écrite en
// base : un PENDING dont le délai de retrait est passé est présenté comme
// CANCELLED, mais la ligne reste intacte. Corriger une startDate erronée
// suffit donc à rétablir les gains, et aucune lecture ne modifie la base.
function effectiveClaimStatus(status, gameWindow) {
if (status === "PENDING" && !gameWindow.isOpen) return "CANCELLED";
return status;
}
module.exports = { getGameWindow, expireOverdueClaims };
// Message destiné au joueur qui tente de jouer hors de la fenêtre : chaque
// phase a sa propre explication, avec la date utile quand elle existe.
function participationClosedMessage(gameWindow) {
switch (gameWindow.phase) {
case PHASES.NOT_CONFIGURED:
return "Le jeu-concours n'est pas encore ouvert. Revenez prochainement.";
case PHASES.SCHEDULED:
return `Le jeu-concours ouvrira le ${gameWindow.startDate.toLocaleDateString("fr-FR")}.`;
case PHASES.CLAIM_ONLY:
return "La période de participation est terminée. Vous pouvez encore retirer les lots déjà gagnés.";
default:
return "Le jeu-concours est terminé. Vous ne pouvez plus tester de code ni réclamer de lot.";
}
}
module.exports = {
getGameWindow,
effectiveClaimStatus,
participationClosedMessage,
PHASES,
GAME_DURATION_DAYS,
CLAIM_EXTENSION_DAYS,
};

View File

@@ -9,6 +9,13 @@ const createEmployeeSchema = z.object({
gender: z.string().optional(),
});
const gameSettingsSchema = z.object({
startDate: z
.string()
.refine((value) => !Number.isNaN(new Date(value).getTime()), "Date de début invalide"),
});
module.exports = {
createEmployeeSchema,
gameSettingsSchema,
};

View File

@@ -0,0 +1,106 @@
const request = require("supertest");
const app = require("../src/app");
const prisma = require("../src/config/prisma");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
const mailer = require("../src/config/mailer");
jest.mock("../src/config/mailer", () => ({ sendMail: jest.fn() }));
const DAY = 86400000;
const PREFIX = "notif-";
const CODE = "NOTIFTICK1";
let ctx = {};
async function cleanup() {
const us = await prisma.user.findMany({ where: { email: { contains: PREFIX } } });
const ids = us.map((u) => u.id);
const ps = await prisma.participation.findMany({ where: { userId: { in: ids } } });
await prisma.claim.deleteMany({ where: { participationId: { in: ps.map((p) => p.id) } } });
await prisma.participation.deleteMany({ where: { userId: { in: ids } } });
await prisma.grandPrizeWinner.deleteMany({ where: { userId: { in: ids } } });
await prisma.ticket.deleteMany({ where: { code: CODE } });
await prisma.user.deleteMany({ where: { id: { in: ids } } });
}
const tk = (u) => jwt.sign({ id: u.id, role: u.role }, process.env.JWT_SECRET, { expiresIn: "1h" });
const jourJ = (d) =>
prisma.gameSettings.update({
where: { id: ctx.settingsId },
data: { startDate: new Date(Date.now() - d * DAY) },
});
beforeAll(async () => {
await cleanup();
const row = await prisma.gameSettings.findFirst({ orderBy: { id: "asc" } });
ctx.settingsId = row.id;
ctx.startDateOriginale = row.startDate;
const lot = await prisma.lot.findFirst({ orderBy: { id: "asc" } });
await prisma.ticket.create({ data: { code: CODE, lotId: lot.id } });
const h = await bcrypt.hash("Password123", 10);
ctx.admin = await prisma.user.create({ data: { firstname: "Alice", lastname: "Admin", email: PREFIX + "admin@test.local", password: h, role: "ADMIN", isEmailVerified: true } });
ctx.client = await prisma.user.create({ data: { firstname: "Chloe", lastname: "Martin", email: PREFIX + "chloe@test.local", password: h, role: "CLIENT", isEmailVerified: true } });
ctx.tAdmin = tk(ctx.admin);
await jourJ(1);
mailer.sendMail.mockResolvedValue(true);
await request(app).post("/api/game/participate").set("Authorization", "Bearer " + tk(ctx.client)).send({ code: CODE }).expect(201);
await jourJ(35);
});
afterAll(async () => {
await prisma.gameSettings.update({ where: { id: ctx.settingsId }, data: { startDate: ctx.startDateOriginale } });
await cleanup();
await prisma.$disconnect();
});
describe("Notification du grand gagnant", () => {
beforeEach(() => jest.clearAllMocks());
test("le gagnant recoit un email a son adresse", async () => {
mailer.sendMail.mockResolvedValue(true);
const res = await request(app).post("/api/admin/draw").set("Authorization", "Bearer " + ctx.tAdmin).expect(201);
expect(res.body.emailSent).toBe(true);
expect(mailer.sendMail).toHaveBeenCalledTimes(1);
const envoi = mailer.sendMail.mock.calls[0][0];
expect(envoi.to).toBe(ctx.client.email);
expect(envoi.subject).toMatch(/gros lot/i);
expect(envoi.html).toContain("Chloe");
expect(envoi.html).toContain("360");
await prisma.grandPrizeWinner.deleteMany({ where: { userId: ctx.client.id } });
});
test("un SMTP en panne n invalide pas le tirage", async () => {
mailer.sendMail.mockRejectedValue(new Error("SMTP indisponible"));
const res = await request(app).post("/api/admin/draw").set("Authorization", "Bearer " + ctx.tAdmin);
expect(res.status).toBe(201);
expect(res.body.emailSent).toBe(false);
expect(res.body.message).toMatch(/contacter directement/);
// Le tirage reste enregistre : c est un acte irreversible.
const enBase = await prisma.grandPrizeWinner.findFirst({ where: { userId: ctx.client.id } });
expect(enBase).not.toBeNull();
await prisma.grandPrizeWinner.deleteMany({ where: { userId: ctx.client.id } });
});
test("aucun email n est envoye si le tirage est refuse", async () => {
mailer.sendMail.mockResolvedValue(true);
await jourJ(1);
const res = await request(app).post("/api/admin/draw").set("Authorization", "Bearer " + ctx.tAdmin);
expect(res.status).toBe(409);
expect(mailer.sendMail).not.toHaveBeenCalled();
await jourJ(35);
});
});

View File

@@ -0,0 +1,113 @@
const request = require("supertest");
const app = require("../src/app");
const prisma = require("../src/config/prisma");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
const mailer = require("../src/config/mailer");
jest.mock("../src/config/mailer", () => ({ sendMail: jest.fn() }));
const DAY = 86400000;
const PREFIX = "gain-";
const CODES = ["GAINTICK01", "GAINTICK02", "GAINTICK03"];
let ctx = {};
async function cleanup() {
const us = await prisma.user.findMany({ where: { email: { contains: PREFIX } } });
const ids = us.map((u) => u.id);
const ps = await prisma.participation.findMany({ where: { userId: { in: ids } } });
await prisma.claim.deleteMany({ where: { participationId: { in: ps.map((p) => p.id) } } });
await prisma.participation.deleteMany({ where: { userId: { in: ids } } });
await prisma.grandPrizeWinner.deleteMany({ where: { userId: { in: ids } } });
await prisma.ticket.deleteMany({ where: { code: { in: CODES } } });
await prisma.user.deleteMany({ where: { id: { in: ids } } });
}
const tk = (u) => jwt.sign({ id: u.id, role: u.role }, process.env.JWT_SECRET, { expiresIn: "1h" });
const jourJ = (d) =>
prisma.gameSettings.update({
where: { id: ctx.settingsId },
data: { startDate: new Date(Date.now() - d * DAY) },
});
beforeAll(async () => {
await cleanup();
const row = await prisma.gameSettings.findFirst({ orderBy: { id: "asc" } });
ctx.settingsId = row.id;
ctx.startDateOriginale = row.startDate;
ctx.lot = await prisma.lot.findFirst({ orderBy: { id: "asc" } });
for (const c of CODES) await prisma.ticket.create({ data: { code: c, lotId: ctx.lot.id } });
const h = await bcrypt.hash("Password123", 10);
ctx.client = await prisma.user.create({ data: { firstname: "Chloe", lastname: "Martin", email: PREFIX + "chloe@test.local", password: h, role: "CLIENT", isEmailVerified: true } });
ctx.tClient = tk(ctx.client);
await jourJ(1);
});
afterAll(async () => {
await prisma.gameSettings.update({ where: { id: ctx.settingsId }, data: { startDate: ctx.startDateOriginale } });
await cleanup();
await prisma.$disconnect();
});
describe("Notification du gain a chaque participation", () => {
beforeEach(() => jest.clearAllMocks());
test("le joueur recoit un email decrivant son lot", async () => {
mailer.sendMail.mockResolvedValue(true);
const res = await request(app)
.post("/api/game/participate")
.set("Authorization", "Bearer " + ctx.tClient)
.send({ code: CODES[0] })
.expect(201);
expect(res.body.emailSent).toBe(true);
expect(mailer.sendMail).toHaveBeenCalledTimes(1);
const envoi = mailer.sendMail.mock.calls[0][0];
expect(envoi.to).toBe(ctx.client.email);
expect(envoi.subject).toContain(ctx.lot.name);
expect(envoi.html).toContain("Chloe");
expect(envoi.html).toContain(CODES[0]);
expect(envoi.html).toContain(ctx.lot.name);
});
test("un SMTP en panne n annule pas la participation", async () => {
mailer.sendMail.mockRejectedValue(new Error("SMTP indisponible"));
const res = await request(app)
.post("/api/game/participate")
.set("Authorization", "Bearer " + ctx.tClient)
.send({ code: CODES[1] });
expect(res.status).toBe(201);
expect(res.body.emailSent).toBe(false);
// Le gain reste acquis et consultable.
const t = await prisma.ticket.findUnique({ where: { code: CODES[1] } });
expect(t.isUsed).toBe(true);
const gains = await request(app).get("/api/game/my-gains").set("Authorization", "Bearer " + ctx.tClient).expect(200);
expect(gains.body.gains.some((g) => g.ticketCode === CODES[1])).toBe(true);
});
test("aucun email si le code est refuse", async () => {
mailer.sendMail.mockResolvedValue(true);
const rejoue = await request(app).post("/api/game/participate").set("Authorization", "Bearer " + ctx.tClient).send({ code: CODES[0] });
expect(rejoue.status).toBe(409);
const inconnu = await request(app).post("/api/game/participate").set("Authorization", "Bearer " + ctx.tClient).send({ code: "INEXISTAN1" });
expect(inconnu.status).toBe(404);
await jourJ(45);
const horsDelai = await request(app).post("/api/game/participate").set("Authorization", "Bearer " + ctx.tClient).send({ code: CODES[2] });
expect(horsDelai.status).toBe(410);
await jourJ(1);
expect(mailer.sendMail).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,163 @@
const request = require("supertest");
const app = require("../src/app");
const prisma = require("../src/config/prisma");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
jest.mock("../src/config/mailer", () => ({ sendMail: jest.fn().mockResolvedValue(true) }));
const emails = { adm: "v-adm@test.local", emp: "v-emp@test.local", a: "v-a@test.local", b: "v-b@test.local" };
const CODES = ["VERIFTICK1", "VERIFTICK2", "VERIFTICK3", "VERIFTICK4", "VERIFTICK5"];
const DAY = 86400000;
let ctx = {};
const log = (...a) => console.log(" ", ...a);
async function cleanup() {
const us = await prisma.user.findMany({ where: { email: { in: [...Object.values(emails), "v-r1@test.local", "v-r2@test.local"] } } });
const ids = us.map((u) => u.id);
const ps = await prisma.participation.findMany({ where: { userId: { in: ids } } });
await prisma.claim.deleteMany({ where: { participationId: { in: ps.map((p) => p.id) } } });
await prisma.participation.deleteMany({ where: { userId: { in: ids } } });
await prisma.grandPrizeWinner.deleteMany({ where: { userId: { in: ids } } });
await prisma.ticket.deleteMany({ where: { code: { in: CODES } } });
await prisma.user.deleteMany({ where: { id: { in: ids } } });
}
const setStart = (d) =>
prisma.gameSettings.update({ where: { id: ctx.settings.id }, data: { startDate: new Date(Date.now() - d * DAY) } });
beforeAll(async () => {
await cleanup();
ctx.settings = await prisma.gameSettings.findFirst({ orderBy: { id: "asc" } });
ctx.orig = ctx.settings.startDate;
const lot = await prisma.lot.findFirst({ orderBy: { id: "asc" } });
for (const c of CODES) await prisma.ticket.create({ data: { code: c, lotId: lot.id } });
const h = await bcrypt.hash("Password123", 10);
const mk = (e, r) => prisma.user.create({ data: { firstname: "V", lastname: r, email: e, password: h, role: r, isEmailVerified: true } });
ctx.adm = await mk(emails.adm, "ADMIN");
ctx.emp = await mk(emails.emp, "EMPLOYEE");
ctx.a = await mk(emails.a, "CLIENT");
ctx.b = await mk(emails.b, "CLIENT");
const tk = (u) => jwt.sign({ id: u.id, role: u.role }, process.env.JWT_SECRET, { expiresIn: "1h" });
ctx.tadm = tk(ctx.adm);
ctx.temp = tk(ctx.emp);
ctx.ta = tk(ctx.a);
ctx.tb = tk(ctx.b);
});
afterAll(async () => {
await prisma.gameSettings.update({ where: { id: ctx.settings.id }, data: { startDate: ctx.orig } });
await cleanup();
await prisma.$disconnect();
});
describe("VERIFICATION DES 7 CORRECTIFS", () => {
test("#1 ADMIN peut lancer le jeu", async () => {
const futur = new Date(Date.now() + 5 * DAY).toISOString();
const r = await request(app).put("/api/admin/game-settings").set("Authorization", `Bearer ${ctx.tadm}`).send({ startDate: futur });
log("PUT game-settings ->", r.status, r.body.message);
expect(r.status).toBe(200);
const g = await request(app).get("/api/admin/game-settings").set("Authorization", `Bearer ${ctx.tadm}`).expect(200);
log("GET game-settings -> participation ouverte :", g.body.gameWindow.isParticipationOpen, "(jeu pas encore commence)");
const bad = await request(app).put("/api/admin/game-settings").set("Authorization", `Bearer ${ctx.tadm}`).send({ startDate: "pas-une-date" });
log("date invalide ->", bad.status, bad.body.message);
expect(bad.status).toBe(400);
const nonAdmin = await request(app).put("/api/admin/game-settings").set("Authorization", `Bearer ${ctx.ta}`).send({ startDate: futur });
log("client tente de lancer le jeu ->", nonAdmin.status);
expect(nonAdmin.status).toBe(403);
});
test("#2 participation refusee apres 30 jours", async () => {
await setStart(45);
const r = await request(app).post("/api/game/participate").set("Authorization", `Bearer ${ctx.ta}`).send({ code: CODES[0] });
log("jour 45 ->", r.status, r.body.message);
expect(r.status).toBe(410);
await setStart(1);
const ok = await request(app).post("/api/game/participate").set("Authorization", `Bearer ${ctx.ta}`).send({ code: CODES[0] });
log("jour 1 ->", ok.status, ok.body.message || "accepte");
expect(ok.status).toBe(201);
ctx.claimId = ok.body.claim.id;
});
test("#3 expiration derivee et reversible", async () => {
await setStart(70);
const g1 = await request(app).get("/api/game/my-gains").set("Authorization", `Bearer ${ctx.ta}`).expect(200);
log("jour 70, statut affiche :", g1.body.gains.map((g) => g.claimStatus).join(","));
expect(g1.body.gains[0].claimStatus).toBe("CANCELLED");
const enBase = await prisma.claim.findUnique({ where: { id: ctx.claimId } });
log("statut REEL en base :", enBase.status, "(non ecrase)");
expect(enBase.status).toBe("PENDING");
await setStart(1);
const g2 = await request(app).get("/api/game/my-gains").set("Authorization", `Bearer ${ctx.ta}`).expect(200);
log("retour jour 1, statut :", g2.body.gains.map((g) => g.claimStatus).join(","), "(reversible)");
expect(g2.body.gains[0].claimStatus).toBe("PENDING");
});
test("#4 tirage refuse pendant le jeu", async () => {
await setStart(1);
const r = await request(app).post("/api/admin/draw").set("Authorization", `Bearer ${ctx.tadm}`);
log("tirage pendant le jeu ->", r.status, r.body.message);
expect(r.status).toBe(409);
await setStart(35);
const r2 = await request(app).post("/api/admin/draw").set("Authorization", `Bearer ${ctx.tadm}`);
log("tirage apres cloture ->", r2.status, r2.body.message, r2.body.totalEligibleParticipants ? `(${r2.body.totalEligibleParticipants} eligibles)` : "");
expect(r2.status).toBe(201);
await prisma.grandPrizeWinner.deleteMany({ where: { userId: { in: [ctx.a.id, ctx.b.id] } } });
await setStart(1);
});
test("#5 admin et employe ne peuvent plus jouer", async () => {
const r1 = await request(app).post("/api/game/participate").set("Authorization", `Bearer ${ctx.tadm}`).send({ code: CODES[1] });
const r2 = await request(app).post("/api/game/participate").set("Authorization", `Bearer ${ctx.temp}`).send({ code: CODES[1] });
log("ADMIN ->", r1.status, r1.body.message);
log("EMPLOYE ->", r2.status, r2.body.message);
expect(r1.status).toBe(403);
expect(r2.status).toBe(403);
const t = await prisma.ticket.findUnique({ where: { code: CODES[1] } });
log("le ticket reste vierge :", t.isUsed === false);
expect(t.isUsed).toBe(false);
});
test("#6 suppression de compte ne remet pas le ticket en jeu", async () => {
await request(app).post("/api/game/participate").set("Authorization", `Bearer ${ctx.tb}`).send({ code: CODES[2] }).expect(201);
await request(app).delete("/api/auth/account").set("Authorization", `Bearer ${ctx.tb}`).expect(200);
const t = await prisma.ticket.findUnique({ where: { code: CODES[2] } });
log("apres suppression : isUsed =", t.isUsed, "| userId =", t.userId);
expect(t.isUsed).toBe(true);
const rejoue = await request(app).post("/api/game/participate").set("Authorization", `Bearer ${ctx.ta}`).send({ code: CODES[2] });
log("tentative de rejouer le code ->", rejoue.status, rejoue.body.message);
expect(rejoue.status).toBe(409);
});
test("#7 course sur un meme code", async () => {
const h = await bcrypt.hash("Password123", 10);
const c1 = await prisma.user.create({ data: { firstname: "R", lastname: "1", email: "v-r1@test.local", password: h, role: "CLIENT", isEmailVerified: true } });
const c2 = await prisma.user.create({ data: { firstname: "R", lastname: "2", email: "v-r2@test.local", password: h, role: "CLIENT", isEmailVerified: true } });
const t1 = jwt.sign({ id: c1.id, role: "CLIENT" }, process.env.JWT_SECRET, { expiresIn: "1h" });
const t2 = jwt.sign({ id: c2.id, role: "CLIENT" }, process.env.JWT_SECRET, { expiresIn: "1h" });
const [r1, r2] = await Promise.all([
request(app).post("/api/game/participate").set("Authorization", `Bearer ${t1}`).send({ code: CODES[3] }),
request(app).post("/api/game/participate").set("Authorization", `Bearer ${t2}`).send({ code: CODES[3] }),
]);
const codes = [r1.status, r2.status].sort();
log("statuts obtenus :", codes.join(" + "), "(201 + 409 attendu, aucun 500)");
expect(codes).toEqual([201, 409]);
const n = await prisma.participation.count({ where: { ticket: { code: CODES[3] } } });
log("participations pour ce ticket :", n);
expect(n).toBe(1);
});
});

View File

@@ -0,0 +1,145 @@
const request = require("supertest");
const app = require("../src/app");
const prisma = require("../src/config/prisma");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
jest.mock("../src/config/mailer", () => ({ sendMail: jest.fn().mockResolvedValue(true) }));
const DAY = 86400000;
const PREFIX = "r2-";
const CODES = ["R2TICKET01", "R2TICKET02", "R2TICKET03", "R2TICKET04"];
let ctx = {};
const log = (...a) => console.log(" ", ...a);
const titre = (t) => console.log("\n===== " + t + " =====");
async function cleanup() {
const us = await prisma.user.findMany({ where: { email: { contains: PREFIX } } });
const ids = us.map((u) => u.id);
const ps = await prisma.participation.findMany({ where: { userId: { in: ids } } });
await prisma.claim.deleteMany({ where: { participationId: { in: ps.map((p) => p.id) } } });
await prisma.participation.deleteMany({ where: { userId: { in: ids } } });
await prisma.grandPrizeWinner.deleteMany({ where: { userId: { in: ids } } });
await prisma.ticket.deleteMany({ where: { code: { in: CODES } } });
await prisma.user.deleteMany({ where: { id: { in: ids } } });
}
async function jourJ(offsetJours) {
const row = await prisma.gameSettings.findFirst({ orderBy: { id: "asc" } });
const debut = new Date(Date.now() - offsetJours * DAY);
if (row) await prisma.gameSettings.update({ where: { id: row.id }, data: { startDate: debut } });
else await prisma.gameSettings.create({ data: { startDate: debut } });
}
const tk = (u) => jwt.sign({ id: u.id, role: u.role }, process.env.JWT_SECRET, { expiresIn: "2h" });
beforeAll(async () => {
await cleanup();
const row = await prisma.gameSettings.findFirst({ orderBy: { id: "asc" } });
ctx.startDateOriginale = row ? row.startDate : null;
const lot = await prisma.lot.findFirst({ orderBy: { id: "asc" } });
for (const c of CODES) await prisma.ticket.create({ data: { code: c, lotId: lot.id } });
const h = await bcrypt.hash("Password123", 10);
ctx.admin = await prisma.user.create({ data: { firstname: "Alice", lastname: "Admin", email: PREFIX + "admin@test.local", password: h, role: "ADMIN", isEmailVerified: true } });
ctx.client = await prisma.user.create({ data: { firstname: "Zoe", lastname: "Cliente", email: PREFIX + "zoe@test.local", password: h, role: "CLIENT", isEmailVerified: true } });
ctx.tAdmin = tk(ctx.admin);
ctx.tClient = tk(ctx.client);
});
afterAll(async () => {
const row = await prisma.gameSettings.findFirst({ orderBy: { id: "asc" } });
if (ctx.startDateOriginale) {
if (row) await prisma.gameSettings.update({ where: { id: row.id }, data: { startDate: ctx.startDateOriginale } });
else await prisma.gameSettings.create({ data: { startDate: ctx.startDateOriginale } });
}
await cleanup();
await prisma.$disconnect();
});
describe("VERIFICATION DES 3 CORRECTIFS DE PHASE", () => {
test("A. rien nest jouable avant tout lancement", async () => {
titre("A : aucune GameSettings en base");
await prisma.gameSettings.deleteMany({});
const vue = await request(app).get("/api/admin/game-settings").set("Authorization", "Bearer " + ctx.tAdmin).expect(200);
log("phase :", vue.body.gameWindow.phase, "| participations ouvertes :", vue.body.gameWindow.isParticipationOpen);
expect(vue.body.gameWindow.phase).toBe("NOT_CONFIGURED");
const essai = await request(app).post("/api/game/participate").set("Authorization", "Bearer " + ctx.tClient).send({ code: CODES[0] });
log("un client tente de jouer ->", essai.status, essai.body.message);
expect(essai.status).toBe(410);
const t = await prisma.ticket.findUnique({ where: { code: CODES[0] } });
log("le ticket reste vierge :", t.isUsed === false);
expect(t.isUsed).toBe(false);
const tirage = await request(app).post("/api/admin/draw").set("Authorization", "Bearer " + ctx.tAdmin);
log("admin tente le tirage ->", tirage.status, tirage.body.message);
expect(tirage.status).toBe(409);
expect(await prisma.grandPrizeWinner.count()).toBe(0);
});
test("B. jeu programme : message juste et tirage bloque", async () => {
titre("B : lancement programme a J+7");
const futur = new Date(Date.now() + 7 * DAY).toISOString();
const r = await request(app).put("/api/admin/game-settings").set("Authorization", "Bearer " + ctx.tAdmin).send({ startDate: futur });
log("PUT game-settings ->", r.status, "| phase :", r.body.gameWindow.phase);
expect(r.body.gameWindow.phase).toBe("SCHEDULED");
const anticipe = await request(app).post("/api/game/participate").set("Authorization", "Bearer " + ctx.tClient).send({ code: CODES[0] });
log("client joue avant ouverture ->", anticipe.status);
log("message :", JSON.stringify(anticipe.body.message));
expect(anticipe.body.message).toMatch(/ouvrira le/);
expect(anticipe.body.message).not.toMatch(/terminée/);
const tirage = await request(app).post("/api/admin/draw").set("Authorization", "Bearer " + ctx.tAdmin);
log("tirage avant ouverture ->", tirage.status, tirage.body.message);
expect(tirage.status).toBe(409);
expect(tirage.body.message).toMatch(/pas encore commencé/);
});
test("C. les quatre phases dun jeu configure", async () => {
titre("C : parcours des phases");
await jourJ(1);
let w = (await request(app).get("/api/admin/game-settings").set("Authorization", "Bearer " + ctx.tAdmin)).body.gameWindow;
const ok = await request(app).post("/api/game/participate").set("Authorization", "Bearer " + ctx.tClient).send({ code: CODES[0] });
log("jour 1 | phase", w.phase, "| participate ->", ok.status);
expect(w.phase).toBe("OPEN");
expect(ok.status).toBe(201);
ctx.claimId = ok.body.claim.id;
await jourJ(35);
w = (await request(app).get("/api/admin/game-settings").set("Authorization", "Bearer " + ctx.tAdmin)).body.gameWindow;
const tard = await request(app).post("/api/game/participate").set("Authorization", "Bearer " + ctx.tClient).send({ code: CODES[1] });
log("jour 35 | phase", w.phase, "| participate ->", tard.status, JSON.stringify(tard.body.message));
expect(w.phase).toBe("CLAIM_ONLY");
expect(tard.body.message).toMatch(/participation est terminée/);
const tirage = await request(app).post("/api/admin/draw").set("Authorization", "Bearer " + ctx.tAdmin);
log("tirage apres cloture ->", tirage.status, tirage.body.message);
expect(tirage.status).toBe(201);
await jourJ(70);
w = (await request(app).get("/api/admin/game-settings").set("Authorization", "Bearer " + ctx.tAdmin)).body.gameWindow;
const fini = await request(app).post("/api/game/participate").set("Authorization", "Bearer " + ctx.tClient).send({ code: CODES[2] });
log("jour 70 | phase", w.phase, "| participate ->", fini.status, JSON.stringify(fini.body.message));
expect(w.phase).toBe("CLOSED");
expect(fini.body.message).toMatch(/jeu-concours est terminé/);
});
test("D. non-regression : le parcours nominal fonctionne toujours", async () => {
titre("D : verification du parcours de base");
await jourJ(2);
const g = await request(app).get("/api/game/my-gains").set("Authorization", "Bearer " + ctx.tClient).expect(200);
log("gains du client :", JSON.stringify(g.body.gains.map((x) => x.claimStatus)));
expect(g.body.gains[0].claimStatus).toBe("PENDING");
const stats = await request(app).get("/api/admin/stats").set("Authorization", "Bearer " + ctx.tAdmin).expect(200);
log("stats : phase =", stats.body.gameWindow.phase, "| participations =", stats.body.participations);
expect(stats.body.gameWindow.phase).toBe("OPEN");
});
});

View File

@@ -0,0 +1,66 @@
const request = require("supertest");
const app = require("../src/app");
const prisma = require("../src/config/prisma");
const mailer = require("../src/config/mailer");
jest.mock("../src/config/mailer", () => ({
sendMail: jest.fn(),
}));
// Comme le reste de la suite, ce fichier tourne sur la base de dev : il ne
// touche qu'au compte qu'il crée lui-même, identifié par cet email.
const testEmail = "rollback-mail-test@test.com";
const payload = {
firstname: "Camille",
lastname: "Martin",
email: testEmail,
password: "Password123",
birthdate: "1990-05-14",
gender: "Femme",
};
beforeEach(async () => {
jest.clearAllMocks();
await prisma.user.deleteMany({ where: { email: testEmail } });
});
afterAll(async () => {
await prisma.user.deleteMany({ where: { email: testEmail } });
await prisma.$disconnect();
});
describe("Inscription - echec d'envoi du code de validation", () => {
test("repond 502 et ne laisse aucun compte orphelin", async () => {
mailer.sendMail.mockRejectedValue(new Error("SMTP indisponible"));
const res = await request(app).post("/api/auth/register").send(payload);
expect(res.statusCode).toBe(502);
const user = await prisma.user.findUnique({ where: { email: testEmail } });
expect(user).toBeNull();
});
test("l'email reste reutilisable apres un echec d'envoi", async () => {
mailer.sendMail.mockRejectedValueOnce(new Error("SMTP indisponible"));
const echec = await request(app).post("/api/auth/register").send(payload);
expect(echec.statusCode).toBe(502);
mailer.sendMail.mockResolvedValue(true);
const succes = await request(app).post("/api/auth/register").send(payload);
expect(succes.statusCode).toBe(201);
});
test("enregistre la date de naissance et le genre", async () => {
mailer.sendMail.mockResolvedValue(true);
const res = await request(app).post("/api/auth/register").send(payload);
expect(res.statusCode).toBe(201);
const user = await prisma.user.findUnique({ where: { email: testEmail } });
expect(user.gender).toBe("Femme");
expect(user.birthdate.toISOString()).toBe("1990-05-14T00:00:00.000Z");
});
});