/** Cloudflare OAuth(本地 sim 模式)测试:自动建账号、域名同步、state CSRF、setup Email Workers、凭据存储 */ import { describe, expect, test } from "bun:test"; import { authedFetch, loginCookie, makeTestEnv } from "./helpers.ts"; describe("Cloudflare OAuth(sim 模式)", () => { test("start 免 node_id 自动建账号;approve 同步全部域名;setup 配置 Email Workers", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); // 添加账号:OAuth start 不带 node_id(服务端自动创建账号) const start = (await ( await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start") ).json()) as { url: string; state: string }; expect(start.url).toContain("/oauth/sim/authorize?state="); // sim approve(等价于用户在 Cloudflare 授权页点击同意) const approve = (await ( await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/sim/approve", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ state: start.state }), }) ).json()) as { ok: boolean; error?: string; node_id: string }; expect(approve.ok).toBe(true); // 安装结果:发现 + 域名同步完成 const detail = (await ( await authedFetch(t, cookie, `http://localhost/api/accounts/${approve.node_id}`) ).json()) as { account: { status: string; cloudflare_account_id: string | null; name: string }; steps: Record; domains: { name: string }[]; }; expect(detail.account.status).toBe("offline"); // 等待 Node 心跳上线 expect(detail.account.cloudflare_account_id).toBe("sim-account-0001"); expect(detail.account.name).toBe("WmailHub Sim Account"); // 自动命名替换为 CF 账户名 for (const key of ["oauth", "identity", "discover", "domains"]) { expect(detail.steps[key].status).toBe("done"); } // OAuth 只同步域名,不自动部署 Email Worker(按域名手动触发) expect(detail.domains.map((d) => d.name)).toEqual( expect.arrayContaining(["example.test", "demo.test", "mailbox.dev"]), ); const domList = (await ( await authedFetch(t, cookie, "http://localhost/api/domains") ).json()) as { items: { id: string; name: string; status: string }[] }; // 种子环境自带一个 acct-1 下的 example.test(active);OAuth 同步出的新域名均为 pending const synced = domList.items.filter((d) => ["demo.test", "mailbox.dev"].includes(d.name)); expect(synced.length).toBe(2); for (const d of synced) expect(d.status).toBe("pending"); // catch-all 邮箱已随同步创建 const mb = await t.env.DB.prepare("SELECT address FROM mailboxes WHERE is_catch_all = 1").all<{ address: string }>(); expect(mb.results.map((r) => r.address)).toContain("*@example.test"); // setup:为同步出的 example.test 配置 Email Workers(Worker + Routing + catch-all 规则) const example = domList.items.find((d) => d.name === "example.test" && d.status === "pending")!; const setup = (await ( await authedFetch(t, cookie, `http://localhost/api/domains/${example.id}/setup`, { method: "POST" }) ).json()) as { ok: boolean; error?: string }; expect(setup.ok).toBe(true); const after = (await ( await authedFetch(t, cookie, "http://localhost/api/domains") ).json()) as { items: { id: string; name: string; status: string; worker_id: string | null }[] }; const exampleAfter = after.items.find((d) => d.id === example.id)!; expect(exampleAfter.status).toBe("active"); expect(exampleAfter.worker_id).toContain("wmailhub-email-"); // sim 资源记录(Email Worker / Routing rule 只在 setup 后出现) const sim = await t.env.DB.prepare("SELECT kind FROM cf_sim_resources").all<{ kind: string }>(); const kinds = sim.results.map((r) => r.kind); expect(kinds).toContain("zone"); expect(kinds).toContain("email_worker"); expect(kinds).toContain("routing_rule"); // OAuth token 已加密入库(不含明文 token) const conn = await t.env.DB.prepare("SELECT encrypted_access_token FROM oauth_connections").first<{ encrypted_access_token: string; }>(); expect(conn?.encrypted_access_token).toBeTruthy(); expect(conn!.encrypted_access_token.startsWith("sim_")).toBe(false); }); test("state 一次性:重放 approve 被拒(OAuth CSRF 防护)", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const { state } = (await ( await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start") ).json()) as { state: string }; const first = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/sim/approve", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ state }), }); expect(first.status).toBe(200); // state 已消费,重放被拒 const second = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/sim/approve", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ state }), }); expect(second.status).toBe(400); }); test("伪造/过期 state 被拒;真实 callback 端点同样拒绝", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const bad = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/sim/approve", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ state: "cfstate_forged" }), }); expect(bad.status).toBe(400); const cb = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/callback?code=x&state=forged"); expect(cb.status).toBe(400); }); test("enrollment:token 换取身份,凭证明文仅返回一次且库中只存摘要/密文", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); // 自动创建账号后,用 rotate 取得 enrollment token await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start"); const accounts = (await ( await authedFetch(t, cookie, "http://localhost/api/accounts") ).json()) as { items: { id: string }[] }; const accountId = accounts.items[0].id; const rotated = (await ( await authedFetch(t, cookie, `http://localhost/api/accounts/${accountId}/rotate`, { method: "POST" }) ).json()) as { enrollment_token: string }; expect(rotated.enrollment_token.startsWith("wenr_")).toBe(true); const enroll = (await ( await t.app.fetch( new Request("http://localhost/api/internal/enroll", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enrollment_token: rotated.enrollment_token }), }), t.env, ) ).json()) as { node_id: string; credential_secret: string; ingress_secret: string }; expect(enroll.node_id).toBe(accountId); expect(enroll.credential_secret.startsWith("wns_")).toBe(true); // 库中不存明文 secret const cred = await t.env.DB.prepare( "SELECT secret_hash, secret_enc FROM node_credentials WHERE node_id = ? AND status='active'", ) .bind(accountId) .first<{ secret_hash: string; secret_enc: string }>(); expect(cred?.secret_hash).not.toBe(enroll.credential_secret); expect(cred?.secret_enc).not.toContain(enroll.credential_secret); // 无效 token 被拒 const badEnroll = await t.app.fetch( new Request("http://localhost/api/internal/enroll", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enrollment_token: "wenr_invalid" }), }), t.env, ); expect(badEnroll.status).toBe(401); }); });