import { INestApplication } from "@nestjs/common"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { agent, bearer, closeTestApp, createTestApp, loginSuperAdmin, qaEmail, type AuthedTokens, } from "./setup.js"; /** * `sendMail` is mocked (not stubbed-out — the real call path through * auth.service.ts still runs) so this test can recover the plaintext * reset token, which by design is NEVER returned in any API response — * only emailed. This mirrors how a real user would get it: from the * email body. `packages/integrations`' own mailer is fail-soft when SMTP * isn't configured (confirmed by the other specs' harmless console * logs), but that's irrelevant here since we intercept before it runs. */ const sendMailMock = vi.fn().mockResolvedValue(undefined); vi.mock("@eden/integrations", () => ({ sendMail: (...args: unknown[]) => sendMailMock(...args), })); function extractTokenFromLastEmail(): string { const lastCall = sendMailMock.mock.calls.at(-1)?.[0] as { html: string } | undefined; const match = lastCall?.html.match(/token=([a-f0-9]{64})/); if (!match?.[1]) throw new Error("Reset token not found in the last mocked email"); return match[1]; } /** * Protects the password-reset invariants added in auth.service.ts * (forgotPassword/resetPassword) — anti-enumeration on request, token * single-use, expiry, and session revocation on completion. */ describe("auth — password reset", () => { let app: INestApplication; let admin: AuthedTokens; let fixtureEmail: string; let fixtureUserId: string; let fixturePassword: string; beforeAll(async () => { app = await createTestApp(); admin = await loginSuperAdmin(app); fixtureEmail = qaEmail("pwreset"); const invite = await agent(app) .post("/api/v1/users/invite") .set(...bearer(admin.accessToken)) .send({ email: fixtureEmail, roleKey: "user", applications: ["core"] }) .expect(201); fixtureUserId = invite.body.user.id; fixturePassword = invite.body.tempPassword; sendMailMock.mockClear(); // discard the invite email capture, not relevant here }); afterAll(async () => { await agent(app) .patch(`/api/v1/users/${fixtureUserId}`) .set(...bearer(admin.accessToken)) .send({ isActive: false }) .expect(200); await closeTestApp(app); }); it("forgot-password returns the same generic response for a registered and an unregistered email", async () => { const known = await agent(app).post("/api/v1/auth/forgot-password").send({ email: fixtureEmail }).expect(200); const unknown = await agent(app) .post("/api/v1/auth/forgot-password") .send({ email: qaEmail("nobody") }) .expect(200); expect(known.body.message).toBe(unknown.body.message); // Only the registered email actually triggers a send. expect(sendMailMock).toHaveBeenCalledTimes(1); expect(sendMailMock.mock.calls[0]?.[0].to).toBe(fixtureEmail); }); it("a bogus token is rejected", async () => { await agent(app) .post("/api/v1/auth/reset-password") .send({ token: "0".repeat(64), newPassword: "brandNewPassword123" }) .expect(400); }); it("completing the reset changes the password, revokes existing sessions, and the token is single-use", async () => { // A real active session, established BEFORE the reset — must not survive it. const sessionBeforeReset = await agent(app) .post("/api/v1/auth/login") .send({ email: fixtureEmail, password: fixturePassword, applicationKey: "core" }) .expect(200); const oldRefreshToken = sessionBeforeReset.body.refreshToken as string; sendMailMock.mockClear(); await agent(app).post("/api/v1/auth/forgot-password").send({ email: fixtureEmail }).expect(200); const token = extractTokenFromLastEmail(); const newPassword = "brandNewPassword123"; await agent(app).post("/api/v1/auth/reset-password").send({ token, newPassword }).expect(204); // Old session is dead — reset revokes every existing session. await agent(app).post("/api/v1/auth/refresh").send({ refreshToken: oldRefreshToken }).expect(401); // Old password no longer works; the new one does. await agent(app) .post("/api/v1/auth/login") .send({ email: fixtureEmail, password: fixturePassword, applicationKey: "core" }) .expect(401); await agent(app) .post("/api/v1/auth/login") .send({ email: fixtureEmail, password: newPassword, applicationKey: "core" }) .expect(200); // The same token cannot be replayed. await agent(app).post("/api/v1/auth/reset-password").send({ token, newPassword: "yetAnotherPassword456" }).expect(400); }); });