import { expect, test } from "@playwright/test"; /** * `crypto.randomUUID` only exists on secure origins. The isolated NAS preview * is plain HTTP on a LAN/Tailnet address, where calling it directly threw * `crypto.randomUUID is not a function` at render time and replaced the whole * session-review route with the error boundary. * * These tests run against the Vite dev server so the module can be imported * directly, and pin that idempotency keys stay available without a secure * context. */ const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; test.describe("insecure-context idempotency keys", () => { test("randomUuid keeps working when crypto.randomUUID is unavailable", async ({ page, }) => { await page.addInitScript(() => { // `randomUUID` lives on Crypto.prototype, so shadow it with an own // undefined property instead of deleting it. Object.defineProperty(globalThis.crypto, "randomUUID", { value: undefined, configurable: true, }); }); await page.goto("/"); const probe = await page.evaluate(async () => { const module = await import("/src/lib/uuid.ts"); return { randomUuidPresent: typeof (globalThis.crypto as { randomUUID?: unknown }).randomUUID, getRandomValuesPresent: typeof globalThis.crypto.getRandomValues, values: [module.randomUuid(), module.randomUuid(), module.randomUuid()], }; }); expect(probe.randomUuidPresent).toBe("undefined"); expect(probe.getRandomValuesPresent).toBe("function"); for (const value of probe.values) { expect(value).toMatch(UUID_V4); } expect(new Set(probe.values).size).toBe(3); }); test("randomUuid degrades once more when Web Crypto is missing entirely", async ({ page, }) => { await page.addInitScript(() => { // `randomUUID` lives on Crypto.prototype, so shadow it with an own // undefined property instead of deleting it. Object.defineProperty(globalThis.crypto, "randomUUID", { value: undefined, configurable: true, }); Object.defineProperty(globalThis.crypto, "getRandomValues", { value: undefined, configurable: true, }); }); await page.goto("/"); const probe = await page.evaluate(async () => { const module = await import("/src/lib/uuid.ts"); const api = globalThis.crypto as { randomUUID?: unknown; getRandomValues?: unknown; }; return { randomUuidPresent: typeof api.randomUUID, getRandomValuesPresent: typeof api.getRandomValues, values: [module.randomUuid(), module.randomUuid()], }; }); expect(probe.randomUuidPresent).toBe("undefined"); expect(probe.getRandomValuesPresent).toBe("undefined"); const values = probe.values; for (const value of values) { expect(value).toMatch(UUID_V4); } expect(new Set(values).size).toBe(2); }); test("no product source calls crypto.randomUUID without the fallback", async () => { const { readFileSync, readdirSync, statSync } = await import("node:fs"); const path = await import("node:path"); const root = path.join(process.cwd(), "src"); const offenders: string[] = []; const walk = (dir: string) => { for (const entry of readdirSync(dir)) { const full = path.join(dir, entry); if (statSync(full).isDirectory()) { walk(full); continue; } if (!/\.(ts|tsx)$/.test(entry)) continue; if (full.endsWith(path.join("lib", "uuid.ts"))) continue; if (readFileSync(full, "utf8").includes("crypto.randomUUID")) { offenders.push(path.relative(root, full)); } } }; walk(root); expect(offenders).toEqual([]); }); });