/** Shared Stripe webhook test harness: env, mocks, signed event posting and plan lookups. */ import { mock } from "bun:test"; import Stripe from "stripe"; import { fakeD1 } from "../metering/fake-d1.test-helper"; import { memoryScopes } from "../scoped-db/testing"; export const state = { d1: fakeD1(), scopes: memoryScopes(), sequence: 0 }; export const kv = new Map(); export const SECRET = "whsec_test_secret"; export const fakeEnv: Record = { get DB() { return state.d1.binding; }, 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), }, METERING_PLANS: "", PUBLIC_URL: "https://forge.test", STRIPE_SECRET_KEY: "sk_test_unit", STRIPE_WEBHOOK_SECRET: SECRET, STRIPE_PRICE_PRO: "price_pro", STRIPE_TOPUP_PRICES: JSON.stringify({ compute_10h: "price_c", tokens_10m: { price: "price_t" }, tokens_50m: "nope", unknown_pack: "price_x", }), }; mock.module("cloudflare:workers", () => ({ env: fakeEnv, waitUntil: () => undefined })); mock.module("../scoped-db", () => ({ repoDb: (id: string) => state.scopes.repoDb(id), accountDb: (id: string) => state.scopes.accountDb(id), stmt: state.scopes.module.stmt, })); /** The account's own SQLite (AccountState), where billing and credit rows live. */ export function accountSql(accountId: string) { return state.scopes.raw("account", accountId); } export const disabled: { accountId: string; reason: string }[] = []; export const adminNotices: string[] = []; mock.module("../account-disable-actions", () => ({ disableAccount: async (input: { accountId: string; reason: string }) => { disabled.push({ accountId: input.accountId, reason: input.reason }); return true; }, notifyAdminsAboutAccount: async (_accountId: string, title: string) => void adminNotices.push(title), })); export const { handleStripeWebhook, processStripeEvent } = await import("./webhook"); export const { topupPacks } = await import("./config"); export const { creditBalance } = await import("../metering/credits"); const signer = new Stripe("sk_test_unit"); const crypto = Stripe.createSubtleCryptoProvider(); export function event( type: string, object: Record, created = 1_700_000_000 + state.sequence, ): Stripe.Event { state.sequence += 1; return { id: `evt_${state.sequence}`, object: "event", type, created, livemode: false, data: { object }, } as unknown as Stripe.Event; } export async function post(payload: Stripe.Event, secret = SECRET): Promise { const body = JSON.stringify(payload); const signature = await signer.webhooks.generateTestHeaderStringAsync({ payload: body, secret, cryptoProvider: crypto, }); return handleStripeWebhook( new Request("https://forge.test/stripe/webhook", { method: "POST", headers: { "stripe-signature": signature }, body, }), ); } export function plan(accountId: string): string | null { const row = accountSql(accountId).query("SELECT plan FROM account_billing WHERE account_id = ?").get(accountId) as { plan: string; } | null; return row?.plan ?? null; } export function subscription(status: string, extra: Record = {}) { return { id: "sub_1", object: "subscription", customer: "cus_1", status, cancel_at_period_end: false, metadata: { account_id: "o1" }, items: { data: [{ price: { id: "price_pro" }, current_period_end: 1_800_000_000 }] }, ...extra, }; } /** Fresh database, KV and captured side effects; call from each test file's beforeEach. */ export function resetWebhookHarness(): void { state.d1 = fakeD1(); state.scopes = memoryScopes(); kv.clear(); disabled.length = 0; adminNotices.length = 0; state.d1.db.exec("INSERT INTO accounts (id, name, kind) VALUES ('u1', 'alice', 'user'), ('o1', 'acme', 'org');"); }