fill.test.ts

7.9 KB · raw

1// @vitest-environment jsdom23import { beforeEach, describe, expect, it, vi } from "vitest";45import { fillFields, fillCardForm, formatCardNumber } from "./fill";67describe("content fill", () => {8  beforeEach(() => {9    document.body.innerHTML = "";10  });1112  function makeVisibleInput(type: string): HTMLInputElement {13    const input = document.createElement("input");14    input.type = type;15    Object.defineProperty(input, "getBoundingClientRect", {16      configurable: true,17      value: () => ({18        width: 100,19        height: 20,20        top: 0,21        left: 0,22        right: 100,23        bottom: 20,24        x: 0,25        y: 0,26        toJSON: () => ({}),27      }),28    });29    document.body.appendChild(input);30    return input;31  }3233  it("fillFields writes username and password into matching inputs", () => {34    const username = makeVisibleInput("text");35    const password = makeVisibleInput("password");3637    fillFields(username, password, "june", "hunter2");3839    expect(username.value).toBe("june");40    expect(password.value).toBe("hunter2");41  });4243  it("fillFields refuses to fill when the form action domain does not match", () => {44    const form = document.createElement("form");45    form.action = "https://evil.com/login";46    const username = makeVisibleInput("text");47    const password = makeVisibleInput("password");48    form.append(username, password);49    document.body.appendChild(form);5051    fillFields(username, password, "june", "hunter2", form, "github.com");5253    expect(username.value).toBe("");54    expect(password.value).toBe("");55  });5657  // PB-26-008: where a <form> exists, the action must be https and on the58  // expected domain or a subdomain of it — not merely the same eTLD+1.59  it("fillFields refuses a plain-http form action (PB-26-008)", () => {60    const form = document.createElement("form");61    form.action = "http://github.com/login";62    const username = makeVisibleInput("text");63    const password = makeVisibleInput("password");64    form.append(username, password);65    document.body.appendChild(form);6667    fillFields(username, password, "june", "hunter2", form, "github.com");6869    expect(username.value).toBe("");70    expect(password.value).toBe("");71  });7273  it("fillFields allows an https form action on a subdomain of the expected domain (PB-26-008)", () => {74    const form = document.createElement("form");75    form.action = "https://auth.github.com/session";76    const username = makeVisibleInput("text");77    const password = makeVisibleInput("password");78    form.append(username, password);79    document.body.appendChild(form);8081    fillFields(username, password, "june", "hunter2", form, "github.com");8283    expect(username.value).toBe("june");84    expect(password.value).toBe("hunter2");85  });8687  it("fillFields still fills the username when the username field is hidden", () => {88    const username = makeVisibleInput("text");89    const password = makeVisibleInput("password");90    Object.defineProperty(username, "getBoundingClientRect", {91      value: () => ({92        width: 0,93        height: 0,94        top: 0,95        left: 0,96        right: 0,97        bottom: 0,98        x: 0,99        y: 0,100        toJSON: () => ({}),101      }),102    });103104    fillFields(username, password, "june", "hunter2");105106    expect(username.value).toBe("june");107    expect(password.value).toBe("hunter2");108  });109110  it("fillFields dispatches standard input/change events rather than custom password events", () => {111    const username = makeVisibleInput("text");112    const password = makeVisibleInput("password");113    const usernameDispatch = vi.spyOn(username, "dispatchEvent");114    const passwordDispatch = vi.spyOn(password, "dispatchEvent");115116    fillFields(username, password, "june", "hunter2");117118    expect(usernameDispatch.mock.calls.map(([event]) => event.type)).toEqual(["input", "change"]);119    expect(passwordDispatch.mock.calls.map(([event]) => event.type)).toEqual(["input", "change"]);120    expect(passwordDispatch.mock.calls.every(([event]) => !(event instanceof CustomEvent))).toBe(true);121  });122});123124describe("card fill", () => {125  beforeEach(() => {126    document.body.innerHTML = "";127  });128129  function makeInput(attrs: Record<string, string> = {}): HTMLInputElement {130    const input = document.createElement("input");131    for (const [k, v] of Object.entries(attrs)) input.setAttribute(k, v);132    // jsdom rects are all-zero, which isVisible treats as hidden — stub a133    // real rect so these inputs count as visible unless a test hides them.134    Object.defineProperty(input, "getBoundingClientRect", {135      configurable: true,136      value: () => ({137        width: 100,138        height: 20,139        top: 0,140        left: 0,141        right: 100,142        bottom: 20,143        x: 0,144        y: 0,145        toJSON: () => ({}),146      }),147    });148    document.body.appendChild(input);149    return input;150  }151152  function makeHiddenInput(attrs: Record<string, string> = {}): HTMLInputElement {153    const input = makeInput(attrs);154    Object.defineProperty(input, "getBoundingClientRect", {155      configurable: true,156      value: () => ({157        width: 0,158        height: 0,159        top: 0,160        left: 0,161        right: 0,162        bottom: 0,163        x: 0,164        y: 0,165        toJSON: () => ({}),166      }),167    });168    return input;169  }170171  // PB-26-007: card fill must apply the same visibility gate as the172  // password path — a crafted page must not be able to exfiltrate card173  // data into hidden fields.174  it("fillCardForm — skips hidden fields (PB-26-007)", () => {175    const number = makeHiddenInput({ type: "text" });176    const cvc = makeHiddenInput({ type: "text" });177    const name = makeInput({ type: "text" });178179    fillCardForm(180      { number, expMonth: null, expYear: null, expiryCombo: null, cvc, name },181      { cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardholderName: "Jane Smith" }182    );183184    expect(number.value).toBe("");185    expect(cvc.value).toBe("");186    expect(name.value).toBe("Jane Smith");187  });188189  it("fillCardForm — fills number, expiry, CVC, and name fields", () => {190    const number = makeInput({ type: "text" });191    const expMonth = makeInput({ type: "text" });192    const expYear = makeInput({ type: "text" });193    const cvc = makeInput({ type: "text" });194    const name = makeInput({ type: "text" });195196    fillCardForm(197      { number, expMonth, expYear, expiryCombo: null, cvc, name },198      { cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardholderName: "Jane Smith" }199    );200201    expect(number.value).toBe("4111 1111 1111 1111");202    expect(expMonth.value).toBe("05");203    expect(expYear.value).toBe("2028");204    expect(cvc.value).toBe("123");205    expect(name.value).toBe("Jane Smith");206  });207208  it("fillCardForm — handles combined expiry field (MM/YY)", () => {209    const number = makeInput({ type: "text" });210    const expiryCombo = makeInput({ type: "text" });211    const cvc = makeInput({ type: "text" });212213    fillCardForm(214      { number, expMonth: null, expYear: null, expiryCombo, cvc, name: null },215      { cardNumber: "5555555555554444", expMonth: "12", expYear: "2026", cvc: "456", cardholderName: "John Doe" }216    );217218    expect(expiryCombo.value).toBe("12/26");219  });220221  it("fillCardForm — dispatches input and change events for React compatibility", () => {222    const number = makeInput({ type: "text" });223    const numberDispatch = vi.spyOn(number, "dispatchEvent");224225    fillCardForm(226      { number, expMonth: null, expYear: null, expiryCombo: null, cvc: null, name: null },227      { cardNumber: "4111111111111111", expMonth: "05", expYear: "2028", cvc: "123", cardholderName: "Jane" }228    );229230    expect(numberDispatch.mock.calls.map(([event]) => event.type)).toEqual(["input", "change"]);231  });232233  it("formatCardNumber — formats Visa as 4-4-4-4", () => {234    expect(formatCardNumber("4111111111111111")).toBe("4111 1111 1111 1111");235  });236237  it("formatCardNumber — formats Amex as 4-6-5", () => {238    expect(formatCardNumber("378282246310005")).toBe("3782 822463 10005");239  });240});