import { beforeEach, describe, expect, mock, test } from "bun:test"; import { fakeD1 } from "../metering/fake-d1.test-helper"; import { memoryScopes } from "../scoped-db/testing"; import { CUSTOM_DOMAIN_LIMIT } from "~/lib/pricing"; let d1 = fakeD1(); const fakeEnv: Record = { get DB() { return d1.binding; }, METERING_PLANS: "", PUBLIC_URL: "https://forge.test", }; mock.module("cloudflare:workers", () => ({ env: fakeEnv, waitUntil: () => undefined })); let scopes = memoryScopes(); mock.module("../scoped-db", () => ({ repoDb: (id: string) => scopes.repoDb(id), accountDb: (id: string) => scopes.accountDb(id), stmt: scopes.module.stmt, })); /** A site in its scope (the repository's object, or `site:` when it has no repository), with its locator. */ function seedSite(id: string, domain: string, repoId: string | null = null) { const scope = repoId ?? `site:${id}`; scopes.raw("repo", scope).run( `INSERT INTO sites (id, owner_id, repo_id, label, source, custom_domain, created_at, updated_at) VALUES (?, 'u1', ?, ?, 'upload', ?, 0, 0)`, [id, repoId, id, domain], ); d1.db.run("INSERT INTO scope_locators (kind, key, scope_id) VALUES ('site', ?, ?)", [id, scope]); } const cancelled: string[] = []; mock.module("./config", () => ({ stripeEnabled: () => Boolean(fakeEnv.STRIPE_SECRET_KEY), stripe: () => ({ subscriptions: { retrieve: async (id: string) => ({ id, status: id === "sub_gone" ? "canceled" : "active" }), cancel: async (id: string) => { if (id === "sub_missing") throw new Error("resource_missing"); cancelled.push(id); }, }, subscriptionItems: { update: async () => ({}) }, }), billingNotConfigured: () => new Error("not configured"), proPriceVerified: async () => false, verifiedPacks: async () => [], checkoutDisabled: () => false, liveMode: () => false, stripeModeError: () => null, })); const notices: string[] = []; mock.module("../notify/deliver", () => ({ notifyUsers: async (input: { accountId: string }) => void notices.push(input.accountId), })); const { assertCanAddCustomDomain, customDomainCount, DOMAIN_GRACE_MS, lapsedDomainAccounts, saveDomainAddon } = await import("./domain-addon"); const { setCustomDomainsGrant, startUnpaidGraceClocks } = await import("./domain-addon"); const { HttpError } = await import("../errors"); async function refusal(replacing = false): Promise { try { await assertCanAddCustomDomain("u1", replacing); return null; } catch (error) { return error instanceof HttpError ? error.status : -1; } } beforeEach(() => { d1 = fakeD1(); scopes = memoryScopes(); delete fakeEnv.STRIPE_SECRET_KEY; delete fakeEnv.STRIPE_PRICE_CUSTOM_DOMAIN; fakeEnv.METERING_PLANS = ""; d1.db.run(` INSERT INTO accounts (id, name, kind) VALUES ('u1', 'alice', 'user'); INSERT INTO repos VALUES ('r1', 'u1', 'blog'); `); seedSite("s1", "a.example.com", "r1"); seedSite("s2", "b.example.com"); }); describe("custom domain add-on", () => { test("counts domains across Sites", async () => { expect(await customDomainCount("u1")).toBe(2); }); test("without billing the plan flag decides, and an admin grant overrides it", async () => { expect(await refusal()).toBeNull(); fakeEnv.METERING_PLANS = JSON.stringify({ free: { customDomains: false } }); expect(await refusal()).toBe(402); await setCustomDomainsGrant("u1", true); expect(await refusal()).toBeNull(); }); test("with billing only an active add-on allows new domains", async () => { fakeEnv.STRIPE_SECRET_KEY = "sk_test_unit"; fakeEnv.STRIPE_PRICE_CUSTOM_DOMAIN = "price_domain"; expect(await refusal()).toBe(402); const fields = { subscriptionId: "sub_d", itemId: "si_d", status: "active", quantity: 2 }; await saveDomainAddon("u1", fields, 100); expect(await refusal()).toBeNull(); await saveDomainAddon("u1", { ...fields, status: "canceled" }, 200); expect(await refusal()).toBe(402); expect(await lapsedDomainAccounts(Date.now())).toEqual([]); expect(await lapsedDomainAccounts(Date.now() + DOMAIN_GRACE_MS + 1)).toEqual(["u1"]); }); test("a lapse only releases accounts that still hold domains", async () => { fakeEnv.STRIPE_SECRET_KEY = "sk_test_unit"; fakeEnv.STRIPE_PRICE_CUSTOM_DOMAIN = "price_domain"; await saveDomainAddon("u1", { subscriptionId: "sub_d", itemId: "si_d", status: "canceled", quantity: 2 }, 1); for (const scope of ["r1", "site:s2"]) scopes.raw("repo", scope).run("UPDATE sites SET custom_domain = NULL"); expect(await lapsedDomainAccounts(Date.now() + DOMAIN_GRACE_MS + 1)).toEqual([]); }); test("domains held without an add-on or grant start the grace clock once, and owners are told", async () => { fakeEnv.STRIPE_SECRET_KEY = "sk_test_unit"; fakeEnv.STRIPE_PRICE_CUSTOM_DOMAIN = "price_domain"; notices.length = 0; expect(await startUnpaidGraceClocks(1000)).toEqual(["u1"]); expect(await startUnpaidGraceClocks(2000)).toEqual([]); expect(notices).toEqual(["u1"]); expect(await lapsedDomainAccounts(1000 + DOMAIN_GRACE_MS + 1)).toEqual(["u1"]); await setCustomDomainsGrant("u1", true); expect(await startUnpaidGraceClocks(3000)).toEqual([]); await setCustomDomainsGrant("u1", false); expect(await startUnpaidGraceClocks(4000)).toEqual(["u1"]); }); test("a second live add-on subscription is cancelled instead of billing twice", async () => { fakeEnv.STRIPE_SECRET_KEY = "sk_test_unit"; fakeEnv.STRIPE_PRICE_CUSTOM_DOMAIN = "price_domain"; await saveDomainAddon("u1", { subscriptionId: "sub_a", itemId: "si_a", status: "active", quantity: 2 }, 100); await saveDomainAddon("u1", { subscriptionId: "sub_b", itemId: "si_b", status: "active", quantity: 1 }, 200); expect(cancelled).toEqual(["sub_b"]); await saveDomainAddon("u1", { subscriptionId: "sub_gone", itemId: "si_g", status: "active", quantity: 1 }, 300); await saveDomainAddon("u1", { subscriptionId: "sub_missing", itemId: "si_m", status: "active", quantity: 1 }, 400); expect(cancelled).toEqual(["sub_b"]); }); test("a grant toggle never wipes a lapse caused by a subscription ending", async () => { fakeEnv.STRIPE_SECRET_KEY = "sk_test_unit"; fakeEnv.STRIPE_PRICE_CUSTOM_DOMAIN = "price_domain"; await saveDomainAddon("u1", { subscriptionId: "sub_d", itemId: "si_d", status: "canceled", quantity: 2 }, 1); await setCustomDomainsGrant("u1", true); await setCustomDomainsGrant("u1", false); expect(await lapsedDomainAccounts(Date.now() + DOMAIN_GRACE_MS + 1)).toEqual(["u1"]); }); test("caps domains per account, but a site may still swap its own", async () => { for (let index = 0; index < CUSTOM_DOMAIN_LIMIT; index += 1) { seedSite(`x${index}`, `${index}.example.com`); } expect(await refusal(false)).toBe(400); expect(await refusal(true)).toBeNull(); }); });