import { expect, test } from "bun:test"; import { createContext } from "@wrnexus/core"; import { createAuthEngine } from "../src/engine.ts"; import { createAuthHttpHandlers } from "../src/http/index.ts"; import { MemoryAuthStore } from "../src/stores/memory.ts"; import type { AuthDeliveryMessage } from "../src/types.ts"; test("MFA resend creates a fresh code after cooldown and invalidates the previous code", async () => { let now = 1_790_000_000_000; const messages: AuthDeliveryMessage[] = []; const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "mfa-resend-test-secret-longer-than-thirty-two-characters", clock: { now: () => now }, requireTrustedDeviceVerification: true, delivery: { async send(message) { messages.push(message); }, }, }); await engine.register({ email: "resend@example.test", password: "StrongPassword123!" }); await engine.verifyEmail(messages.find((m) => m.template === "verify-email")!.token!); const login = await engine.login({ identifier: "resend@example.test", password: "StrongPassword123!", fingerprint: "browser", }); const handlers = createAuthHttpHandlers({ engine }); const request = (resend: boolean) => { const req = new Request("https://sso.example.test/api/auth/mfa/otp", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ mfaToken: login.mfaToken, method: "email-otp", resend }), }); return handlers.beginMfaOtp(createContext(req, new URL(req.url))); }; const first = await (await request(false)).json(); const firstCode = messages.at(-1)!.code!; const count = messages.length; expect((await request(true)).status).toBe(400); expect(messages.length).toBe(count); expect((await (await request(false)).json()).id).toBe(first.id); now += 60_000; const next = await (await request(true)).json(); expect(next.ok).toBe(true); expect(next.id).not.toBe(first.id); expect(next.retryAfter).toBe(60); expect(messages.length).toBe(count + 1); expect((await engine.verifyOtp(first.id, firstCode, "mfa")).ok).toBe(false); expect( ( await engine.completeMfa({ mfaToken: login.mfaToken!, method: "email-otp", challengeId: next.id, code: messages.at(-1)!.code!, fingerprint: "browser", }) ).ok, ).toBe(true); now += 60_000; expect((await request(true)).status).toBe(400); });