123import { beforeEach, describe, expect, it } from "vitest";4import { detectPasswordPolicy, extractNearbyText } from "./policy";56describe("detectPasswordPolicy", () => {7 it("PB-26-703 bounds hostile nearby DOM before joining text", () => {8 const input = document.createElement("input");9 input.type = "password";10 input.title = "x".repeat(20_000);11 const wrapper = document.createElement("div");12 wrapper.append(input);13 for (let i = 0; i < 1_000; i += 1) {14 const hint = document.createElement("span");15 hint.textContent = "y".repeat(20_000);16 wrapper.append(hint);17 }18 document.body.append(wrapper);1920 const text = extractNearbyText(input);21 expect(text.length).toBeLessThanOrEqual(4_096);22 });2324 beforeEach(() => {25 document.body.innerHTML = "";26 });2728 function setupPasswordField(attrs: Record<string, string> = {}, nearby = ""): HTMLInputElement {29 const wrap = document.createElement("div");30 const input = document.createElement("input");31 input.type = "password";32 Object.entries(attrs).forEach(([k, v]) => input.setAttribute(k, v));33 wrap.appendChild(input);34 if (nearby) {35 const hint = document.createElement("p");36 hint.className = "requirement";37 hint.textContent = nearby;38 wrap.appendChild(hint);39 }40 document.body.appendChild(wrap);41 return input;42 }4344 it("reads maxlength attribute", () => {45 setupPasswordField({ maxlength: "16" });46 expect(detectPasswordPolicy().maxLength).toBe(16);47 });4849 it("reads minLength/maxLength from pattern attribute", () => {50 setupPasswordField({ pattern: ".{8,32}" });51 const p = detectPasswordPolicy();52 expect(p.minLength).toBe(8);53 expect(p.maxLength).toBe(32);54 });5556 it("parses 'at least N' from nearby text", () => {57 setupPasswordField({}, "Password must contain at least 12 characters.");58 expect(detectPasswordPolicy().minLength).toBe(12);59 });6061 it("parses 'cannot exceed N characters'", () => {62 setupPasswordField({}, "Password cannot exceed 40 characters.");63 expect(detectPasswordPolicy().maxLength).toBe(40);64 });6566 it("detects requires/forbids symbols from text", () => {67 setupPasswordField({}, "Must contain a special character and an uppercase letter.");68 const p = detectPasswordPolicy();69 expect(p.requiresSymbols).toBe(true);70 expect(p.requiresUppercase).toBe(true);71 });7273 it("detects no-symbols phrasing", () => {74 setupPasswordField({}, "No special characters allowed in this password.");75 expect(detectPasswordPolicy().forbidsSymbols).toBe(true);76 });7778 it("extracts Oracle-style forbidden character list (space-separated)", () => {79 setupPasswordField(80 {},81 "Password cannot exceed 40 characters, contain the users first name, last name, email address, spaces, or ` ~ < > \\ characters."82 );83 const p = detectPasswordPolicy();84 expect(p.maxLength).toBe(40);85 const forbidden = p.forbiddenChars ?? "";86 for (const c of ["`", "~", "<", ">", "\\"]) {87 expect(forbidden).toContain(c);88 }89 });9091 it("extracts comma-separated forbidden chars", () => {92 setupPasswordField(93 {},94 "Your password must not contain < , > , or \\ symbols."95 );96 const forbidden = detectPasswordPolicy().forbiddenChars ?? "";97 expect(forbidden).toContain("<");98 expect(forbidden).toContain(">");99 expect(forbidden).toContain("\\");100 });101102 it("does not leak commas or periods from descriptive prose", () => {103 setupPasswordField(104 {},105 "Cannot contain your first name, last name, or email address."106 );107 const forbidden = detectPasswordPolicy().forbiddenChars ?? "";108 expect(forbidden).toBe("");109 });110111 it("returns empty policy when there is no password field", () => {112 expect(detectPasswordPolicy()).toEqual({});113 });114115 it("extractNearbyText honors aria-describedby", () => {116 document.body.innerHTML = `117 <input id="pw" type="password" aria-describedby="pw-help">118 <div id="pw-help">At least 12 characters required.</div>119 `;120 const el = document.getElementById("pw") as HTMLInputElement;121 expect(extractNearbyText(el)).toContain("At least 12");122 });123});