/** 批量操作测试(列表复选框):四个资源的批量删除与子资源级联 */ import { describe, expect, test } from "bun:test"; import { authedFetch, loginCookie, makeTestEnv, signedIngestRequest, TEST_MAIL } from "./helpers.ts"; import { nowIso } from "../console/src/util.ts"; /** # 函数 * 调批量删除接口 */ async function batchDelete(t: Awaited>, cookie: string, path: string, ids: unknown) { const resp = await authedFetch(t, cookie, `http://localhost/api${path}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids }), }); return { status: resp.status, body: (await resp.json()) as { deleted: number; mailboxes?: number; emails?: number; failed: { id: string; reason: string }[] } }; } /** # 函数 * 往某个邮箱灌一封邮件(走真实两阶段 ingest),返回邮件 id */ async function ingestInto(t: Awaited>, recipient: string): Promise { const resp = await t.app.fetch( await signedIngestRequest(t, new TextEncoder().encode(TEST_MAIL), { recipient }), t.env, ); if (resp.status !== 202) throw new Error(`ingest failed: ${resp.status}`); return ((await resp.json()) as { id: string }).id; } describe("批量删除(POST /*/batch-delete)", () => { test("批量删除邮件:连同 R2 原文/附件/delivery 一并清理;不存在的 id 计入 failed", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const a = await ingestInto(t, "hello@example.test"); const b = await ingestInto(t, "hello@example.test"); const res = await batchDelete(t, cookie, "/emails/batch-delete", [a, b, "msg_not_exist"]); expect(res.status).toBe(200); expect(res.body.deleted).toBe(2); expect(res.body.failed.length).toBe(1); const left = await t.env.DB.prepare("SELECT id FROM emails").all<{ id: string }>(); expect(left.results.length).toBe(0); const atts = await t.env.DB.prepare("SELECT id FROM email_attachments").all<{ id: string }>(); expect(atts.results.length).toBe(0); // R2 对象已删除 const obj = await t.env.R2.head("emails/2026/09/30/xx/msg_1/raw.eml").catch(() => null); expect(obj).toBeNull(); }); test("批量删除邮箱:级联删除其下邮件", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const emailId = await ingestInto(t, "hello@example.test"); const res = await batchDelete(t, cookie, "/mailboxes/batch-delete", [t.mailboxId, "mbx_missing"]); expect(res.body.deleted).toBe(1); expect(res.body.emails).toBe(1); expect(res.body.failed.length).toBe(1); const mail = await t.env.DB.prepare("SELECT id FROM mailboxes WHERE id = ?").bind(t.mailboxId).first(); expect(mail).toBeNull(); const em = await t.env.DB.prepare("SELECT id FROM emails WHERE id = ?").bind(emailId).first(); expect(em).toBeNull(); }); test("批量删除域名:级联删除 catch-all 邮箱与其邮件", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const emailId = await ingestInto(t, "hello@example.test"); const res = await batchDelete(t, cookie, "/domains/batch-delete", [t.domainId, "dom_missing"]); expect(res.body.deleted).toBe(1); expect(res.body.mailboxes).toBe(1); expect(res.body.emails).toBe(1); expect(res.body.failed.length).toBe(1); const dom = await t.env.DB.prepare("SELECT id FROM domains WHERE id = ?").bind(t.domainId).first(); expect(dom).toBeNull(); const mb = await t.env.DB.prepare("SELECT id FROM mailboxes").all<{ id: string }>(); expect(mb.results.length).toBe(0); const em = await t.env.DB.prepare("SELECT id FROM emails WHERE id = ?").bind(emailId).first(); expect(em).toBeNull(); }); test("批量删除账号:软删 + 撤销 Worker 凭据;域名数据保留", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const res = await batchDelete(t, cookie, "/accounts/batch-delete", [t.accountId, "acct_missing"]); expect(res.body.deleted).toBe(1); expect(res.body.failed.length).toBe(1); const acct = await t.env.DB.prepare("SELECT status FROM cloudflare_accounts WHERE id = ?") .bind(t.accountId) .first<{ status: string }>(); expect(acct?.status).toBe("revoked"); const cred = await t.env.DB.prepare("SELECT status FROM worker_credentials WHERE cloudflare_account_id = ?") .bind(t.accountId) .first<{ status: string }>(); expect(cred?.status).toBe("revoked"); // 域名仍在 const dom = await t.env.DB.prepare("SELECT id FROM domains WHERE id = ?").bind(t.domainId).first(); expect(dom).not.toBeNull(); }); test("边界:空数组 / 非字符串 / 重复 id 均安全处理;需要登录", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const empty = await batchDelete(t, cookie, "/emails/batch-delete", []); expect(empty.status).toBe(200); expect(empty.body.deleted).toBe(0); // 非字符串与重复 id 被过滤/去重 const emailId = await ingestInto(t, "hello@example.test"); const mixed = await batchDelete(t, cookie, "/emails/batch-delete", [emailId, emailId, 123, null, ""]); expect(mixed.body.deleted).toBe(1); // 未登录 401 const anon = await t.app.fetch( new Request("http://localhost/api/emails/batch-delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids: ["x"] }), }), t.env, ); expect(anon.status).toBe(401); }); test("单条删除语义不变:有邮箱的域名仍返回 409", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const resp = await authedFetch(t, cookie, `http://localhost/api/domains/${t.domainId}`, { method: "DELETE" }); expect(resp.status).toBe(409); const row = await t.env.DB.prepare("SELECT id FROM domains WHERE id = ?").bind(t.domainId).first(); expect(row).not.toBeNull(); // 时间戳工具仍在使用(避免 noUnusedLocals 之外的隐式问题) expect(typeof nowIso()).toBe("string"); }); });