// @vitest-environment node import { beforeEach, describe, expect, test, vi } from "vitest"; vi.mock("~/utils/session.server", async () => { const { createCookieSessionStorage } = await import("react-router"); return { storage: createCookieSessionStorage({ cookie: { name: "test_session", secrets: ["test-session-secret"], path: "/", httpOnly: true, sameSite: "lax" }, }), }; }); import { storage } from "~/utils/session.server"; import { checkMfaStepUpState, clearMfaStepUpState, getMfaStepUpState, MFA_STEP_UP_MAX_AGE_MS, MFA_STEP_UP_SESSION_KEY, setMfaStepUpState, } from "./step-up.server"; async function createSession() { return storage.getSession(); } describe("step-up.server", () => { beforeEach(() => { vi.clearAllMocks(); }); test("records and verifies fresh MFA step-up state for the same user", async () => { const session = await createSession(); const verifiedAt = new Date("2026-05-08T14:00:00.000Z"); const state = setMfaStepUpState(session, { userId: "admin-1", method: "totp", redirectTo: "/admin?tab=security", now: verifiedAt, }); const result = checkMfaStepUpState(session, "admin-1", { now: new Date(verifiedAt.getTime() + 30_000) }); expect(state).toEqual({ userId: "admin-1", method: "totp", verifiedAt: verifiedAt.toISOString(), redirectTo: "/admin?tab=security", }); expect(getMfaStepUpState(session)).toEqual(state); expect(result).toEqual({ verified: true, state, ageMs: 30_000 }); }); test("records step-up state without optional redirect metadata", async () => { const session = await createSession(); const verifiedAt = new Date("2026-05-08T14:00:00.000Z"); const state = setMfaStepUpState(session, { userId: "admin-1", method: "recovery-code", now: verifiedAt, }); expect(state).toEqual({ userId: "admin-1", method: "recovery-code", verifiedAt: verifiedAt.toISOString(), }); expect(getMfaStepUpState(session)).toEqual(state); }); test("fails closed for stale and future timestamps", async () => { const staleSession = await createSession(); const futureSession = await createSession(); const now = new Date("2026-05-08T14:10:00.000Z"); setMfaStepUpState(staleSession, { userId: "admin-1", method: "recovery-code", now: new Date(now.getTime() - MFA_STEP_UP_MAX_AGE_MS - 1), }); setMfaStepUpState(futureSession, { userId: "admin-1", method: "totp", now: new Date(now.getTime() + 1), }); expect(checkMfaStepUpState(staleSession, "admin-1", { now })).toEqual({ verified: false, reason: "stale" }); expect(checkMfaStepUpState(futureSession, "admin-1", { now })).toEqual({ verified: false, reason: "stale" }); }); test("does not authorize a different user", async () => { const session = await createSession(); setMfaStepUpState(session, { userId: "admin-1", method: "totp", now: new Date("2026-05-08T14:00:00.000Z") }); expect(checkMfaStepUpState(session, "admin-2", { now: new Date("2026-05-08T14:01:00.000Z") })).toEqual({ verified: false, reason: "wrong-user", }); }); test("treats missing and malformed state as unverified without throwing", async () => { const missingSession = await createSession(); const malformedPayloads = [ { method: "totp", verifiedAt: "2026-05-08T14:00:00.000Z" }, { userId: "", method: "totp", verifiedAt: "2026-05-08T14:00:00.000Z" }, "not-json-object", { userId: "admin-1", method: "sms", verifiedAt: "2026-05-08T14:00:00.000Z" }, { userId: "admin-1", verifiedAt: "2026-05-08T14:00:00.000Z" }, { userId: "admin-1", method: "totp", verifiedAt: "not-a-date" }, { userId: "admin-1", method: "totp", verifiedAt: "2026-05-08T14:00:00.000Z", redirectTo: 123 }, ]; expect(checkMfaStepUpState(missingSession, "admin-1")).toEqual({ verified: false, reason: "missing" }); for (const payload of malformedPayloads) { const session = await createSession(); session.set(MFA_STEP_UP_SESSION_KEY, payload); expect(() => getMfaStepUpState(session)).not.toThrow(); expect(getMfaStepUpState(session)).toBeNull(); expect(checkMfaStepUpState(session, "admin-1")).toEqual({ verified: false, reason: "malformed" }); } }); test("clears existing MFA step-up state", async () => { const session = await createSession(); setMfaStepUpState(session, { userId: "admin-1", method: "totp", now: new Date("2026-05-08T14:00:00.000Z") }); clearMfaStepUpState(session); expect(getMfaStepUpState(session)).toBeNull(); expect(checkMfaStepUpState(session, "admin-1")).toEqual({ verified: false, reason: "missing" }); }); test("serialized step-up state contains no cookies, tokens, hashes, passwords, or factor secrets", async () => { const session = await createSession(); const state = setMfaStepUpState(session, { userId: "admin-1", method: "recovery-code", redirectTo: "/admin", now: new Date("2026-05-08T14:00:00.000Z"), }); const serialized = JSON.stringify(state); expect(serialized).toContain("admin-1"); expect(serialized).toContain("recovery-code"); expect(serialized).not.toMatch(/cookie|csrf|token|secret|hash|password|encrypted|totpSecret|recoveryCodes/i); }); });