Files
helper-tools/tests/helpers/random-limits-downloads.test.ts
T
2026-09-01 02:33:45 +02:00

134 lines
4.7 KiB
TypeScript

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(<T extends ArrayBufferView>(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 = <T extends ArrayBufferView>(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);
});
});