/** * 删除账号/域名的 Cloudflare 侧清理测试(v2.6.0): * - 删域名:关闭该域名的 Email Routing(与开启同理只有 POST 真生效) * - 删账号:逐域名关闭 Email Routing + 删除账号级 Email Worker 脚本 * - Cloudflare 清理失败只产生告警,不阻断本地删除 * Cloudflare 侧由 tests/cf-mock.ts 在 fetch 层打桩,断言真实发出的 HTTP 调用。 */ import { afterEach, describe, expect, test } from "bun:test"; import { encryptConnectionToken } from "../console/src/db.ts"; import { authedFetch, loginCookie, makeTestEnv } from "./helpers.ts"; import { installCloudflareMock, type CfMock } from "./cf-mock"; /** # 成员变量: 当前 Cloudflare 桩(afterEach 恢复原始 fetch) **/ let mock: CfMock | null = null; afterEach(() => { mock?.restore(); mock = null; }); /** # 函数 * 把播种账号 acct_test1 / 域名 dom_test1 配成「已装 Email Workers」: * 补 zone 绑定 + 一条未过期的 Cloudflare 凭据(decryptAccountToken 优先按账号取)。 * 播种的 catch-all 邮箱一并删掉,否则单域名删除会被「有邮箱」409 挡住。 */ async function makeConfigured(t: Awaited>) { const now = new Date().toISOString(); await t.env.DB.prepare("UPDATE domains SET zone_id = 'zone-example' WHERE id = 'dom_test1'").run(); await t.env.DB.prepare("DELETE FROM mailboxes WHERE domain_id = 'dom_test1'").run(); await t.env.DB.prepare( "INSERT INTO oauth_connections (id, provider, account_id, encrypted_access_token, expires_at, created_at, updated_at) VALUES ('conn_tear', 'cloudflare', ?, ?, NULL, ?, ?)", ).bind(t.accountId, await encryptConnectionToken(t.env, "cf-oauth-access-token-xyz"), now, now).run(); } describe("删除账号/域名的 Cloudflare 侧清理", () => { test("删除已配置域名:POST 关闭 Email Routing,本地行删除,无告警", async () => { const t = await makeTestEnv(); await makeConfigured(t); const cf = installCloudflareMock(); mock = cf; const cookie = await loginCookie(t); const resp = await authedFetch(t, cookie, "http://localhost/api/domains/dom_test1", { method: "DELETE" }); expect(resp.status).toBe(200); const data = (await resp.json()) as { ok: boolean; warnings: string[] }; expect(data.ok).toBe(true); expect(data.warnings).toEqual([]); expect(cf.hit("POST", "/zones/zone-example/email/routing/disable")).toBe(true); expect(await t.env.DB.prepare("SELECT id FROM domains WHERE id = 'dom_test1'").first()).toBeNull(); const log = await t.env.DB.prepare("SELECT id FROM operation_logs WHERE action = 'domain.delete'").first(); expect(log).not.toBeNull(); }); test("未配置 Email Workers 的域名删除:不产生 Cloudflare 调用", async () => { const t = await makeTestEnv(); await makeConfigured(t); await t.env.DB.prepare("UPDATE domains SET status = 'pending', zone_id = NULL WHERE id = 'dom_test1'").run(); const cf = installCloudflareMock(); mock = cf; const cookie = await loginCookie(t); const resp = await authedFetch(t, cookie, "http://localhost/api/domains/dom_test1", { method: "DELETE" }); expect(resp.status).toBe(200); expect(cf.hit("POST", "/email/routing/disable")).toBe(false); }); test("Cloudflare 关闭 Routing 失败:仍完成本地删除,响应带告警并记日志", async () => { const t = await makeTestEnv(); await makeConfigured(t); const cf = installCloudflareMock({ teardownError: "disable" }); mock = cf; const cookie = await loginCookie(t); const resp = await authedFetch(t, cookie, "http://localhost/api/domains/dom_test1", { method: "DELETE" }); expect(resp.status).toBe(200); const data = (await resp.json()) as { ok: boolean; warnings: string[] }; expect(data.ok).toBe(true); expect(data.warnings.length).toBe(1); expect(data.warnings[0]).toContain("Email Routing"); expect(await t.env.DB.prepare("SELECT id FROM domains WHERE id = 'dom_test1'").first()).toBeNull(); const log = await t.env.DB .prepare("SELECT detail FROM operation_logs WHERE action = 'domain.delete'") .first<{ detail: string }>(); expect(log?.detail).toContain("Email Routing"); }); test("批量删除域名:逐域名清理 Routing,级联删邮箱,无告警", async () => { const t = await makeTestEnv(); await makeConfigured(t); // makeConfigured 删掉了播种邮箱,这里补一个以验证级联删除 const now = new Date().toISOString(); await t.env.DB.prepare( "INSERT INTO mailboxes (id, domain_id, address, status, is_catch_all, created_at, updated_at) VALUES ('mbx_tear', 'dom_test1', 'a@example.test', 'active', 0, ?, ?)", ).bind(now, now).run(); const cf = installCloudflareMock(); mock = cf; const cookie = await loginCookie(t); const resp = await authedFetch(t, cookie, "http://localhost/api/domains/batch-delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: ["dom_test1"] }), }); const data = (await resp.json()) as { deleted: number; mailboxes: number; warnings: string[] }; expect(data.deleted).toBe(1); expect(data.mailboxes).toBe(1); expect(data.warnings).toEqual([]); expect(cf.hit("POST", "/zones/zone-example/email/routing/disable")).toBe(true); }); test("删除账号:关闭各域名 Email Routing + 删除 Email Worker 脚本", async () => { const t = await makeTestEnv(); await makeConfigured(t); const cf = installCloudflareMock(); mock = cf; const cookie = await loginCookie(t); const resp = await authedFetch(t, cookie, `http://localhost/api/accounts/${t.accountId}`, { method: "DELETE" }); expect(resp.status).toBe(200); const data = (await resp.json()) as { ok: boolean; warnings: string[] }; expect(data.ok).toBe(true); expect(data.warnings).toEqual([]); expect(cf.hit("POST", "/zones/zone-example/email/routing/disable")).toBe(true); expect(cf.hit("DELETE", "/workers/scripts/wmailhub-email-acct-test1")).toBe(true); // 软删生效:列表不再出现,操作日志带清理摘要 const list = (await (await authedFetch(t, cookie, "http://localhost/api/accounts")).json()) as { total: number }; expect(list.total).toBe(0); const log = await t.env.DB .prepare("SELECT detail FROM operation_logs WHERE action = 'account.delete'") .first<{ detail: string }>(); expect(log?.detail).toContain("wmailhub-email-acct-test1"); }); test("删除账号但 Cloudflare 删 Worker 失败:告警不阻断,Routing 照常关闭", async () => { const t = await makeTestEnv(); await makeConfigured(t); const cf = installCloudflareMock({ teardownError: "worker" }); mock = cf; const cookie = await loginCookie(t); const resp = await authedFetch(t, cookie, `http://localhost/api/accounts/${t.accountId}`, { method: "DELETE" }); expect(resp.status).toBe(200); const data = (await resp.json()) as { ok: boolean; warnings: string[] }; expect(data.ok).toBe(true); expect(data.warnings.length).toBe(1); expect(data.warnings[0]).toContain("Email Worker"); expect(cf.hit("POST", "/zones/zone-example/email/routing/disable")).toBe(true); expect(cf.hit("DELETE", "/workers/scripts/wmailhub-email-acct-test1")).toBe(true); }); });