const request = require("supertest"); const bcrypt = require("bcrypt"); 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().mockResolvedValue(true) })); const EMAIL = "forgot-password-test@test.local"; const NON_VERIFIE = "forgot-password-unverified@test.local"; const INSCRIT = "forgot-password-register@test.local"; // Jeton extrait du lien reçu par email. function tokenFromMail() { const html = mailer.sendMail.mock.calls.at(-1)[0].html; return html.match(/\/definir-mot-de-passe\/([a-f0-9]+)"/)[1]; } async function cleanup() { await prisma.user.deleteMany({ where: { email: { in: [EMAIL, NON_VERIFIE, INSCRIT] } } }); } beforeAll(async () => { await cleanup(); const h = await bcrypt.hash("AncienMdp1!", 10); await prisma.user.create({ data: { firstname: "Léa", lastname: "Durand", email: EMAIL, password: h, isEmailVerified: true } }); await prisma.user.create({ data: { firstname: "Tom", lastname: "Petit", email: NON_VERIFIE, password: h, isEmailVerified: false } }); }); afterAll(async () => { await cleanup(); await prisma.$disconnect(); }); describe("Mot de passe oublie", () => { beforeEach(() => mailer.sendMail.mockClear()); test("un email inconnu recoit la meme reponse, sans envoi", async () => { const res = await request(app).post("/api/auth/forgot-password").send({ email: "inconnu@test.local" }); expect(res.status).toBe(200); expect(res.body.message).toMatch(/Si un compte existe/); expect(mailer.sendMail).not.toHaveBeenCalled(); }); test("un email connu recoit un lien vers la page du front", async () => { const res = await request(app).post("/api/auth/forgot-password").send({ email: EMAIL }); expect(res.status).toBe(200); expect(mailer.sendMail).toHaveBeenCalledTimes(1); const mail = mailer.sendMail.mock.calls[0][0]; expect(mail.to).toBe(EMAIL); const frontendUrl = (process.env.FRONTEND_URL || "http://localhost:5173").split(",")[0].trim(); expect(mail.html).toContain(`${frontendUrl}/definir-mot-de-passe/`); expect(tokenFromMail()).toHaveLength(64); }); test("une nouvelle demande invalide le lien precedent", async () => { await request(app).post("/api/auth/forgot-password").send({ email: EMAIL }); const ancien = tokenFromMail(); await request(app).post("/api/auth/forgot-password").send({ email: EMAIL }); const nouveau = tokenFromMail(); const res = await request(app) .post(`/api/auth/reset-password/${ancien}`) .send({ password: "NouveauMdp1!", confirmPassword: "NouveauMdp1!" }); expect(res.status).toBe(400); expect(nouveau).not.toBe(ancien); }); test("le lien permet de changer le mot de passe une seule fois", async () => { await request(app).post("/api/auth/forgot-password").send({ email: EMAIL }); const token = tokenFromMail(); const ok = await request(app) .post(`/api/auth/reset-password/${token}`) .send({ password: "NouveauMdp1!", confirmPassword: "NouveauMdp1!" }); expect(ok.status).toBe(200); expect((await request(app).post("/api/auth/login").send({ email: EMAIL, password: "AncienMdp1!" })).status).toBe(401); expect((await request(app).post("/api/auth/login").send({ email: EMAIL, password: "NouveauMdp1!" })).status).toBe(200); const reutilise = await request(app) .post(`/api/auth/reset-password/${token}`) .send({ password: "EncoreAutre1!", confirmPassword: "EncoreAutre1!" }); expect(reutilise.status).toBe(400); expect(reutilise.body.message).toMatch(/invalide ou expiré/); }); test("un lien expire est refuse", async () => { await request(app).post("/api/auth/forgot-password").send({ email: EMAIL }); const token = tokenFromMail(); await prisma.user.update({ where: { email: EMAIL }, data: { resetPasswordExpires: new Date(Date.now() - 1000) } }); const res = await request(app) .post(`/api/auth/reset-password/${token}`) .send({ password: "EncoreAutre1!", confirmPassword: "EncoreAutre1!" }); expect(res.status).toBe(400); expect(res.body.message).toMatch(/invalide ou expiré/); }); test("l email est reconnu quelle que soit la casse et malgre des espaces", async () => { const res = await request(app).post("/api/auth/forgot-password").send({ email: ` ${EMAIL.toUpperCase()} ` }); expect(res.status).toBe(200); expect(mailer.sendMail).toHaveBeenCalledTimes(1); expect(mailer.sendMail.mock.calls[0][0].to).toBe(EMAIL); }); test("la reinitialisation valide un compte jamais verifie, qui peut ensuite se connecter", async () => { await request(app).post("/api/auth/forgot-password").send({ email: NON_VERIFIE }); await request(app) .post(`/api/auth/reset-password/${tokenFromMail()}`) .send({ password: "NouveauMdp1!", confirmPassword: "NouveauMdp1!" }) .expect(200); const login = await request(app).post("/api/auth/login").send({ email: NON_VERIFIE, password: "NouveauMdp1!" }); expect(login.status).toBe(200); const enBase = await prisma.user.findUnique({ where: { email: NON_VERIFIE } }); expect(enBase.isEmailVerified).toBe(true); expect(enBase.emailVerifyToken).toBeNull(); }); test("le profil connecte recoit le lien sur l adresse du compte, sans captcha", async () => { await request(app).post("/api/auth/password-setup-link").expect(401); const login = await request(app).post("/api/auth/login").send({ email: EMAIL, password: "NouveauMdp1!" }).expect(200); const res = await request(app) .post("/api/auth/password-setup-link") .set("Authorization", `Bearer ${login.body.token}`); expect(res.status).toBe(200); expect(mailer.sendMail.mock.calls[0][0].to).toBe(EMAIL); expect(tokenFromMail()).toHaveLength(64); }); }); describe("Emails normalises a l inscription et a la connexion", () => { test("un email saisi avec majuscules est stocke en minuscules et reconnu a la connexion", async () => { const res = await request(app).post("/api/auth/register").send({ firstname: "Nora", lastname: "Blanc", email: ` ${INSCRIT.replace("forgot", "FORGOT")} `, password: "MotDePasse1!", confirmPassword: "MotDePasse1!", birthdate: "1990-05-14", }); expect(res.status).toBe(201); const enBase = await prisma.user.findUnique({ where: { email: INSCRIT } }); expect(enBase).not.toBeNull(); // Même adresse avec une autre casse : doublon refusé. const doublon = await request(app).post("/api/auth/register").send({ firstname: "Nora", lastname: "Blanc", email: INSCRIT.toUpperCase(), password: "MotDePasse1!", confirmPassword: "MotDePasse1!", }); expect(doublon.status).toBe(409); await prisma.user.update({ where: { email: INSCRIT }, data: { isEmailVerified: true } }); const login = await request(app).post("/api/auth/login").send({ email: INSCRIT.toUpperCase(), password: "MotDePasse1!" }); expect(login.status).toBe(200); }); });