import { Buffer } from "node:buffer"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const cryptoMocks = vi.hoisted(() => ({ randomBytes: vi.fn((bytes: number) => Buffer.alloc(bytes, 0xab)), randomInt: vi.fn(), randomUUID: vi.fn(), })); vi.mock("node:crypto", () => ({ randomBytes: cryptoMocks.randomBytes, randomInt: cryptoMocks.randomInt, randomUUID: cryptoMocks.randomUUID, })); let generateSecureFraction: typeof import("./secure-random.js").generateSecureFraction; let generateSecureHex: typeof import("./secure-random.js").generateSecureHex; let generateSecureInt: typeof import("./secure-random.js").generateSecureInt; let generateSecureToken: typeof import("./secure-random.js").generateSecureToken; let generateSecureUuid: typeof import("./secure-random.js").generateSecureUuid; beforeAll(async () => { ({ generateSecureFraction, generateSecureHex, generateSecureInt, generateSecureToken, generateSecureUuid, } = await import("./secure-random.js")); }); beforeEach(() => { cryptoMocks.randomBytes.mockClear(); cryptoMocks.randomUUID.mockReset(); }); describe("secure-random", () => { it("delegates UUID generation to crypto.randomUUID", () => { cryptoMocks.randomUUID.mockReturnValueOnce("uuid-1").mockReturnValueOnce("uuid-2"); expect(generateSecureUuid()).toBe("uuid-1"); expect(generateSecureUuid()).toBe("uuid-2"); expect(cryptoMocks.randomUUID).toHaveBeenCalledTimes(2); }); it.each([ { name: "uses the default byte count", byteCount: undefined, expectedBytes: 16, expectedToken: Buffer.alloc(16, 0xab).toString("base64url"), }, { name: "passes custom byte counts through", byteCount: 18, expectedBytes: 18, expectedToken: Buffer.alloc(18, 0xab).toString("base64url"), }, { name: "supports zero-byte tokens", byteCount: 0, expectedBytes: 0, expectedToken: "", }, ])("generates url-safe tokens when $name", ({ byteCount, expectedBytes, expectedToken }) => { cryptoMocks.randomBytes.mockClear(); const token = byteCount === undefined ? generateSecureToken() : generateSecureToken(byteCount); expect(cryptoMocks.randomBytes).toHaveBeenCalledWith(expectedBytes); expect(token).toBe(expectedToken); expect(token).toMatch(/^[A-Za-z0-9_-]*$/); }); it("generates secure hex strings", () => { cryptoMocks.randomBytes.mockClear(); expect(generateSecureHex(4)).toBe(Buffer.alloc(4, 0xab).toString("hex")); expect(cryptoMocks.randomBytes).toHaveBeenCalledWith(4); }); it("maps random bytes into a unit interval fraction", () => { cryptoMocks.randomBytes.mockReturnValueOnce(Buffer.from([0x80, 0x00, 0x00, 0x00])); expect(generateSecureFraction()).toBe(0.5); expect(cryptoMocks.randomBytes).toHaveBeenCalledWith(4); }); it("delegates bounded integer generation to crypto.randomInt", () => { cryptoMocks.randomInt.mockReturnValueOnce(2).mockReturnValueOnce(7); expect(generateSecureInt(5)).toBe(2); expect(generateSecureInt(3, 9)).toBe(7); expect(cryptoMocks.randomInt).toHaveBeenNthCalledWith(1, 5); expect(cryptoMocks.randomInt).toHaveBeenNthCalledWith(2, 3, 9); }); });