/** authorize 决策矩阵测试(docs/11 §3/§7):accept/reject/temporary_error 语义与拒绝码 */ import { describe, expect, test } from "bun:test"; import { handleEmailEvent, default as workerMod, type EmailMessage } from "../worker/src/index.ts"; import type { WorkerEnv } from "../worker/src/env.ts"; import { makeTestEnv, signedAuthorizeRequest, TEST_MAIL } from "./helpers.ts"; import { sha256Hex } from "../shared/src/index.ts"; import { encryptSecret } from "../console/src/db.ts"; /** # 成员变量: 决策结果(decision/code) **/ interface Decision { decision: "accept" | "reject" | "temporary_error"; code?: string; reason?: string; } /** # 函数 * 调 authorize 接口并解析决策 */ async function authorize( t: Awaited>, recipient: string, sender = "sender@example.net", ): Promise<{ status: number; body: Decision }> { const resp = await t.app.fetch(await signedAuthorizeRequest(t, recipient, sender), t.env); return { status: resp.status, body: (await resp.json()) as Decision }; } describe("POST /internal/email/authorize 决策矩阵", () => { test("存在的 catch-all 域名 -> accept(catch-all 下任意未知地址也接收)", async () => { const t = await makeTestEnv(); const r = await authorize(t, "hello@example.test"); expect(r.status).toBe(200); expect(r.body.decision).toBe("accept"); const unknown = await authorize(t, "anyone@example.test"); expect(unknown.body.decision).toBe("accept"); }); test("不存在邮箱且无有效 catch-all -> reject MAILBOX_NOT_FOUND", async () => { const t = await makeTestEnv(); // 删除 catch-all,只留精确地址(禁用的 catch-all 属于 MAILBOX_DISABLED 语义) await t.env.DB.prepare("DELETE FROM mailboxes WHERE id = ?").bind(t.mailboxId).run(); 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_exact2', 'dom_test1', 'only@example.test', 'active', 0, ?, ?)", ) .bind(now, now) .run(); const r = await authorize(t, "unknown@example.test"); expect(r.status).toBe(200); expect(r.body.decision).toBe("reject"); expect(r.body.code).toBe("MAILBOX_NOT_FOUND"); expect(r.body.reason).toBe("Mailbox does not exist"); }); test("禁用邮箱 -> reject MAILBOX_DISABLED", async () => { const t = await makeTestEnv(); 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_disabled', 'dom_test1', 'off@example.test', 'disabled', 0, ?, ?)", ) .bind(now, now) .run(); const r = await authorize(t, "off@example.test"); expect(r.body.decision).toBe("reject"); expect(r.body.code).toBe("MAILBOX_DISABLED"); }); test("禁用 catch-all 邮箱 -> reject MAILBOX_DISABLED(不再回退)", async () => { const t = await makeTestEnv(); await t.env.DB.prepare("UPDATE mailboxes SET status = 'disabled' WHERE id = ?").bind(t.mailboxId).run(); const r = await authorize(t, "anyone@example.test"); expect(r.body.decision).toBe("reject"); expect(r.body.code).toBe("MAILBOX_DISABLED"); }); test("pending 域名 -> reject DOMAIN_DISABLED(Email Workers 未启用)", async () => { const t = await makeTestEnv(); await t.env.DB.prepare("UPDATE domains SET status = 'pending' WHERE id = ?").bind(t.domainId).run(); const r = await authorize(t, "hello@example.test"); expect(r.body.decision).toBe("reject"); expect(r.body.code).toBe("DOMAIN_DISABLED"); }); test("不存在的域名 -> reject DOMAIN_NOT_FOUND", async () => { const t = await makeTestEnv(); const r = await authorize(t, "hello@no-such-domain.test"); expect(r.body.decision).toBe("reject"); expect(r.body.code).toBe("DOMAIN_NOT_FOUND"); }); test("Worker 归属校验:credential 所属账号与收件域名必须一致", async () => { const t = await makeTestEnv(); const now = new Date().toISOString(); // 另一个账号持有同名域名 example.test;默认 credential 不应跨账号匹配 await t.env.DB.prepare( "INSERT INTO cloudflare_accounts (id, name, cloudflare_account_id, worker_id, status, provision_steps, created_at, updated_at) VALUES ('acct_other', 'other-account', 'cfacct-2', 'wmailhub-email-acct-other', 'active', '{}', ?, ?)", ) .bind(now, now) .run(); // 默认 credential 收件人域名不存在于本账号(先移除种子域名) -> DOMAIN_NOT_FOUND await t.env.DB.prepare("DELETE FROM mailboxes WHERE id = ?").bind(t.mailboxId).run(); await t.env.DB.prepare("DELETE FROM domains WHERE id = ?").bind(t.domainId).run(); const r = await authorize(t, "hello@example.test"); expect(r.body.decision).toBe("reject"); expect(r.body.code).toBe("DOMAIN_NOT_FOUND"); // other 账号的 credential + 它自己的域名 -> accept await t.env.DB.prepare( "INSERT INTO domains (id, cloudflare_account_id, account_id, name, status, created_at, updated_at) VALUES ('dom_other', 'cfacct-2', 'acct_other', 'example.test', 'active', ?, ?)", ) .bind(now, now) .run(); await t.env.DB.prepare( "INSERT INTO mailboxes (id, domain_id, address, status, is_catch_all, created_at, updated_at) VALUES ('mbx_other', 'dom_other', '*@example.test', 'active', 1, ?, ?)", ) .bind(now, now) .run(); const otherSecret = "whs_other_secret_for_signing"; await t.env.DB.prepare( "INSERT INTO worker_credentials (id, cloudflare_account_id, worker_id, secret_hash, secret_enc, secret_last4, status, created_at) VALUES ('wcred_other', 'acct_other', 'wmailhub-email-acct-other', ?, ?, ?, 'active', ?)", ) .bind(await sha256Hex(otherSecret), await encryptSecret(t.env, otherSecret), otherSecret.slice(-4), now) .run(); const resp = await t.app.fetch( await signedAuthorizeRequest(t, "hello@example.test", "a@b.c", { workerId: "wmailhub-email-acct-other", secret: otherSecret, }), t.env, ); expect(resp.status).toBe(200); expect(((await resp.json()) as Decision).decision).toBe("accept"); }); test("畸形收件人 -> reject MAILBOX_NOT_FOUND(不抛 500)", async () => { const t = await makeTestEnv(); for (const bad of ["no-at-sign", "@example.test", "a@"]) { const r = await authorize(t, bad); expect(r.status).toBe(200); expect(r.body.decision).toBe("reject"); } }); }); /** # 成员变量: fake console 行为配置 **/ interface FakeConsoleOptions { /** authorize 阶段响应(缺省 accept) **/ authorize?: (req: Request) => Response | Promise; /** ingest 阶段响应(缺省 202 成功) **/ ingest?: (req: Request) => Response | Promise; /** 模拟 authorize 阶段网络异常(不可达/超时) **/ authorizeThrows?: string; } /** # 函数 * 构造 WorkerEnv:fetchImpl 按路径分发到 fake Console */ function makeWorkerEnv(opts: FakeConsoleOptions): WorkerEnv { const fetchImpl = (async (input: RequestInfo | URL, init?: RequestInit) => { const req = input instanceof Request ? input : new Request(input, init); const pathname = new URL(req.url).pathname; if (pathname === "/internal/email/authorize") { if (opts.authorizeThrows) throw new Error(opts.authorizeThrows); return await (opts.authorize ?? (() => Response.json({ decision: "accept" })))(req); } if (pathname === "/internal/email/ingest") { return await (opts.ingest ?? (() => Response.json({ ok: true, id: "msg_x", mailbox_id: "mbx_x" }, { status: 202 })))(req); } throw new Error(`unexpected fetch ${pathname}`); }) as typeof fetch; return { CONSOLE_URL: "http://fake-console", WORKER_ID: "wmailhub-email-test", WORKER_SECRET: "whs_fake_secret", fetchImpl, }; } describe("Email Worker 两阶段行为(临时错误语义)", () => { const MAIL_BYTES = new TextEncoder().encode(TEST_MAIL); function message(): EmailMessage { return { from: "sender@example.net", to: "test@example.com", raw: MAIL_BYTES }; } test("accept -> ingest 原文提交成功(信封头 + message/rfc822)", async () => { let ingestBody: Uint8Array | null = null; const wenv = makeWorkerEnv({ ingest: async (req) => { expect(req.headers.get("X-WmailHub-Recipient")).toBe("test@example.com"); expect(req.headers.get("X-WmailHub-Sender")).toBe("sender@example.net"); expect(req.headers.get("Content-Type")).toBe("message/rfc822"); ingestBody = new Uint8Array(await req.arrayBuffer()); return Response.json({ ok: true, id: "msg_1", mailbox_id: "mbx_1" }, { status: 202 }); }, }); const result = await handleEmailEvent(wenv, message()); expect(result.action).toBe("accepted"); expect(result.email_id).toBe("msg_1"); // 原始 MIME 原样提交(未转 JSON 字符串) expect(new TextDecoder().decode(ingestBody ?? new Uint8Array())).toContain("plain body"); }); test("reject -> 不调用 ingest,返回拒绝码(setReject 由 email() 执行)", async () => { const wenv = makeWorkerEnv({ authorize: () => Response.json({ decision: "reject", code: "MAILBOX_NOT_FOUND", reason: "Mailbox does not exist" }), }); const result = await handleEmailEvent(wenv, message()); expect(result.action).toBe("rejected"); expect(result.code).toBe("MAILBOX_NOT_FOUND"); }); test("Console 5xx -> temporary_error(不能变成 mailbox not found)", async () => { const wenv = makeWorkerEnv({ authorize: () => new Response("boom", { status: 500 }), }); const result = await handleEmailEvent(wenv, message()); expect(result.action).toBe("temporary_error"); expect(result.reason).toContain("500"); }); test("Console 网络不可达/超时 -> temporary_error", async () => { const wenv = makeWorkerEnv({ authorizeThrows: "connect timeout" }); const result = await handleEmailEvent(wenv, message()); expect(result.action).toBe("temporary_error"); expect(result.reason).toContain("timeout"); }); test("ingest 失败 -> 不伪造业务拒绝(temporary_error)", async () => { const wenv = makeWorkerEnv({ ingest: () => new Response("storage down", { status: 500 }), }); const result = await handleEmailEvent(wenv, message()); expect(result.action).toBe("temporary_error"); expect(result.reason).toContain("ingest"); }); test("默认 email() handler:reject 调 setReject;temporary 抛异常", async () => { // reject -> setReject(SMTP 永久拒绝) const wenv1 = makeWorkerEnv({ authorize: () => Response.json({ decision: "reject", code: "DOMAIN_DISABLED", reason: "Receiving for this domain is not enabled", }), }); const reasons: string[] = []; await workerMod.email({ ...message(), setReject: (r) => reasons.push(r) }, wenv1); expect(reasons).toEqual(["Receiving for this domain is not enabled"]); // temporary_error -> 抛异常交 Cloudflare 重试 const wenv2 = makeWorkerEnv({ authorizeThrows: "console down" }); await expect(workerMod.email(message(), wenv2)).rejects.toThrow("console down"); }); });