// @vitest-environment jsdom import { beforeEach, describe, expect, it } from "vitest"; import { detectPasswordPolicy, extractNearbyText } from "./policy"; describe("detectPasswordPolicy", () => { beforeEach(() => { document.body.innerHTML = ""; }); function setupPasswordField(attrs: Record = {}, nearby = ""): HTMLInputElement { const wrap = document.createElement("div"); const input = document.createElement("input"); input.type = "password"; Object.entries(attrs).forEach(([k, v]) => input.setAttribute(k, v)); wrap.appendChild(input); if (nearby) { const hint = document.createElement("p"); hint.className = "requirement"; hint.textContent = nearby; wrap.appendChild(hint); } document.body.appendChild(wrap); return input; } it("reads maxlength attribute", () => { setupPasswordField({ maxlength: "16" }); expect(detectPasswordPolicy().maxLength).toBe(16); }); it("reads minLength/maxLength from pattern attribute", () => { setupPasswordField({ pattern: ".{8,32}" }); const p = detectPasswordPolicy(); expect(p.minLength).toBe(8); expect(p.maxLength).toBe(32); }); it("parses 'at least N' from nearby text", () => { setupPasswordField({}, "Password must contain at least 12 characters."); expect(detectPasswordPolicy().minLength).toBe(12); }); it("parses 'cannot exceed N characters'", () => { setupPasswordField({}, "Password cannot exceed 40 characters."); expect(detectPasswordPolicy().maxLength).toBe(40); }); it("detects requires/forbids symbols from text", () => { setupPasswordField({}, "Must contain a special character and an uppercase letter."); const p = detectPasswordPolicy(); expect(p.requiresSymbols).toBe(true); expect(p.requiresUppercase).toBe(true); }); it("detects no-symbols phrasing", () => { setupPasswordField({}, "No special characters allowed in this password."); expect(detectPasswordPolicy().forbidsSymbols).toBe(true); }); it("extracts Oracle-style forbidden character list (space-separated)", () => { setupPasswordField( {}, "Password cannot exceed 40 characters, contain the users first name, last name, email address, spaces, or ` ~ < > \\ characters." ); const p = detectPasswordPolicy(); expect(p.maxLength).toBe(40); const forbidden = p.forbiddenChars ?? ""; for (const c of ["`", "~", "<", ">", "\\"]) { expect(forbidden).toContain(c); } }); it("extracts comma-separated forbidden chars", () => { setupPasswordField( {}, "Your password must not contain < , > , or \\ symbols." ); const forbidden = detectPasswordPolicy().forbiddenChars ?? ""; expect(forbidden).toContain("<"); expect(forbidden).toContain(">"); expect(forbidden).toContain("\\"); }); it("does not leak commas or periods from descriptive prose", () => { setupPasswordField( {}, "Cannot contain your first name, last name, or email address." ); const forbidden = detectPasswordPolicy().forbiddenChars ?? ""; expect(forbidden).toBe(""); }); it("returns empty policy when there is no password field", () => { expect(detectPasswordPolicy()).toEqual({}); }); it("extractNearbyText honors aria-describedby", () => { document.body.innerHTML = `
At least 12 characters required.
`; const el = document.getElementById("pw") as HTMLInputElement; expect(extractNearbyText(el)).toContain("At least 12"); }); });