import { beforeEach, describe, expect, it, vi } from "vitest"; import { consumeMagicLinkToken, createSignedMagicLinkToken, issueMagicLink, verifySignedMagicLinkToken } from "./magic-link.server"; import { getAppConfiguration } from "~/utils/db/appConfiguration.db.server"; import { getMailTranslator, getMailTranslatorFromRequest } from "~/locale/mail-translator.server"; import { queueTransactionalEmail } from "~/modules/emails/services/TransactionalEmailQueue.server"; import { db } from "~/utils/db.server"; vi.mock("@toolctrl/config", () => ({ loadRequiredWebConfig: vi.fn(() => ({ cryptoSecret: "test-secret", sessionSecret: "session-secret" })), })); vi.mock("~/utils/db.server", () => ({ db: { magicLinkToken: { create: vi.fn(), findUnique: vi.fn(), update: vi.fn(), }, user: { findUnique: vi.fn(), }, }, })); vi.mock("~/utils/url.server", () => ({ getAbsoluteUrl: vi.fn((_request: Request, path: string) => `https://example.com${path}`), })); vi.mock("~/modules/emails/services/TransactionalEmailQueue.server", () => ({ queueTransactionalEmail: vi.fn(), })); vi.mock("~/utils/db/appConfiguration.db.server", () => ({ getAppConfiguration: vi.fn(), })); vi.mock("~/locale/mail-translator.server", () => ({ getMailTranslator: vi.fn(), getMailTranslatorFromRequest: vi.fn(), })); const getAppConfigurationMock = vi.mocked(getAppConfiguration); const getMailTranslatorMock = vi.mocked(getMailTranslator); const getMailTranslatorFromRequestMock = vi.mocked(getMailTranslatorFromRequest); const queueTransactionalEmailMock = vi.mocked(queueTransactionalEmail); const magicLinkTokenCreateMock = vi.mocked(db.magicLinkToken.create); const magicLinkTokenFindUniqueMock = vi.mocked(db.magicLinkToken.findUnique); const magicLinkTokenUpdateMock = vi.mocked(db.magicLinkToken.update); const userFindUniqueMock = vi.mocked(db.user.findUnique); beforeEach(() => { vi.clearAllMocks(); getAppConfigurationMock.mockResolvedValue({ app: { name: "Acme Portal" }, email: { fromName: "Acme Support", supportEmail: "support@acme.test" }, branding: { logo: "https://assets.example.com/logo.png", favicon: null }, } as Awaited>); getMailTranslatorMock.mockResolvedValue({ locale: "de", t: (key, vars) => (key === "magicLink.subject" ? `Dein Anmeldelink für ${vars?.productName}` : key), }); getMailTranslatorFromRequestMock.mockResolvedValue({ locale: "en", t: (key) => key, }); queueTransactionalEmailMock.mockResolvedValue({ enqueued: true, jobId: "job_1" }); magicLinkTokenCreateMock.mockResolvedValue({ id: "token_1" } as Awaited>); magicLinkTokenFindUniqueMock.mockResolvedValue(null); magicLinkTokenUpdateMock.mockResolvedValue({ id: "token_1" } as Awaited>); userFindUniqueMock.mockResolvedValue({ locale: "de" } as Awaited>); }); describe("issueMagicLink", () => { it("queues a magic-link mail rendered through the React-Email pipeline", async () => { const result = await issueMagicLink({ request: new Request("https://example.com/forgot-password"), userId: "user_1", email: "ada@example.com", name: "Ada", now: new Date("2026-05-19T12:00:00.000Z"), }); expect(result.token).toMatch(/\./); // Translator is resolved against the user's locale for the subject. expect(getMailTranslatorMock).toHaveBeenCalledWith("de"); expect(getMailTranslatorFromRequestMock).not.toHaveBeenCalled(); expect(queueTransactionalEmailMock).toHaveBeenCalledOnce(); expect(queueTransactionalEmailMock).toHaveBeenCalledWith( expect.objectContaining({ to: "ada@example.com", alias: "magic-link", subject: "Dein Anmeldelink für Acme Portal", userId: "user_1", data: expect.objectContaining({ action_url: expect.stringContaining("https://example.com/auth/magic-link?token="), name: "Ada", expires_in_minutes: "15", }), priorityOverride: "critical", }) ); const queued = queueTransactionalEmailMock.mock.calls[0]?.[0]; expect(queued?.body).toContain("Click the button below to sign in to your Acme Portal account. No password required."); expect(queued?.body).toContain("Sign in"); expect(queued?.body).toContain("This link expires in 15 minutes"); expect(queued?.body).not.toContain("{i18nT("); }); }); describe("signed magic-link tokens", () => { it("verifies a signed token payload and rejects malformed or tampered tokens", () => { const issued = createSignedMagicLinkToken({ userId: "user_1", tokenId: "token_1", now: new Date("2026-05-19T12:00:00.000Z"), purpose: "reset", }); expect(verifySignedMagicLinkToken(issued.token)).toMatchObject({ sub: "user_1", jti: "token_1", p: "reset", v: "v1", }); expect(verifySignedMagicLinkToken("not-a-token")).toBeNull(); expect(verifySignedMagicLinkToken(`${issued.token}.extra`)).toBeNull(); expect(verifySignedMagicLinkToken(`${issued.token.slice(0, -1)}x`)).toBeNull(); }); }); describe("consumeMagicLinkToken", () => { it("consumes a valid matching token exactly once", async () => { const now = new Date("2026-05-19T12:00:00.000Z"); const issued = createSignedMagicLinkToken({ userId: "user_1", tokenId: "token_1", now, purpose: "login" }); magicLinkTokenFindUniqueMock.mockResolvedValue({ id: "token_1", userId: "user_1", tokenHash: issued.tokenHash, expiresAt: issued.expiresAt, consumedAt: null, } as Awaited>); await expect(consumeMagicLinkToken(issued.token, { now, expectedPurpose: "login" })).resolves.toEqual({ ok: true, userId: "user_1", tokenId: "token_1", purpose: "login", }); expect(magicLinkTokenUpdateMock).toHaveBeenCalledWith({ where: { id: "token_1" }, data: { consumedAt: now }, }); }); it("rejects unknown or mismatched stored tokens without consuming them", async () => { const now = new Date("2026-05-19T12:00:00.000Z"); const issued = createSignedMagicLinkToken({ userId: "user_1", tokenId: "token_1", now }); magicLinkTokenFindUniqueMock.mockResolvedValue(null); await expect(consumeMagicLinkToken(issued.token, { now })).resolves.toEqual({ ok: false, reason: "not-found" }); expect(magicLinkTokenUpdateMock).not.toHaveBeenCalled(); }); it("rejects expired signed payloads before reading the database", async () => { const issuedAt = new Date("2026-05-19T12:00:00.000Z"); const now = new Date("2026-05-19T12:16:00.000Z"); const issued = createSignedMagicLinkToken({ userId: "user_1", tokenId: "token_1", now: issuedAt }); await expect(consumeMagicLinkToken(issued.token, { now })).resolves.toEqual({ ok: false, reason: "expired" }); expect(magicLinkTokenFindUniqueMock).not.toHaveBeenCalled(); expect(magicLinkTokenUpdateMock).not.toHaveBeenCalled(); }); it("rejects stored tokens that expired after a valid signed payload was issued", async () => { const now = new Date("2026-05-19T12:00:00.000Z"); const issued = createSignedMagicLinkToken({ userId: "user_1", tokenId: "token_1", now, ttlMinutes: 60, }); magicLinkTokenFindUniqueMock.mockResolvedValue({ id: "token_1", userId: "user_1", tokenHash: issued.tokenHash, expiresAt: new Date("2026-05-19T11:59:59.999Z"), consumedAt: null, } as Awaited>); await expect(consumeMagicLinkToken(issued.token, { now })).resolves.toEqual({ ok: false, reason: "expired" }); expect(magicLinkTokenUpdateMock).not.toHaveBeenCalled(); }); it("rejects replayed tokens before writing a new consumedAt", async () => { const now = new Date("2026-05-19T12:00:00.000Z"); const issued = createSignedMagicLinkToken({ userId: "user_1", tokenId: "token_1", now }); magicLinkTokenFindUniqueMock.mockResolvedValue({ id: "token_1", userId: "user_1", tokenHash: issued.tokenHash, expiresAt: issued.expiresAt, consumedAt: new Date("2026-05-19T12:01:00.000Z"), } as Awaited>); await expect(consumeMagicLinkToken(issued.token, { now })).resolves.toEqual({ ok: false, reason: "replayed" }); expect(magicLinkTokenUpdateMock).not.toHaveBeenCalled(); }); it("rejects purpose mismatches without consuming the token", async () => { const now = new Date("2026-05-19T12:00:00.000Z"); const issued = createSignedMagicLinkToken({ userId: "user_1", tokenId: "token_1", now, purpose: "reset" }); magicLinkTokenFindUniqueMock.mockResolvedValue({ id: "token_1", userId: "user_1", tokenHash: issued.tokenHash, expiresAt: issued.expiresAt, consumedAt: null, } as Awaited>); await expect(consumeMagicLinkToken(issued.token, { now, expectedPurpose: "login" })).resolves.toEqual({ ok: false, reason: "purpose-mismatch" }); expect(magicLinkTokenUpdateMock).not.toHaveBeenCalled(); }); });