Import.test.tsx

4.1 KB · raw

1// @vitest-environment jsdom2//3// PB-26-112: the 10 MiB import ceiling used to be enforced AFTER the file was4// materialised as a string, but only on the CSV/JSON branch — the .peach picker5// checked `file.size` first. A multi-gigabyte CSV was therefore read into a6// popup string before any validation ran.7//8// Both branches now check `file.size` before `file.text()`. These tests assert9// the rejection path with a stubbed File: the stub's text() records the call, so10// "refused without reading it" is directly observable rather than inferred.1112import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";1314const { mockSendMessage } = vi.hoisted(() => ({ mockSendMessage: vi.fn() }));15vi.mock("../../shared/bridge", () => ({ sendMessage: mockSendMessage }));1617import { IMPORT_LIMITS } from "../../shared/import-limits";18import { ImportView } from "./Import";19import { flushView, renderView } from "./view-test-utils";2021let mounted: Array<{ dispose: () => void }> = [];2223/**24 * A File-shaped stub large enough to exceed the ceiling. `size` is the only25 * field the guards read; `text()` is what must never be called.26 */27function oversizedFile(name: string): File & { readCalls: number } {28  const file = {29    name,30    size: IMPORT_LIMITS.MAX_JSON_BYTES + 1,31    type: "text/csv",32    readCalls: 0,33    async text() {34      file.readCalls += 1;35      return "";36    },37    async arrayBuffer() {38      file.readCalls += 1;39      return new ArrayBuffer(0);40    },41  } as unknown as File & { readCalls: number };42  return file;43}4445function attachOversized(input: HTMLInputElement, file: File): void {46  Object.defineProperty(input, "files", {47    configurable: true,48    get: () => [file],49  });50}5152beforeEach(() => {53  mounted = [];54  mockSendMessage.mockReset();55  mockSendMessage.mockImplementation(() => Promise.resolve({ ok: true, data: null }));56  (globalThis as unknown as { chrome: unknown }).chrome = {57    runtime: {58      onMessage: { addListener: () => undefined, removeListener: () => undefined },59      getURL: (path: string) => `chrome-extension://test/${path}`,60    },61    storage: {62      local: {63        get: () => Promise.resolve({}),64        set: () => Promise.resolve(),65        remove: () => Promise.resolve(),66      },67      session: {68        get: () => Promise.resolve({}),69        set: () => Promise.resolve(),70        remove: () => Promise.resolve(),71      },72    },73  };74});7576afterEach(() => {77  for (const view of mounted) view.dispose();78  mounted = [];79  vi.restoreAllMocks();80});8182describe("PB-26-112: import branches bound the file before reading it", () => {83  it("the CSV/JSON picker refuses an oversized file without reading it", async () => {84    const view = renderView(() => (85      <ImportView initialView="file" onBack={() => undefined} onImport={() => undefined} />86    ));87    mounted.push(view);88    await flushView();8990    const picker = view.container.querySelector<HTMLInputElement>(91      'input[type="file"][accept=".csv,.json,.peach,application/x-peach"]',92    );93    expect(picker, "expected a CSV/JSON file input").not.toBeNull();9495    const file = oversizedFile("huge.csv");96    attachOversized(picker!, file);97    picker!.dispatchEvent(new Event("change", { bubbles: true }));98    await flushView();99100    expect(file.readCalls).toBe(0);101    expect(view.container.textContent).toContain("too large");102  });103104  it("the .peach picker refuses an oversized file without reading it", async () => {105    const view = renderView(() => (106      <ImportView initialView="peachscript" onBack={() => undefined} onImport={() => undefined} />107    ));108    mounted.push(view);109    await flushView();110111    const picker = view.container.querySelector<HTMLInputElement>(112      'input[type="file"][accept=".peach,application/x-peach,text/plain"]',113    );114    expect(picker, "expected a .peach file input").not.toBeNull();115116    const file = oversizedFile("huge.peach");117    attachOversized(picker!, file);118    picker!.dispatchEvent(new Event("change", { bubbles: true }));119    await flushView();120121    expect(file.readCalls).toBe(0);122    expect(view.container.textContent).toContain("too large");123  });124});