import { describe, expect, it, vi } from "vitest"; import { HelperLimitError, assertBoundedBytes, assertBoundedItems, assertBoundedText, createObjectUrlLease, createSeededRandom, sanitizeDownloadFilename, secureRandomBytes, secureRandomInt, shuffleSeeded, triggerBlobDownload, } 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); } }); }); 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); }); });