Lock.test.tsx

3.9 KB · raw

1// @vitest-environment jsdom2//3// PB-26-208: every .peach entry path checks File.size before file.text() —4// including drag/drop and the file input on the lock screen restore form.5// The stub File's text() records the call, so "refused without reading it"6// is directly observable rather than inferred.7import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";89const { mockSendMessage } = vi.hoisted(() => ({ mockSendMessage: vi.fn() }));10vi.mock("../App", () => ({ sendMessage: mockSendMessage }));11vi.mock("../components/RuneBackground", () => ({12  RuneBackground: () => null,13}));14vi.mock("../components/RuneCanvas", () => ({ RuneCanvas: () => null }));1516import { IMPORT_LIMITS } from "../../shared/import-limits";17import { LockView } from "./Lock";18import { flushView, renderView } from "./view-test-utils";1920let mounted: Array<{ dispose: () => void }> = [];2122/**23 * A File-shaped stub over the ceiling. `size` is the only field the guards24 * read; `text()` is what must never be called.25 */26function oversizedFile(name: string): File & { readCalls: number } {27  const file = {28    name,29    size: IMPORT_LIMITS.MAX_JSON_BYTES + 1,30    type: "text/plain",31    readCalls: 0,32    async text() {33      file.readCalls += 1;34      return "";35    },36    async arrayBuffer() {37      file.readCalls += 1;38      return new ArrayBuffer(0);39    },40  } as unknown as File & { readCalls: number };41  return file;42}4344function renderSymbolsRestore() {45  const view = renderView(() => (46    <LockView47      hasVault={true}48      ready={true}49      onUnlock={() => undefined}50      initialRestore={true}51      initialRestoreSource="symbols"52    />53  ));54  mounted.push(view);55  return view;56}5758beforeEach(() => {59  mounted = [];60  mockSendMessage.mockReset();61  mockSendMessage.mockImplementation(() => Promise.resolve({ ok: false }));62  (globalThis as unknown as { chrome: unknown }).chrome = {63    runtime: {64      onMessage: { addListener: () => undefined, removeListener: () => undefined },65      getURL: (path: string) => `chrome-extension://test/${path}`,66    },67    storage: {68      local: {69        get: () => Promise.resolve({}),70        set: () => Promise.resolve(),71        remove: () => Promise.resolve(),72      },73      session: {74        get: () => Promise.resolve({}),75        set: () => Promise.resolve(),76        remove: () => Promise.resolve(),77      },78    },79  };80});8182afterEach(() => {83  for (const view of mounted) view.dispose();84  mounted = [];85  vi.restoreAllMocks();86});8788describe("PB-26-208: lock-screen .peach paths bound the file before reading it", () => {89  it("drag/drop refuses an oversized file without reading it", async () => {90    const view = renderSymbolsRestore();91    await flushView();9293    const form = view.container.querySelector<HTMLElement>(".peach-restore-form");94    expect(form, "expected the symbols restore form").not.toBeNull();9596    const file = oversizedFile("vault.peach");97    const drop = new Event("drop", { bubbles: true, cancelable: true }) as Event & {98      dataTransfer?: { files: File[] };99    };100    drop.dataTransfer = { files: [file] };101    form!.dispatchEvent(drop);102    await flushView();103104    expect(file.readCalls).toBe(0);105    expect(view.container.textContent).toContain("too large");106  });107108  it("the file input refuses an oversized file without reading it", async () => {109    const view = renderSymbolsRestore();110    await flushView();111112    const picker = view.container.querySelector<HTMLInputElement>(113      'input[type="file"][accept=".peach,text/plain"]',114    );115    expect(picker, "expected a .peach file input").not.toBeNull();116117    const file = oversizedFile("vault.peach");118    Object.defineProperty(picker!, "files", {119      configurable: true,120      get: () => [file],121    });122    picker!.dispatchEvent(new Event("change", { bubbles: true }));123    await flushView();124125    expect(file.readCalls).toBe(0);126    expect(view.container.textContent).toContain("too large");127  });128});