import { beforeEach, describe, expect, mock, test } from "bun:test"; import type { Viewer } from "~/lib/types"; class StubEntrypoint {} mock.module("cloudflare:workers", () => ({ env: { PUBLIC_URL: "https://forge.example.com", ACCOUNT_STATE: { idFromName: (name: string) => name, get: () => ({ destroy: async () => undefined }) }, }, waitUntil: () => undefined, WorkflowEntrypoint: StubEntrypoint, DurableObject: StubEntrypoint, WorkerEntrypoint: StubEntrypoint, })); const state = { orgExists: true, repos: [] as Array<{ id: string; name: string }>, retired: new Set(), statements: [] as string[], deletedRepos: [] as string[], removedRunners: [] as string[], }; const roles: Record = { "u-owner": "owner", "u-member": "member" }; const d1 = { first: async (sql: string, ...params: unknown[]) => { if (sql.includes("kind = 'org'")) return state.orgExists && params[0] === "acme" ? { id: "org-1", name: "acme" } : null; if (sql.includes("FROM accounts WHERE name = ?")) return null; if (sql.includes("retired_account_names")) return state.retired.has(String(params[0]).toLowerCase()) ? { ok: 1 } : null; if (sql.includes("FROM account_redirects WHERE old_name")) return null; if (sql.includes("SELECT role FROM org_members")) { const role = roles[String(params[1])]; return role ? { role } : null; } if (sql.includes("FROM org_members")) return roles[String(params[1])] ? { ok: 1 } : null; if (sql.includes("WHERE r.id = ?")) return { id: params[0], name: "repo", owner_name: "acme" }; return null; }, all: async (sql: string) => { if (sql.includes("FROM repos WHERE owner_id")) return state.repos; if (sql.includes("FROM runners WHERE owner_id")) return [{ id: "runner-1" }]; if (sql.includes("FROM sites WHERE owner_id")) return [{ id: "site-upload" }]; if (sql.includes("FROM account_redirects WHERE account_id")) return [{ old_name: "acme-old" }]; return []; }, run: async () => ({ meta: { changes: 1 } }), stmt: (sql: string, ...params: unknown[]) => ({ sql, params }), batch: async (statements: Array<{ sql: string; params: unknown[] }>) => { for (const statement of statements) { state.statements.push(statement.sql); if (statement.sql.includes("retired_account_names")) state.retired.add(String(statement.params[0]).toLowerCase()); if (statement.sql.startsWith("DELETE FROM accounts")) state.orgExists = false; } return []; }, placeholders: () => "", chunks: (items: T[]) => [items], MAX_IN_LIST: 90, }; mock.module("./db", () => d1); const scoped = { first: async () => null, all: d1.all, run: d1.run, batch: async () => [] }; mock.module("./scoped-db", () => ({ accountDb: () => scoped, repoDb: () => scoped, stmt: d1.stmt })); const actualSiteStore = await import("./sites/store"); mock.module("./sites/store", () => ({ ...actualSiteStore, sitesOwnedBy: async () => [{ id: "site-upload", repo_id: null, label: "upload", custom_domain: null }], siteDb: () => scoped, forgetSite: async () => undefined, })); mock.module("./repo-provisioning", () => ({ describeProvisioningError: () => "", markProvisioningFailed: async () => undefined, startProvisioning: async () => undefined, RepoProvisionWorkflow: StubEntrypoint, })); const actualRepoOps = await import("./repo-ops"); const actualRunners = await import("./ci/runner-registration"); mock.module("./repo-ops", () => ({ ...actualRepoOps, deleteRepository: async (repo: { id: string }) => { state.deletedRepos.push(repo.id); }, })); mock.module("./ci/runner-registration", () => ({ ...actualRunners, deleteRunnerRow: async (id: string) => { state.removedRunners.push(id); }, })); const disabledSites: string[] = []; const cancelledAddons: string[] = []; mock.module("./sites/site", () => ({ disableSite: async (site: { id: string }) => void disabledSites.push(site.id), })); mock.module("./billing/domain-addon", () => ({ cancelDomainAddon: async (id: string) => void cancelledAddons.push(id), })); const { deleteOrganization } = await import("./org-delete"); const { accountNameTaken } = await import("./accounts"); const { HttpError } = await import("./errors"); function viewer(id: string): Viewer { return { id, name: id, displayName: null, email: `${id}@example.com`, isAdmin: false, emailVerified: true }; } async function statusOf(promise: Promise): Promise { try { await promise; return 200; } catch (error) { if (error instanceof HttpError) return error.status; throw error; } } beforeEach(() => { state.orgExists = true; state.repos = []; state.retired = new Set(); state.statements = []; state.deletedRepos = []; state.removedRunners = []; }); describe("deleteOrganization", () => { test("a member is forbidden and a stranger sees 404", async () => { expect( await statusOf( deleteOrganization({ org: "acme", confirm: "acme", deleteRepos: false, viewer: viewer("u-member") }), ), ).toBe(403); expect( await statusOf(deleteOrganization({ org: "acme", confirm: "acme", deleteRepos: false, viewer: viewer("u-x") })), ).toBe(404); expect(state.statements).toEqual([]); }); test("the confirmation must match the org name", async () => { expect( await statusOf( deleteOrganization({ org: "acme", confirm: "Acme", deleteRepos: false, viewer: viewer("u-owner") }), ), ).toBe(400); expect(state.statements).toEqual([]); }); test("refuses while repositories remain, listing them", async () => { state.repos = [ { id: "r1", name: "api" }, { id: "r2", name: "web" }, ]; let message = ""; try { await deleteOrganization({ org: "acme", confirm: "acme", deleteRepos: false, viewer: viewer("u-owner") }); } catch (error) { if (error instanceof HttpError) message = `${error.status} ${error.message}`; } expect(message).toBe("409 Delete or transfer its repositories first: api, web"); expect(state.deletedRepos).toEqual([]); }); test("with delete_repos, deletes each repo via deleteRepository and retires the names", async () => { state.repos = [{ id: "r1", name: "api" }]; const result = await deleteOrganization({ org: "acme", confirm: "acme", deleteRepos: true, viewer: viewer("u-owner"), }); expect(result.deletedRepos).toEqual(["api"]); expect(state.deletedRepos).toEqual(["r1"]); expect(state.removedRunners).toEqual(["runner-1"]); expect(disabledSites).toEqual(["site-upload"]); expect(cancelledAddons).toEqual(["org-1"]); expect(state.statements.at(-1)).toBe("DELETE FROM accounts WHERE id = ?"); expect(state.statements).toContain("DELETE FROM integrations WHERE scope_type = 'org' AND scope_id = ?"); expect(await accountNameTaken("acme")).toBe(true); expect(await accountNameTaken("ACME-old")).toBe(true); expect(await accountNameTaken("someone-else")).toBe(false); }); });