const request = require("supertest"); const app = require("../src/app"); const prisma = require("../src/config/prisma"); const mailer = require("../src/config/mailer"); const { passport, googleEnabled } = require("../src/config/passport"); jest.mock("../src/config/mailer", () => ({ sendMail: jest.fn() })); const EMAIL = "google-pwd-test@test.local"; const PROFILE = { id: "sim-google-id-123456", displayName: "Chloe Martin", name: { givenName: "Chloe", familyName: "Martin" }, emails: [{ value: EMAIL }], photos: [{ value: "https://example.test/avatar.png" }], }; async function cleanup() { await prisma.user.deleteMany({ where: { email: EMAIL } }); } // Rejoue le callback de la strategie Google sans sortir sur le reseau. function verifierProfilGoogle() { const strategy = passport._strategy("google"); return new Promise((resolve, reject) => { strategy._verify("access-token", "refresh-token", PROFILE, (err, user) => { if (err) reject(err); else resolve(user); }); }); } beforeAll(cleanup); afterAll(async () => { await cleanup(); await prisma.$disconnect(); }); const decrire = googleEnabled ? describe : describe.skip; decrire("Inscription Google et definition du mot de passe", () => { beforeEach(() => jest.clearAllMocks()); test("un compte Google est cree sans mot de passe et recoit un lien", async () => { mailer.sendMail.mockResolvedValue(true); const user = await verifierProfilGoogle(); expect(user.provider).toBe("GOOGLE"); expect(user.password).toBeNull(); expect(user.isEmailVerified).toBe(true); expect(mailer.sendMail).toHaveBeenCalledTimes(1); const envoi = mailer.sendMail.mock.calls[0][0]; expect(envoi.to).toBe(EMAIL); expect(envoi.subject).toMatch(/mot de passe/i); // Le lien doit viser le formulaire du front, pas la route POST de l API. const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173"; expect(envoi.html).toContain(`${frontendUrl}/definir-mot-de-passe/`); const enBase = await prisma.user.findUnique({ where: { email: EMAIL } }); expect(enBase.resetPasswordToken).toHaveLength(64); expect(enBase.resetPasswordExpires.getTime()).toBeGreaterThan(Date.now()); ctxToken = enBase.resetPasswordToken; }); test("le profil signale que le compte n a pas de mot de passe", async () => { const jwt = require("jsonwebtoken"); const u = await prisma.user.findUnique({ where: { email: EMAIL } }); const token = jwt.sign({ id: u.id, role: u.role }, process.env.JWT_SECRET, { expiresIn: "1h" }); const res = await request(app).get("/api/auth/me").set("Authorization", "Bearer " + token).expect(200); expect(res.body.user.hasPassword).toBe(false); expect(res.body.user.provider).toBe("GOOGLE"); // Le hash ne doit jamais sortir de l API. expect(res.body.user.password).toBeUndefined(); }); test("la connexion par mot de passe est refusee tant qu il n en existe pas", async () => { const res = await request(app).post("/api/auth/login").send({ email: EMAIL, password: "PeuImporte123" }); expect(res.status).toBe(401); expect(res.body.message).toMatch(/connexion Google/); }); test("une confirmation qui ne correspond pas est refusee", async () => { const res = await request(app) .post("/api/auth/reset-password/" + ctxToken) .send({ password: "MonMotDePasse1!", confirmPassword: "AutreMotDePasse2!" }); expect(res.status).toBe(400); expect(res.body.message).toMatch(/ne correspondent pas/); const u = await prisma.user.findUnique({ where: { email: EMAIL } }); expect(u.password).toBeNull(); }); test("un mot de passe trop court est refuse", async () => { const res = await request(app) .post("/api/auth/reset-password/" + ctxToken) .send({ password: "court", confirmPassword: "court" }); expect(res.status).toBe(400); expect(res.body.message).toMatch(/8 caractères/); }); test("un mot de passe sans caractere special est refuse", async () => { const res = await request(app) .post("/api/auth/reset-password/" + ctxToken) .send({ password: "MonMotDePasse1", confirmPassword: "MonMotDePasse1" }); expect(res.status).toBe(400); expect(res.body.message).toMatch(/caractère spécial/); }); test("le mot de passe defini permet ensuite de se connecter et de le changer", async () => { const ok = await request(app) .post("/api/auth/reset-password/" + ctxToken) .send({ password: "MonMotDePasse1!", confirmPassword: "MonMotDePasse1!" }) .expect(200); expect(ok.body.message).toMatch(/réinitialisé/); const login = await request(app).post("/api/auth/login").send({ email: EMAIL, password: "MonMotDePasse1!" }).expect(200); expect(login.body.token).toBeDefined(); // Le jeton a bien ete consomme. const u = await prisma.user.findUnique({ where: { email: EMAIL } }); expect(u.resetPasswordToken).toBeNull(); const me = await request(app).get("/api/auth/me").set("Authorization", "Bearer " + login.body.token).expect(200); expect(me.body.user.hasPassword).toBe(true); const change = await request(app) .patch("/api/auth/change-password") .set("Authorization", "Bearer " + login.body.token) .send({ currentPassword: "MonMotDePasse1!", newPassword: "EncoreUnAutre2!" }); expect(change.status).toBe(200); // Même règle qu'à l'inscription pour le changement depuis le profil. const sansSpecial = await request(app) .patch("/api/auth/change-password") .set("Authorization", "Bearer " + login.body.token) .send({ currentPassword: "EncoreUnAutre2!", newPassword: "SansCaractere3" }); expect(sansSpecial.status).toBe(400); expect(sansSpecial.body.message).toMatch(/caractère spécial/); }); test("une seconde connexion Google ne renvoie pas de nouveau lien", async () => { mailer.sendMail.mockResolvedValue(true); const user = await verifierProfilGoogle(); expect(user.email).toBe(EMAIL); expect(mailer.sendMail).not.toHaveBeenCalled(); }); }); let ctxToken;