import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; const env: Record = {}; mock.module("cloudflare:workers", () => ({ env, waitUntil: () => undefined })); mock.module("./limits", () => ({ rawRequestIp: () => "203.0.113.9" })); const { requireTurnstile, turnstileSiteKey } = await import("./turnstile"); const realFetch = globalThis.fetch; let calls: URLSearchParams[] = []; let reply: { status: number; body: unknown } = { status: 200, body: {} }; beforeEach(() => { Object.assign(env, { PUBLIC_URL: "https://gitfor.dev", TURNSTILE_SITEKEY: "0x4AAAAAAFUg9PmDIBH5nisz", TURNSTILE_SECRET: "secret", TURNSTILE_HOSTNAMES: "", }); calls = []; reply = { status: 200, body: { success: true, action: "signup", hostname: "gitfor.dev" } }; globalThis.fetch = (async (_url: string, init: RequestInit) => { calls.push(new URLSearchParams(String(init.body))); return new Response(JSON.stringify(reply.body), { status: reply.status }); }) as unknown as typeof fetch; }); afterEach(() => { globalThis.fetch = realFetch; }); describe("requireTurnstile", () => { test("accepts a token Siteverify vouches for, sending secret, token and client IP", async () => { await requireTurnstile("tok", "signup"); expect(calls).toHaveLength(1); expect(calls[0].get("secret")).toBe("secret"); expect(calls[0].get("response")).toBe("tok"); expect(calls[0].get("remoteip")).toBe("203.0.113.9"); }); test("rejects a missing, empty or oversized token without calling Siteverify", async () => { await expect(requireTurnstile(undefined, "signup")).rejects.toThrow("Complete the verification"); await expect(requireTurnstile("", "signup")).rejects.toThrow("Complete the verification"); await expect(requireTurnstile("x".repeat(2049), "signup")).rejects.toThrow("Complete the verification"); expect(calls).toHaveLength(0); }); test("rejects failures, wrong actions and other hostnames, including localhost in production", async () => { reply.body = { success: false, "error-codes": ["timeout-or-duplicate"] }; await expect(requireTurnstile("replayed", "signup")).rejects.toThrow("Verification failed"); reply.body = { success: true, action: "login", hostname: "gitfor.dev" }; await expect(requireTurnstile("tok", "signup")).rejects.toThrow("Verification failed"); reply.body = { success: true, action: "signup", hostname: "localhost" }; await expect(requireTurnstile("tok", "signup")).rejects.toThrow("Verification failed"); }); test("fails closed when Siteverify is down", async () => { reply = { status: 500, body: {} }; await expect(requireTurnstile("tok", "signup")).rejects.toThrow("Verification failed"); }); test("uses TURNSTILE_HOSTNAMES over PUBLIC_URL when set", async () => { env.TURNSTILE_HOSTNAMES = "www.gitfor.dev, gitfor.dev"; reply.body = { success: true, action: "login", hostname: "www.gitfor.dev" }; await requireTurnstile("tok", "login"); }); test("fails closed on a deployed forge without a secret, but stays off locally", async () => { env.TURNSTILE_SECRET = undefined; expect(turnstileSiteKey()).toBe("0x4AAAAAAFUg9PmDIBH5nisz"); await expect(requireTurnstile("tok", "signup")).rejects.toThrow("Verification is unavailable"); env.PUBLIC_URL = "http://localhost:3000"; expect(turnstileSiteKey()).toBeNull(); await requireTurnstile(undefined, "signup"); expect(calls).toHaveLength(0); }); test("is off entirely without a sitekey", async () => { env.TURNSTILE_SITEKEY = ""; expect(turnstileSiteKey()).toBeNull(); await requireTurnstile(undefined, "login"); expect(calls).toHaveLength(0); }); });