import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { initPasswordValidation, loadPasswordRules } from "./password_validation"; // The checklist is display-only (the server enforces), but it gates the // submit button and mirrors the ACTIVE server rules — these tests pin the // rules matrix, the fetch fallback behavior, and the submit gating. function buildDom(): void { document.body.innerHTML = `
`; } function setPassword(value: string): void { (document.getElementById("password") as HTMLInputElement).value = value; (document.getElementById("password") as HTMLInputElement).dispatchEvent(new Event("input")); } const submitButton = (): HTMLButtonElement => document.getElementById("register-btn") as HTMLButtonElement; describe("loadPasswordRules", () => { afterEach(() => { vi.unstubAllGlobals(); }); it("adopts fetched rules", async () => { vi.stubGlobal( "fetch", vi.fn( async () => new Response(JSON.stringify({ min_length: 12, require_upper: false }), { status: 200 }), ), ); const rules = await loadPasswordRules(); expect(rules).toEqual({ min_length: 12, require_upper: false, require_lower: true, require_number: true, require_special: true, }); }); it("falls back to defaults when the endpoint fails", async () => { vi.stubGlobal( "fetch", vi.fn(async () => new Response("nope", { status: 500 })), ); const rules = await loadPasswordRules(); expect(rules).toEqual({ min_length: 8, require_upper: true, require_lower: true, require_number: true, require_special: true, }); }); it("falls back to defaults on invalid payloads", async () => { vi.stubGlobal( "fetch", vi.fn(async () => new Response(JSON.stringify({ min_length: -5 }), { status: 200 })), ); const rules = await loadPasswordRules(); expect(rules.min_length).toBe(8); }); }); describe("initPasswordValidation", () => { beforeEach(() => { // Deterministic rules: fetch failure lands on the historical defaults. vi.stubGlobal( "fetch", vi.fn(async () => new Response("nope", { status: 500 })), ); }); afterEach(() => { vi.unstubAllGlobals(); }); // initPasswordValidation debounces through window.setTimeout, which under // jsdom is a distinct timer from the faked global — so these tests wait // out the real 100ms debounce instead of advancing fake clocks. const settleDebounce = (): Promise => new Promise((resolve) => setTimeout(resolve, 150)); it("generates one checklist row per active rule plus the match row", async () => { buildDom(); await loadPasswordRules(); initPasswordValidation({}); const rows = document.querySelectorAll("#password-requirements li"); expect(rows.length).toBe(6); // length, upper, lower, number, special, match }); it("starts with the submit button disabled for an empty password", async () => { buildDom(); await loadPasswordRules(); initPasswordValidation({}); expect(submitButton().disabled).toBe(true); }); it("enables submit when all requirements pass", async () => { buildDom(); await loadPasswordRules(); initPasswordValidation({}); setPassword("Abcdef1!"); (document.getElementById("confirm-password") as HTMLInputElement).value = "Abcdef1!"; (document.getElementById("confirm-password") as HTMLInputElement).dispatchEvent( new Event("input"), ); (document.getElementById("username") as HTMLInputElement).value = "user"; (document.getElementById("username") as HTMLInputElement).dispatchEvent(new Event("input")); (document.getElementById("email") as HTMLInputElement).value = "user@example.com"; (document.getElementById("email") as HTMLInputElement).dispatchEvent(new Event("input")); await settleDebounce(); expect(submitButton().disabled).toBe(false); }); it("keeps submit disabled on a confirmation mismatch", async () => { buildDom(); await loadPasswordRules(); initPasswordValidation({}); setPassword("Abcdef1!"); (document.getElementById("confirm-password") as HTMLInputElement).value = "Different1!"; (document.getElementById("confirm-password") as HTMLInputElement).dispatchEvent( new Event("input"), ); await settleDebounce(); expect(submitButton().disabled).toBe(true); }); it("does not require username/email when requireUsernameEmail is false", async () => { buildDom(); await loadPasswordRules(); initPasswordValidation({ requireUsernameEmail: false }); setPassword("Abcdef1!"); (document.getElementById("confirm-password") as HTMLInputElement).value = "Abcdef1!"; (document.getElementById("confirm-password") as HTMLInputElement).dispatchEvent( new Event("input"), ); await settleDebounce(); expect(submitButton().disabled).toBe(false); }); });