import { beforeEach, describe, expect, mock, test } from "bun:test"; import { scopedTestState } from "./scoped.test-helper"; const limited: string[] = []; const kv = new Map(); mock.module("cloudflare:workers", () => ({ env: { SECRETS_KEY: "k".repeat(40), PUBLIC_URL: "https://forge.test", SITES_DOMAIN: "sites.test", KV: { get: async (key: string) => kv.get(key) ?? null, put: async (key: string, value: string) => void kv.set(key, value), delete: async (key: string) => void kv.delete(key), }, }, waitUntil: () => undefined, })); const scoped = scopedTestState(); scoped.install(); mock.module("../limits", () => ({ enforceRateLimit: async (_binding: string, key: string) => void limited.push(key), numberVar: (_value: string | undefined, fallback: number) => fallback, })); mock.module("./deploy", () => ({ loadSiteById: async (id: string) => ({ id, label: `s-${id}` }), activateSiteDeployment: async () => "active", failSiteDeployment: async () => undefined, insertDeployment: async () => ({ id: "d" }), })); const { claimUrl, ipHash, reserveScratch } = await import("./scratch-core"); beforeEach(() => { kv.clear(); scoped.reset(); limited.length = 0; }); const DAILY_SCRATCH_CAP = 3; describe("scratch per-IP cap", () => { test("allows three a day per address, then refuses", async () => { for (let index = 0; index < DAILY_SCRATCH_CAP; index++) await reserveScratch("203.0.113.9"); await expect(reserveScratch("203.0.113.9")).rejects.toThrow("At most 3 scratch sites per day"); await reserveScratch("203.0.113.10"); }); test("counts every address of an IPv6 /64 against one cap, even when given raw addresses", async () => { await reserveScratch("2001:db8:1:2::1"); await reserveScratch("2001:db8:1:2:aaaa::2"); await reserveScratch("2001:0db8:0001:0002:ffff:ffff:ffff:ffff"); await expect(reserveScratch("2001:db8:1:2::99")).rejects.toThrow("At most 3"); await reserveScratch("2001:db8:1:3::1"); const bucket = await ipHash("2001:db8:1:2::/64"); expect([...kv].find(([key]) => key.includes(bucket))?.[1]).toBe("3"); expect(new Set(limited.slice(0, 3))).toEqual(new Set(["scratch:2001:db8:1:2::/64"])); }); }); describe("claimUrl", () => { test("keeps the claim token out of the query string", () => { const url = new URL(claimUrl("gitfor_claim_abc")); expect(url.search).toBe(""); expect(url.pathname).toBe("/sites/claim"); expect(url.hash).toBe("#token=gitfor_claim_abc"); }); });