/** Cloudflare OAuth(本地 sim 模式)测试:state CSRF、自动安装幂等、连接存储 */ import { describe, expect, test } from "bun:test"; import { authedFetch, loginCookie, makeTestEnv } from "./helpers.ts"; describe("Cloudflare OAuth(sim 模式)", () => { test("start 生成 state;sim approve 完成自动安装(OAuth→identity→discover→worker→routing→domains→mailboxes)", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); // Add Node const created = (await ( await authedFetch(t, cookie, "http://localhost/api/nodes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "Sim Node", ingress_url: "http://127.0.0.1:18788/ingress/email" }), }) ).json()) as { node_id: string; enrollment_token: string }; expect(created.enrollment_token.startsWith("wenr_")).toBe(true); // OAuth start const start = (await ( await authedFetch(t, cookie, `http://localhost/api/oauth/cloudflare/start?node_id=${created.node_id}`) ).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 }; expect(approve.ok).toBe(true); // 安装结果 const detail = (await ( await authedFetch(t, cookie, `http://localhost/api/nodes/${created.node_id}`) ).json()) as { node: { status: string; cloudflare_account_id: string | null }; steps: Record; domains: { name: string }[]; }; expect(detail.node.status).toBe("offline"); // 等待 Node 心跳上线 expect(detail.node.cloudflare_account_id).toBe("sim-account-0001"); for (const key of ["oauth", "identity", "discover", "email_worker", "email_routing", "domains", "mailboxes"]) { expect(detail.steps[key].status).toBe("done"); } expect(detail.domains.map((d) => d.name)).toContain("example.test"); // catch-all mailbox 已创建 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"); // sim 资源记录(Email Worker / Routing rule) 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("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 一次性:重放回调 state 被拒(OAuth CSRF 防护)", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const created = (await ( await authedFetch(t, cookie, "http://localhost/api/nodes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "n" }), }) ).json()) as { node_id: string }; const { state } = (await ( await authedFetch(t, cookie, `http://localhost/api/oauth/cloudflare/start?node_id=${created.node_id}`) ).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); const created = (await ( await authedFetch(t, cookie, "http://localhost/api/nodes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: "n2" }), }) ).json()) as { node_id: string; enrollment_token: string }; 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: created.enrollment_token }), }), t.env, ) ).json()) as { node_id: string; credential_secret: string; ingress_secret: string }; expect(enroll.node_id).toBe(created.node_id); 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(created.node_id) .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); }); });