policy.test.ts

4.1 KB · raw

1// @vitest-environment jsdom23import { 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});