// @vitest-environment jsdom // // PB-26-208: every .peach entry path checks File.size before file.text() — // including drag/drop and the file input on the lock screen restore form. // The stub File's text() records the call, so "refused without reading it" // is directly observable rather than inferred. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { mockSendMessage } = vi.hoisted(() => ({ mockSendMessage: vi.fn() })); vi.mock("../App", () => ({ sendMessage: mockSendMessage })); vi.mock("../components/RuneBackground", () => ({ RuneBackground: () => null, })); vi.mock("../components/RuneCanvas", () => ({ RuneCanvas: () => null })); import { IMPORT_LIMITS } from "../../shared/import-limits"; import { LockView } from "./Lock"; import { flushView, renderView } from "./view-test-utils"; let mounted: Array<{ dispose: () => void }> = []; /** * A File-shaped stub over the ceiling. `size` is the only field the guards * read; `text()` is what must never be called. */ function oversizedFile(name: string): File & { readCalls: number } { const file = { name, size: IMPORT_LIMITS.MAX_JSON_BYTES + 1, type: "text/plain", readCalls: 0, async text() { file.readCalls += 1; return ""; }, async arrayBuffer() { file.readCalls += 1; return new ArrayBuffer(0); }, } as unknown as File & { readCalls: number }; return file; } function renderSymbolsRestore() { const view = renderView(() => ( undefined} initialRestore={true} initialRestoreSource="symbols" /> )); mounted.push(view); return view; } beforeEach(() => { mounted = []; mockSendMessage.mockReset(); mockSendMessage.mockImplementation(() => Promise.resolve({ ok: false })); (globalThis as unknown as { chrome: unknown }).chrome = { runtime: { onMessage: { addListener: () => undefined, removeListener: () => undefined }, getURL: (path: string) => `chrome-extension://test/${path}`, }, storage: { local: { get: () => Promise.resolve({}), set: () => Promise.resolve(), remove: () => Promise.resolve(), }, session: { get: () => Promise.resolve({}), set: () => Promise.resolve(), remove: () => Promise.resolve(), }, }, }; }); afterEach(() => { for (const view of mounted) view.dispose(); mounted = []; vi.restoreAllMocks(); }); describe("PB-26-208: lock-screen .peach paths bound the file before reading it", () => { it("drag/drop refuses an oversized file without reading it", async () => { const view = renderSymbolsRestore(); await flushView(); const form = view.container.querySelector(".peach-restore-form"); expect(form, "expected the symbols restore form").not.toBeNull(); const file = oversizedFile("vault.peach"); const drop = new Event("drop", { bubbles: true, cancelable: true }) as Event & { dataTransfer?: { files: File[] }; }; drop.dataTransfer = { files: [file] }; form!.dispatchEvent(drop); await flushView(); expect(file.readCalls).toBe(0); expect(view.container.textContent).toContain("too large"); }); it("the file input refuses an oversized file without reading it", async () => { const view = renderSymbolsRestore(); await flushView(); const picker = view.container.querySelector( 'input[type="file"][accept=".peach,text/plain"]', ); expect(picker, "expected a .peach file input").not.toBeNull(); const file = oversizedFile("vault.peach"); Object.defineProperty(picker!, "files", { configurable: true, get: () => [file], }); picker!.dispatchEvent(new Event("change", { bubbles: true })); await flushView(); expect(file.readCalls).toBe(0); expect(view.container.textContent).toContain("too large"); }); });