import { describe, expect, it, vi } from "vitest"; import { HelperLimitError, assertBoundedBytes, assertBoundedItems, assertBoundedText, createObjectUrlLease, createObjectUrlLeasePool, createSeededRandom, formatBytes, planBlobDownloads, sanitizeDownloadFilename, secureRandomBytes, secureRandomInt, shuffleSeeded, triggerBlobDownload, triggerBlobDownloads, } from "../../src/helpers"; describe("shared ceilings and download safety", () => { it("reports actual and configured bounds", () => { expect(() => assertBoundedText("123", 2)).toThrow(HelperLimitError); expect(() => assertBoundedBytes(new Uint8Array(3), 2)).toThrow(/limit/u); expect(() => assertBoundedItems(3, 2)).toThrow(/limit/u); try { assertBoundedText("123", 2, "Sample"); } catch (error) { expect(error).toMatchObject({ actual: 3, limit: 2 }); } }); it("sanitizes filenames and revokes object URLs exactly once", () => { expect(sanitizeDownloadFilename("../bad:\0name?.txt")).toBe( "_bad__name_.txt", ); expect(sanitizeDownloadFilename("...", "../fallback?.txt")).toBe( "_fallback_.txt", ); expect(sanitizeDownloadFilename("CON.txt")).toBe("_CON.txt"); expect(sanitizeDownloadFilename("report\u202egnp.exe")).toBe( "report_gnp.exe", ); const emojiBoundary = sanitizeDownloadFilename( `${"a".repeat(179)}😀.txt`, "fallback.txt", 180, ); expect(emojiBoundary).not.toMatch(/[\uD800-\uDBFF]$/u); expect(emojiBoundary.length).toBeLessThanOrEqual(180); expect(() => sanitizeDownloadFilename("x", "fallback", 0)).toThrow( /positive/u, ); const createObjectURL = vi.fn(() => "blob:test"); const revokeObjectURL = vi.fn(); const lease = createObjectUrlLease(new Blob(["x"]), { createObjectURL, revokeObjectURL, }); expect(lease).toMatchObject({ url: "blob:test", revoked: false }); lease.revoke(); lease.revoke(); expect(lease.revoked).toBe(true); expect(revokeObjectURL).toHaveBeenCalledOnce(); }); it("removes download anchors and revokes their URL on the next task", () => { const originalUrl = globalThis.URL; const createObjectURL = vi.fn(() => "blob:download"); const revokeObjectURL = vi.fn(); const click = vi .spyOn(HTMLAnchorElement.prototype, "click") .mockImplementation(() => undefined); vi.stubGlobal("URL", { createObjectURL, revokeObjectURL }); vi.useFakeTimers(); try { const lease = triggerBlobDownload(new Blob(["local"]), "../safe?.txt"); expect(click).toHaveBeenCalledOnce(); expect(click.mock.instances[0]).toMatchObject({ download: "_safe_.txt", }); expect(document.querySelector('a[href="blob:download"]')).toBeNull(); expect(lease.revoked).toBe(false); vi.runAllTimers(); expect(lease.revoked).toBe(true); expect(revokeObjectURL).toHaveBeenCalledWith("blob:download"); } finally { vi.useRealTimers(); vi.stubGlobal("URL", originalUrl); } }); it("formats IEC and SI byte quantities deterministically", () => { expect(formatBytes(0)).toBe("0 B"); expect(formatBytes(1536)).toBe("1.50 KiB"); expect(formatBytes(1500, { system: "si", fractionDigits: 1 })).toBe( "1.5 kB", ); expect(formatBytes(-1024)).toBe("-1.00 KiB"); expect(formatBytes(Number.NaN)).toBe("unknown"); }); it("replaces and revokes named object URL leases", () => { let sequence = 0; const revokeObjectURL = vi.fn(); const pool = createObjectUrlLeasePool({ createObjectURL: vi.fn(() => `blob:${++sequence}`), revokeObjectURL, }); const first = pool.create("preview", new Blob(["one"])); const second = pool.create("preview", new Blob(["two"])); expect(first.revoked).toBe(true); expect(second.url).toBe("blob:2"); expect(pool.size).toBe(1); expect(pool.revoke("missing")).toBe(false); pool.revokeAll(); expect(second.revoked).toBe(true); expect(pool.size).toBe(0); expect(revokeObjectURL).toHaveBeenCalledTimes(2); }); it("plans collision-free batch names and revokes every URL", () => { const items = [ { blob: new Blob(["a"]), filename: "Report.txt" }, { blob: new Blob(["b"]), filename: "report.txt" }, { blob: new Blob(["c"]), filename: "../unsafe?.txt" }, ]; expect(planBlobDownloads(items).map((item) => item.filename)).toEqual([ "Report.txt", "report (2).txt", "_unsafe_.txt", ]); const revokeObjectURL = vi.fn(); let index = 0; const click = vi .spyOn(HTMLAnchorElement.prototype, "click") .mockImplementation(() => undefined); let scheduled: (() => void) | undefined; const batch = triggerBlobDownloads(items, { ownerDocument: document, urlApi: { createObjectURL: () => `blob:batch-${++index}`, revokeObjectURL, }, schedule: (callback) => { scheduled = callback; }, }); expect(click).toHaveBeenCalledTimes(3); expect(batch.leases.every((lease) => !lease.revoked)).toBe(true); scheduled?.(); expect(batch.leases.every((lease) => lease.revoked)).toBe(true); expect(revokeObjectURL).toHaveBeenCalledTimes(3); }); it("bounds lazy download plans before consuming the whole iterable", () => { let yielded = 0; function* many() { while (true) { yielded += 1; yield { blob: new Blob(["x"]), filename: "same.txt" }; } } expect(() => planBlobDownloads(many(), { maximumFiles: 2 })).toThrow( HelperLimitError, ); expect(yielded).toBe(3); expect(() => planBlobDownloads([], { maximumFilenameLength: 7 })).toThrow( /at least 8/u, ); }); it("revokes already-created batch URLs when a click fails", () => { const revokeObjectURL = vi.fn(); const click = vi .spyOn(HTMLAnchorElement.prototype, "click") .mockImplementationOnce(() => undefined) .mockImplementationOnce(() => { throw new Error("blocked"); }); expect(() => triggerBlobDownloads( [ { blob: new Blob(["a"]), filename: "a.txt" }, { blob: new Blob(["b"]), filename: "b.txt" }, ], { ownerDocument: document, urlApi: { createObjectURL: () => `blob:test:${click.mock.calls.length}`, revokeObjectURL, }, }, ), ).toThrow("blocked"); expect(revokeObjectURL).toHaveBeenCalledTimes(2); }); }); describe("secure and seeded randomness", () => { it("fills secure byte requests in Web Crypto-sized chunks", () => { const getRandomValues = vi.fn((array: T) => { new Uint8Array(array.buffer, array.byteOffset, array.byteLength).fill(7); return array; }); const cryptoSource = { getRandomValues } as unknown as Pick< Crypto, "getRandomValues" >; expect(secureRandomBytes(70_000, cryptoSource)).toEqual( new Uint8Array(70_000).fill(7), ); expect(getRandomValues).toHaveBeenCalledTimes(2); expect(() => secureRandomBytes(2, cryptoSource, 1)).toThrow(/exceeds/u); }); it("uses rejection sampling for bounded secure integers", () => { const samples = [0xffffffff, 7]; const getRandomValues = (array: T): T => { new Uint32Array(array.buffer, array.byteOffset, 1)[0] = samples.shift() ?? 0; return array; }; expect(secureRandomInt(10, 20, { getRandomValues })).toBe(17); }); it("produces reproducible seeded primitives without mutating inputs", () => { const first = createSeededRandom("repeatable"); const second = createSeededRandom("repeatable"); expect(Array.from({ length: 8 }, () => first.nextUint32())).toEqual( Array.from({ length: 8 }, () => second.nextUint32()), ); const input = [1, 2, 3, 4, 5]; const shuffled = shuffleSeeded(input, 42); expect(shuffled).toEqual(shuffleSeeded(input, 42)); expect(shuffled).not.toEqual(input); expect(input).toEqual([1, 2, 3, 4, 5]); expect(createSeededRandom("bytes").bytes(7)).toHaveLength(7); const value = createSeededRandom("float").nextFloat(); expect(value).toBeGreaterThanOrEqual(0); expect(value).toBeLessThan(1); }); });