import { expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { Window, type HTMLInputElement, type HTMLButtonElement, type HTMLElement } from "happy-dom"; test("MFA resend waits for cooldown, clears old code and replaces the active challenge", async () => { const window = new Window({ url: "https://sso.example.test/mfa" }); try { let now = Date.now(); window.Date.now = () => now; window.sessionStorage.setItem( "wrnexus.auth.mfa", JSON.stringify({ mfaToken: "synthetic-test-challenge", mfaExpiresAt: now + 300_000, requires: { mfa: ["email-otp"] }, }), ); window.document.body.innerHTML = `

Verification failed

`; const requests: any[] = []; window.fetch = (async (_: unknown, init: RequestInit) => { requests.push(JSON.parse(String(init.body))); return Response.json({ id: "challenge-" + requests.length, retryAfter: 60, expiresAt: now + 300_000, }); }) as any; window.eval(readFileSync(new URL("../assets/client/auth.js", import.meta.url), "utf8")); (window as any).WRNexusAuth.mount(window.document); await new Promise((resolve) => setTimeout(resolve, 0)); const button = window.document.querySelector("[data-auth-mfa-resend]")!; expect(button.disabled).toBe(true); expect(button.textContent).toContain("60s"); button.dispatchEvent(new window.Event("click")); expect(requests.length).toBe(1); const code = window.document.querySelector("[name=otpCode]")!; code.value = "123456"; now += 60_001; button.dispatchEvent(new window.Event("click")); button.dispatchEvent(new window.Event("click")); await new Promise((resolve) => setTimeout(resolve, 0)); expect(requests.length).toBe(2); expect(requests[1].resend).toBe(true); expect(window.document.querySelector("[name=challengeId]")!.value).toBe( "challenge-2", ); expect(code.value).toBe(""); expect(window.document.querySelector("[data-error=_form]")!.textContent).toBe(""); expect(button.disabled).toBe(true); } finally { await window.happyDOM.close(); } }); test("expired and legacy MFA requests are rejected on mount before entering a code", async () => { for (const deadline of [undefined, Date.now() - 1]) { const window = new Window({ url: "https://sso.example.test/mfa" }); try { window.sessionStorage.setItem( "wrnexus.auth.mfa", JSON.stringify({ mfaToken: "old-challenge", mfaExpiresAt: deadline }), ); window.document.body.innerHTML = `
`; window.eval(readFileSync(new URL("../assets/client/auth.js", import.meta.url), "utf8")); (window as any).WRNexusAuth.mount(window.document); expect(window.document.querySelector("button")!.disabled).toBe(true); expect(window.document.querySelector("[data-auth-mfa-restart]")!.hidden).toBe( false, ); expect(window.document.querySelector("[data-auth-mfa-expired]")!.hidden).toBe( false, ); expect(window.document.querySelector("form")!.hidden).toBe(true); expect(window.document.querySelector("[data-auth-mfa-status]")!.textContent).toBe(""); expect(window.sessionStorage.getItem("wrnexus.auth.mfa")).toBeNull(); } finally { await window.happyDOM.close(); } } }); test("selecting delivery radios sends once, ignores stale responses, and never sends for local methods", async () => { const window = new Window({ url: "https://sso.example.test/mfa" }); try { window.sessionStorage.setItem( "wrnexus.auth.mfa", JSON.stringify({ mfaToken: "synthetic-test-challenge", mfaExpiresAt: Date.now() + 300_000 }), ); window.document.body.innerHTML = `
${["totp", "recovery-code", "email-otp", "sms-otp"].map((value) => ``).join("")}

`; const requests: Array<{ method: string; resolve: (response: Response) => void }> = []; window.fetch = ((_: unknown, init: RequestInit) => new Promise((resolve) => { requests.push({ method: JSON.parse(String(init.body)).method, resolve }); })) as any; window.eval(readFileSync(new URL("../assets/client/auth.js", import.meta.url), "utf8")); (window as any).WRNexusAuth.mount(window.document); const form = window.document.querySelector("form")!; const field = (name: string) => form.querySelector(`[name=${name}]`)!; const choose = (value: string) => { const radio = form.querySelector(`[name=method][value="${value}"]`)!; radio.checked = true; radio.dispatchEvent(new window.Event("change", { bubbles: true })); }; choose("recovery-code"); expect(requests).toHaveLength(0); field("recoveryCode").value = "abc123-def456"; form.dispatchEvent(new window.Event("submit", { bubbles: true, cancelable: true })); expect(field("code").value).toBe("abc123-def456"); choose("email-otp"); choose("email-otp"); expect(requests.map((x) => x.method)).toEqual(["email-otp"]); choose("sms-otp"); expect(requests.map((x) => x.method)).toEqual(["email-otp", "sms-otp"]); requests[0]!.resolve(Response.json({ id: "email-challenge" })); await new Promise((resolve) => setTimeout(resolve, 0)); expect(field("challengeId").value).toBe(""); requests[1]!.resolve(Response.json({ id: "sms-challenge" })); await new Promise((resolve) => setTimeout(resolve, 0)); expect(field("challengeId").value).toBe("sms-challenge"); choose("totp"); expect(field("challengeId").value).toBe(""); expect(requests).toHaveLength(2); field("otpCode").value = "123456"; form.dispatchEvent(new window.Event("submit", { bubbles: true, cancelable: true })); expect(field("code").value).toBe("123456"); } finally { await window.happyDOM.close(); } }); test("MFA radios preserve allowed methods and restore continuation without reviving expired challenges", async () => { const window = new Window({ url: "https://sso.example.test/mfa" }); try { window.sessionStorage.setItem( "wrnexus.auth.mfa", JSON.stringify({ mfaToken: "synthetic-test-challenge", mfaExpiresAt: Date.now() + 300_000, requires: { mfa: ["totp"] }, }), ); window.document.body.innerHTML = `

`; window.eval(readFileSync(new URL("../assets/client/auth.js", import.meta.url), "utf8")); (window as any).WRNexusAuth.mount(window.document); const form = window.document.querySelector("form")!; const token = form.querySelector('[name="mfaToken"]')!; expect(token.value).toBe("synthetic-test-challenge"); expect(form.querySelector('input[value="email-otp"]')!.disabled).toBe(true); token.value = ""; // Hydration reapplies the component's initial empty prop. form.dispatchEvent(new window.Event("submit", { bubbles: true, cancelable: true })); expect(token.value).toBe("synthetic-test-challenge"); form.dispatchEvent( new window.CustomEvent("wrn:error", { bubbles: true, detail: { code: "mfa-invalid" } }), ); expect(token.value).toBe("synthetic-test-challenge"); form.dispatchEvent( new window.CustomEvent("wrn:error", { bubbles: true, detail: { code: "mfa-transaction-expired" }, }), ); // Validation finalizers may restore button.disabled after dispatching error. form.querySelector("button")!.disabled = false; expect(form.hidden).toBe(true); expect( window.document.querySelector("[data-auth-mfa-challenge]")!.dataset.mfaExpired, ).toBe("true"); expect(window.sessionStorage.getItem("wrnexus.auth.mfa")).toBeNull(); expect(window.document.querySelector("[data-auth-mfa-restart]")!.hidden).toBe( false, ); const retry = new window.Event("submit", { bubbles: true, cancelable: true }); form.dispatchEvent(retry); expect(retry.defaultPrevented).toBe(true); expect(token.value).toBe(""); } finally { await window.happyDOM.close(); } });