// @vitest-environment jsdom import { beforeEach, describe, expect, it, vi } from "vitest"; import { fillFields, fillCardForm, fillTOTPCode, formatCardNumber } from "./fill"; describe("content fill", () => { beforeEach(() => { document.body.innerHTML = ""; }); function makeVisibleInput(type: string): HTMLInputElement { const input = document.createElement("input"); input.type = type; Object.defineProperty(input, "getBoundingClientRect", { configurable: true, value: () => ({ width: 100, height: 20, top: 0, left: 0, right: 100, bottom: 20, x: 0, y: 0, toJSON: () => ({}), }), }); document.body.appendChild(input); return input; } it("fillFields writes username and password into matching inputs", () => { const username = makeVisibleInput("text"); const password = makeVisibleInput("password"); fillFields(username, password, "june", "hunter2"); expect(username.value).toBe("june"); expect(password.value).toBe("hunter2"); }); it("fillFields refuses to fill when the form action domain does not match", () => { const form = document.createElement("form"); form.action = "https://evil.com/login"; const username = makeVisibleInput("text"); const password = makeVisibleInput("password"); form.append(username, password); document.body.appendChild(form); fillFields(username, password, "june", "hunter2", form, "github.com"); expect(username.value).toBe(""); expect(password.value).toBe(""); }); // PB-26-008: where a
exists, the action must be https and on the // expected domain or a subdomain of it — not merely the same eTLD+1. it("fillFields refuses a plain-http form action (PB-26-008)", () => { const form = document.createElement("form"); form.action = "http://github.com/login"; const username = makeVisibleInput("text"); const password = makeVisibleInput("password"); form.append(username, password); document.body.appendChild(form); fillFields(username, password, "june", "hunter2", form, "github.com"); expect(username.value).toBe(""); expect(password.value).toBe(""); }); it("fillFields allows an https form action on a subdomain of the expected domain (PB-26-008)", () => { const form = document.createElement("form"); form.action = "https://auth.github.com/session"; const username = makeVisibleInput("text"); const password = makeVisibleInput("password"); form.append(username, password); document.body.appendChild(form); fillFields(username, password, "june", "hunter2", form, "github.com"); expect(username.value).toBe("june"); expect(password.value).toBe("hunter2"); }); it("fillFields still fills the username when the username field is hidden", () => { const username = makeVisibleInput("text"); const password = makeVisibleInput("password"); Object.defineProperty(username, "getBoundingClientRect", { value: () => ({ width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0, toJSON: () => ({}), }), }); fillFields(username, password, "june", "hunter2"); expect(username.value).toBe("june"); expect(password.value).toBe("hunter2"); }); it("fillTOTPCode respects the form-action gate (PB-26-105)", () => { const form = document.createElement("form"); form.action = "https://evil.com/login"; const totpEl = makeVisibleInput("text"); form.appendChild(totpEl); document.body.appendChild(form); const filled = fillTOTPCode(totpEl, "123456", form, "github.com"); expect(filled).toBe(false); expect(totpEl.value).toBe(""); }); it("fillTOTPCode allows https form action on expected domain (PB-26-105)", () => { const form = document.createElement("form"); form.action = "https://github.com/login"; const totpEl = makeVisibleInput("text"); form.appendChild(totpEl); document.body.appendChild(form); const filled = fillTOTPCode(totpEl, "123456", form, "github.com"); expect(filled).toBe(true); expect(totpEl.value).toBe("123456"); }); it("fillFields dispatches standard input/change events rather than custom password events", () => { const username = makeVisibleInput("text"); const password = makeVisibleInput("password"); const usernameDispatch = vi.spyOn(username, "dispatchEvent"); const passwordDispatch = vi.spyOn(password, "dispatchEvent"); fillFields(username, password, "june", "hunter2"); expect(usernameDispatch.mock.calls.map(([event]) => event.type)).toEqual(["input", "change"]); expect(passwordDispatch.mock.calls.map(([event]) => event.type)).toEqual(["input", "change"]); expect(passwordDispatch.mock.calls.every(([event]) => !(event instanceof CustomEvent))).toBe(true); }); }); describe("card fill", () => { beforeEach(() => { document.body.innerHTML = ""; }); function makeInput(attrs: Record = {}): HTMLInputElement { const input = document.createElement("input"); for (const [k, v] of Object.entries(attrs)) input.setAttribute(k, v); // jsdom rects are all-zero, which isVisible treats as hidden — stub a // real rect so these inputs count as visible unless a test hides them. Object.defineProperty(input, "getBoundingClientRect", { configurable: true, value: () => ({ width: 100, height: 20, top: 0, left: 0, right: 100, bottom: 20, x: 0, y: 0, toJSON: () => ({}), }), }); document.body.appendChild(input); return input; } function makeHiddenInput(attrs: Record = {}): HTMLInputElement { const input = makeInput(attrs); Object.defineProperty(input, "getBoundingClientRect", { configurable: true, value: () => ({ width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0, toJSON: () => ({}), }), }); return input; } // PB-26-007: card fill must apply the same visibility gate as the // password path — a crafted page must not be able to exfiltrate card // data into hidden fields. it("fillCardForm — skips hidden fields (PB-26-007)", () => { const number = makeHiddenInput({ type: "text" }); const cvc = makeHiddenInput({ type: "text" }); const name = makeInput({ type: "text" }); fillCardForm( { number, expMonth: null, expYear: null, expiryCombo: null, cvc, name }, { cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardholderName: "Jane Smith" } ); expect(number.value).toBe(""); expect(cvc.value).toBe(""); expect(name.value).toBe("Jane Smith"); }); it("fillCardForm — fills number, expiry, CVC, and name fields", () => { const number = makeInput({ type: "text" }); const expMonth = makeInput({ type: "text" }); const expYear = makeInput({ type: "text" }); const cvc = makeInput({ type: "text" }); const name = makeInput({ type: "text" }); fillCardForm( { number, expMonth, expYear, expiryCombo: null, cvc, name }, { cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardholderName: "Jane Smith" } ); expect(number.value).toBe("4111 1111 1111 1111"); expect(expMonth.value).toBe("05"); expect(expYear.value).toBe("2028"); expect(cvc.value).toBe("123"); expect(name.value).toBe("Jane Smith"); }); it("fillCardForm — handles combined expiry field (MM/YY)", () => { const number = makeInput({ type: "text" }); const expiryCombo = makeInput({ type: "text" }); const cvc = makeInput({ type: "text" }); fillCardForm( { number, expMonth: null, expYear: null, expiryCombo, cvc, name: null }, { cardNumber: "5555555555554444", expMonth: "12", expYear: "2026", cvc: "456", cardholderName: "John Doe" } ); expect(expiryCombo.value).toBe("12/26"); }); it("fillCardForm — dispatches input and change events for React compatibility", () => { const number = makeInput({ type: "text" }); const numberDispatch = vi.spyOn(number, "dispatchEvent"); fillCardForm( { number, expMonth: null, expYear: null, expiryCombo: null, cvc: null, name: null }, { cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardholderName: "Jane" } ); expect(numberDispatch.mock.calls.map(([event]) => event.type)).toEqual(["input", "change"]); }); it("formatCardNumber — formats Visa as 4-4-4-4", () => { expect(formatCardNumber("4111111111111111")).toBe("4111 1111 1111 1111"); }); it("formatCardNumber — formats Amex as 4-6-5", () => { expect(formatCardNumber("378282246310005")).toBe("3782 822463 10005"); }); });