Files
bun.sh/test/js/bun/util/randomUUIDv5.test.ts
Jarred Sumner 27c979129c Introduce Bun.randomUUIDv5 (#20782)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: jarred <jarred@bun.sh>
Co-authored-by: Meghan Denny <meghan@bun.sh>
2025-07-02 22:47:14 -07:00

383 lines
12 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import * as uuid from "uuid";
describe("randomUUIDv5", () => {
const dnsNamespace = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
const urlNamespace = "6ba7b811-9dad-11d1-80b4-00c04fd430c8";
test("basic functionality", () => {
const result = Bun.randomUUIDv5("www.example.com", dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
// Check that it's version 5
expect(result[14]).toBe("5");
});
test("deterministic output", () => {
const uuid1 = Bun.randomUUIDv5("www.example.com", dnsNamespace);
const uuid2 = Bun.randomUUIDv5("www.example.com", dnsNamespace);
// Should always generate the same UUID for the same namespace + name
expect(uuid1).toBe(uuid2);
});
test("compatibility with uuid library", () => {
const name = "www.example.com";
const bunUuid = Bun.randomUUIDv5(name, dnsNamespace);
const uuidLibUuid = uuid.v5(name, dnsNamespace);
expect(bunUuid).toBe(uuidLibUuid);
});
test("predefined namespace strings", () => {
// Test with predefined namespace strings
const uuid1 = Bun.randomUUIDv5("www.example.com", "dns");
const uuid2 = Bun.randomUUIDv5("www.example.com", dnsNamespace);
expect(uuid1).toBe(uuid2);
const uuid3 = Bun.randomUUIDv5("http://example.com", "url");
const uuid4 = Bun.randomUUIDv5("http://example.com", urlNamespace);
expect(uuid3).toBe(uuid4);
});
test("empty name", () => {
const result = Bun.randomUUIDv5("", dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
});
test("long name", () => {
const longName = "a".repeat(1000);
const result = Bun.randomUUIDv5(longName, dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
});
test("unicode name", () => {
const unicodeName = "测试.example.com";
const result = Bun.randomUUIDv5(unicodeName, dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
// Should be deterministic
const uuid2 = Bun.randomUUIDv5(unicodeName, dnsNamespace);
expect(result).toBe(uuid2);
});
test("name as ArrayBuffer", () => {
const nameString = "test";
const nameBuffer = new TextEncoder().encode(nameString);
const uuid1 = Bun.randomUUIDv5(nameString, dnsNamespace);
const uuid2 = Bun.randomUUIDv5(nameBuffer, dnsNamespace);
expect(uuid1).toBe(uuid2);
});
test("name as TypedArray", () => {
const nameString = "test";
const nameArray = new Uint8Array(new TextEncoder().encode(nameString));
const uuid1 = Bun.randomUUIDv5(nameString, dnsNamespace);
const uuid2 = Bun.randomUUIDv5(nameArray, dnsNamespace);
expect(uuid1).toBe(uuid2);
});
test("error handling - invalid namespace", () => {
expect(() => {
Bun.randomUUIDv5("test", "invalid-uuid");
}).toThrow();
});
test("error handling - wrong namespace buffer size", () => {
const wrongSizeBuffer = new Uint8Array(15); // Should be 16 bytes
expect(() => {
Bun.randomUUIDv5("test", wrongSizeBuffer);
}).toThrow();
});
test("error handling - invalid encoding", () => {
expect(() => {
// @ts-expect-error - testing invalid encoding
Bun.randomUUIDv5("test", dnsNamespace, "invalid");
}).toThrow();
});
test("variant bits are correct", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace);
const bytes = result.replace(/-/g, "");
// Extract the variant byte (17th hex character, index 16)
const variantByte = parseInt(bytes.substr(16, 2), 16);
// Variant bits should be 10xxxxxx (0x80-0xBF)
expect(variantByte & 0xc0).toBe(0x80);
});
test("version bits are correct", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace);
const bytes = result.replace(/-/g, "");
// Extract the version byte (13th hex character, index 12)
const versionByte = parseInt(bytes.substr(12, 2), 16);
// Version bits should be 0101xxxx (0x50-0x5F)
expect(versionByte & 0xf0).toBe(0x50);
});
test("case insensitive namespace strings", () => {
const uuid1 = Bun.randomUUIDv5("test", "DNS");
const uuid2 = Bun.randomUUIDv5("test", "dns");
const uuid3 = Bun.randomUUIDv5("test", "Dns");
expect(uuid1).toBe(uuid2);
expect(uuid2).toBe(uuid3);
});
test("all predefined namespaces", () => {
const name = "test";
const dnsUuid = Bun.randomUUIDv5(name, "dns");
const urlUuid = Bun.randomUUIDv5(name, "url");
const oidUuid = Bun.randomUUIDv5(name, "oid");
const x500Uuid = Bun.randomUUIDv5(name, "x500");
// All should be different
expect(dnsUuid).not.toBe(urlUuid);
expect(urlUuid).not.toBe(oidUuid);
expect(oidUuid).not.toBe(x500Uuid);
// All should be valid UUIDs
[dnsUuid, urlUuid, oidUuid, x500Uuid].forEach(result => {
expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
expect(result[14]).toBe("5");
});
});
test("different namespaces produce different UUIDs", () => {
const uuid1 = Bun.randomUUIDv5("www.example.com", dnsNamespace);
const uuid2 = Bun.randomUUIDv5("www.example.com", urlNamespace);
expect(uuid1).not.toBe(uuid2);
expect(uuid.v5("www.example.com", dnsNamespace)).toBe(uuid1);
expect(uuid.v5("www.example.com", urlNamespace)).toBe(uuid2);
});
test("different names produce different UUIDs", () => {
const uuid1 = Bun.randomUUIDv5("www.example.com", dnsNamespace);
const uuid2 = Bun.randomUUIDv5("api.example.com", dnsNamespace);
expect(uuid1).not.toBe(uuid2);
});
test("hex encoding (default)", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace);
expect(result).toMatch(/^[0-9a-f-]+$/);
expect(result.length).toBe(36); // Standard UUID string length
});
test("buffer encoding", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace, "buffer");
expect(result).toBeInstanceOf(Uint8Array);
expect(result.byteLength).toBe(16);
});
test("base64 encoding", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace, "base64");
expect(result).toBeTypeOf("string");
expect(result).toMatch(/^[A-Za-z0-9+/=]+$/);
});
test("base64url encoding", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace, "base64url");
expect(result).toBeTypeOf("string");
expect(result).toMatch(/^[A-Za-z0-9_-]+$/);
});
test("namespace as Buffer", () => {
// Convert UUID string to buffer
const nsBytes = new Uint8Array(16);
const nsString = dnsNamespace.replace(/-/g, "");
for (let i = 0; i < 16; i++) {
nsBytes[i] = parseInt(nsString.substr(i * 2, 2), 16);
}
const uuid1 = Bun.randomUUIDv5("test", dnsNamespace);
const uuid2 = Bun.randomUUIDv5("test", nsBytes);
expect(uuid1).toBe(uuid2);
});
test("name as Buffer", () => {
const nameBuffer = new TextEncoder().encode("test");
const uuid1 = Bun.randomUUIDv5("test", dnsNamespace);
const uuid2 = Bun.randomUUIDv5(nameBuffer, dnsNamespace);
expect(uuid1).toBe(uuid2);
});
// Ported v5 tests from uuid library test suite
test("v5 - hello.example.com with DNS namespace", () => {
expect(Bun.randomUUIDv5("hello.example.com", dnsNamespace)).toBe("fdda765f-fc57-5604-a269-52a7df8164ec");
});
test("v5 - http://example.com/hello with URL namespace", () => {
expect(Bun.randomUUIDv5("http://example.com/hello", urlNamespace)).toBe("3bbcee75-cecc-5b56-8031-b6641c1ed1f1");
});
test("v5 - hello with custom namespace", () => {
expect(Bun.randomUUIDv5("hello", "0f5abcd1-c194-47f3-905b-2df7263a084b")).toBe(
"90123e1c-7512-523e-bb28-76fab9f2f73d",
);
});
test("v5 namespace.toUpperCase", () => {
expect(Bun.randomUUIDv5("hello.example.com", dnsNamespace.toUpperCase())).toBe(
"fdda765f-fc57-5604-a269-52a7df8164ec",
);
expect(Bun.randomUUIDv5("http://example.com/hello", urlNamespace.toUpperCase())).toBe(
"3bbcee75-cecc-5b56-8031-b6641c1ed1f1",
);
expect(Bun.randomUUIDv5("hello", "0f5abcd1-c194-47f3-905b-2df7263a084b".toUpperCase())).toBe(
"90123e1c-7512-523e-bb28-76fab9f2f73d",
);
});
test("v5 namespace string validation", () => {
expect(() => {
Bun.randomUUIDv5("hello.example.com", "zyxwvuts-rqpo-nmlk-jihg-fedcba000000");
}).toThrow();
expect(() => {
Bun.randomUUIDv5("hello.example.com", "invalid uuid value");
}).toThrow();
expect(Bun.randomUUIDv5("hello.example.com", "00000000-0000-0000-0000-000000000000")).toBeTypeOf("string");
});
test("v5 namespace buffer validation", () => {
expect(() => {
Bun.randomUUIDv5("hello.example.com", new Uint8Array(15));
}).toThrow();
expect(() => {
Bun.randomUUIDv5("hello.example.com", new Uint8Array(17));
}).toThrow();
expect(Bun.randomUUIDv5("hello.example.com", new Uint8Array(16).fill(0))).toBeTypeOf("string");
});
test("v5 fill buffer", () => {
const expectedUuid = Buffer.from([
0xfd, 0xda, 0x76, 0x5f, 0xfc, 0x57, 0x56, 0x04, 0xa2, 0x69, 0x52, 0xa7, 0xdf, 0x81, 0x64, 0xec,
]);
const result = Bun.randomUUIDv5("hello.example.com", dnsNamespace, "buffer");
expect(result.toString("hex")).toEqual(expectedUuid.toString("hex"));
});
test("v5 undefined/null", () => {
// @ts-expect-error testing invalid input
expect(() => Bun.randomUUIDv5()).toThrow();
// @ts-expect-error testing invalid input
expect(() => Bun.randomUUIDv5("hello")).toThrow();
// @ts-expect-error testing invalid input
expect(() => Bun.randomUUIDv5("hello.example.com", undefined)).toThrow();
// @ts-expect-error testing invalid input
expect(() => Bun.randomUUIDv5("hello.example.com", null)).toThrow();
});
test("RFC 4122 test vectors", () => {
// These should be deterministic
const uuid1 = Bun.randomUUIDv5("http://www.example.com/", dnsNamespace);
const uuid2 = Bun.randomUUIDv5("http://www.example.com/", urlNamespace);
// Both should be valid version 5 UUIDs
expect(uuid1).toEqual("b50f73c9-e407-5ea4-8540-70886e8aa2cd");
expect(uuid2).toEqual("fcde3c85-2270-590f-9e7c-ee003d65e0e2");
});
test("error cases", () => {
// Missing namespace
// @ts-expect-error
expect(() => Bun.randomUUIDv5()).toThrow();
// Missing name
// @ts-expect-error
expect(() => Bun.randomUUIDv5(dnsNamespace)).toThrow();
// Invalid namespace format
expect(() => Bun.randomUUIDv5("test", "invalid-uuid")).toThrow();
// Invalid encoding
// @ts-expect-error
expect(() => Bun.randomUUIDv5("test", dnsNamespace, "invalid")).toThrow();
// Namespace buffer wrong size
expect(() => Bun.randomUUIDv5("test", new Uint8Array(10))).toThrow();
});
test("long names", () => {
const longName = "a".repeat(10000);
const result = Bun.randomUUIDv5(longName, dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
});
test("unicode names", () => {
const unicodeName = "测试🌟";
const result = Bun.randomUUIDv5(unicodeName, dnsNamespace);
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
// Should be deterministic
const uuid2 = Bun.randomUUIDv5(unicodeName, dnsNamespace);
expect(result).toBe(uuid2);
expect(uuid.v5(unicodeName, dnsNamespace)).toBe(result);
});
test("variant bits are set correctly", () => {
const result = Bun.randomUUIDv5("test", dnsNamespace, "buffer");
// Check variant bits (bits 6-7 of clock_seq_hi_and_reserved should be 10)
const variantByte = result[8];
const variantBits = (variantByte & 0xc0) >> 6;
expect(variantBits).toBe(2); // Binary 10
expect(uuid.v5("test", dnsNamespace).replace(/-/g, "")).toEqual(result.toString("hex"));
});
test("url namespace", () => {
const result = Bun.randomUUIDv5("test", "6ba7b811-9dad-11d1-80b4-00c04fd430c8");
expect(result).toBeTypeOf("string");
expect(result).toEqual("da5b8893-d6ca-5c1c-9a9c-91f40a2a3649");
expect(uuid.v5("test", urlNamespace)).toEqual(result);
});
test("dns namespace", () => {
const result = Bun.randomUUIDv5("test", "dns");
expect(result).toBeTypeOf("string");
expect(result[14]).toBe("5");
expect(result).toEqual(uuid.v5("test", uuid.v5.DNS));
});
test("consistent across multiple calls", () => {
const results: string[] = [];
for (let i = 0; i < 100; i++) {
results.push(Bun.randomUUIDv5("consistency-test", dnsNamespace));
}
// All results should be identical
const first = results[0];
for (const result of results) {
expect(result).toBe(first);
}
});
});