refactor: v2.2.0 移除本地 sim 模式,Cloudflare OAuth 只走真实授权
- 删除 SimCFApi / WMAILHUB_SIM_CF / isRealOAuth / POST /api/oauth/cloudflare/sim/approve - 删除前端 OAuthSim 模拟授权页与 /oauth/sim/authorize 路由;账号页「添加账号」整页跳 Cloudflare - 迁移 0005 删除 cf_sim_resources 表 - 未配置 CF_OAUTH_CLIENT_ID/SECRET 时 start/callback 一律 503(不再降级出假账号假资源) - 修 OAuth 回调端点:跨站重定向不携带 SameSite=Strict 会话 cookie,改为由一次性 state 承担 CSRF 防护 - 测试改用 tests/cf-mock.ts 在 fetch 层拦截 Cloudflare API,断言真实 HTTP 调用(72/72 通过) - 本地 dev 无账号时只拉起 Console 并提示人工授权,不再自动造账号
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Cloudflare API 抓包桩(仅测试用):
|
||||
* 拦截 globalThis.fetch,对 dash.cloudflare.com / api.cloudflare.com 返回固定响应,
|
||||
* 并记录真实发出的 HTTP 调用。生产代码里没有任何模拟实现——接入链路跑的始终是
|
||||
* RealCFApi,测试只是在传输层把它要访问的 Cloudflare 换成本地桩。
|
||||
*/
|
||||
|
||||
/** # 成员变量: 记录到的 Cloudflare API 调用 **/
|
||||
export interface CfCall {
|
||||
/** 请求方法 **/
|
||||
method: string;
|
||||
/** 完整 URL **/
|
||||
url: string;
|
||||
/** 路径(去掉 base) **/
|
||||
path: string;
|
||||
/** Authorization 头(OAuth Bearer token) **/
|
||||
authorization: string | null;
|
||||
/** 请求体文本(URL 编码或 JSON);multipart 上传记 null **/
|
||||
body: string | null;
|
||||
}
|
||||
|
||||
/** # 成员变量: 桩的返回数据配置 **/
|
||||
export interface CfMockOptions {
|
||||
/** /accounts 返回的账户列表 **/
|
||||
accounts?: { id: string; name: string }[];
|
||||
/** /zones 返回的域名列表 **/
|
||||
zones?: { id: string; name: string; account_id: string; status: string }[];
|
||||
/** OAuth token 端点签发的 access token **/
|
||||
accessToken?: string;
|
||||
/** token 端点 HTTP 状态(测失败分支用) **/
|
||||
tokenStatus?: number;
|
||||
}
|
||||
|
||||
/** # 成员变量: Cloudflare API 桩句柄 **/
|
||||
export interface CfMock {
|
||||
/** 已记录的调用 **/
|
||||
calls: CfCall[];
|
||||
/** 恢复原始 fetch **/
|
||||
restore(): void;
|
||||
/** 是否发生过指定 method + 路径子串的调用 **/
|
||||
hit(method: string, pathPart: string): boolean;
|
||||
/** 取出第一条匹配调用的 body **/
|
||||
bodyOf(method: string, pathPart: string): string | null;
|
||||
}
|
||||
|
||||
const TOKEN_URL = "https://dash.cloudflare.com/oauth2/token";
|
||||
const API_BASE = "https://api.cloudflare.com/client/v4";
|
||||
|
||||
/** # 函数
|
||||
* 安装 Cloudflare API 桩:接管 globalThis.fetch,返回记录与恢复句柄
|
||||
* 参数: opts.accounts/zones 决定发现到的资源,tokenStatus 可模拟 token 兑换失败
|
||||
*/
|
||||
export function installCloudflareMock(opts: CfMockOptions = {}): CfMock {
|
||||
const accounts = opts.accounts ?? [{ id: "cf-acct-real-1", name: "Real Cloudflare Account" }];
|
||||
const zones = opts.zones ?? [
|
||||
{ id: "zone-example", name: "example.com", account_id: accounts[0]?.id ?? "", status: "active" },
|
||||
{ id: "zone-demo", name: "demo.com", account_id: accounts[0]?.id ?? "", status: "active" },
|
||||
];
|
||||
const accessToken = opts.accessToken ?? "cf-oauth-access-token-xyz";
|
||||
const calls: CfCall[] = [];
|
||||
const original = globalThis.fetch;
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
||||
const headers = new Headers(
|
||||
init?.headers ?? (input instanceof Request ? input.headers : undefined),
|
||||
);
|
||||
const rawBody = init?.body;
|
||||
const body =
|
||||
typeof rawBody === "string"
|
||||
? rawBody
|
||||
: rawBody instanceof URLSearchParams
|
||||
? rawBody.toString()
|
||||
: null;
|
||||
|
||||
calls.push({
|
||||
method,
|
||||
url,
|
||||
path: url.startsWith(API_BASE) ? url.slice(API_BASE.length) : url,
|
||||
authorization: headers.get("authorization"),
|
||||
// multipart 上传(Email Worker 脚本)读不出文本,只记方法与路径
|
||||
body: rawBody instanceof FormData ? null : body,
|
||||
});
|
||||
|
||||
const json = (result: unknown, status = 200) =>
|
||||
new Response(JSON.stringify({ success: true, errors: [], messages: [], result }), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (url === TOKEN_URL) {
|
||||
if ((opts.tokenStatus ?? 200) !== 200) {
|
||||
return new Response(JSON.stringify({ error: "invalid_grant" }), { status: opts.tokenStatus });
|
||||
}
|
||||
// OAuth token 端点是裸 JSON(不是 Cloudflare API 的 {success,result} 信封)
|
||||
return new Response(JSON.stringify({ access_token: accessToken, expires_in: 3600, token_type: "bearer" }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (url.startsWith(`${API_BASE}/accounts`)) return json(accounts);
|
||||
if (url.startsWith(`${API_BASE}/zones`)) {
|
||||
if (url.includes("/email/routing/rules/catch_all")) return json({ id: "catch-all-rule" });
|
||||
if (url.includes("/email/routing")) return json({ enabled: true });
|
||||
return json(zones);
|
||||
}
|
||||
if (url.includes("/workers/scripts/")) return json({ id: url.split("/").pop() });
|
||||
return json(null, 404);
|
||||
}) as typeof globalThis.fetch;
|
||||
|
||||
return {
|
||||
calls,
|
||||
restore: () => {
|
||||
globalThis.fetch = original;
|
||||
},
|
||||
hit: (method, pathPart) =>
|
||||
calls.some((c) => c.method === method.toUpperCase() && c.path.includes(pathPart)),
|
||||
bodyOf: (method, pathPart) =>
|
||||
calls.find((c) => c.method === method.toUpperCase() && c.path.includes(pathPart))?.body ?? null,
|
||||
};
|
||||
}
|
||||
+3
-1
@@ -75,7 +75,9 @@ export async function makeTestEnv(): Promise<TestEnv> {
|
||||
DB: new LocalD1(db),
|
||||
R2: new LocalR2(mkdtempSync(path.join(tmpdir(), "wmh-r2-"))),
|
||||
PUBLIC_BASE_URL: "http://localhost",
|
||||
WMAILHUB_SIM_CF: "1",
|
||||
// 真实 Cloudflare OAuth 客户端(测试里由 cf-mock 在传输层拦截,不外发)
|
||||
CF_OAUTH_CLIENT_ID: "test-client-id",
|
||||
CF_OAUTH_CLIENT_SECRET: "test-client-secret",
|
||||
WMAILHUB_ALLOW_PRIVATE_WEBHOOKS: "1",
|
||||
};
|
||||
const app = createApp();
|
||||
|
||||
+161
-76
@@ -1,31 +1,56 @@
|
||||
/** Cloudflare OAuth(本地 sim 模式)测试:Connect Account 自动接入、域名同步、state CSRF、setup Email Workers、Worker 凭据 */
|
||||
import { describe, expect, test } from "bun:test";
|
||||
/**
|
||||
* Cloudflare OAuth(真实授权链路)测试:
|
||||
* Connect Cloudflare Account 自动接入、域名同步、state CSRF、setup Email Workers、Worker 凭据。
|
||||
* Cloudflare 侧由 tests/cf-mock.ts 在 fetch 层打桩,断言的是真实发出的 HTTP 调用。
|
||||
*/
|
||||
import { afterEach, describe, expect, test } from "bun:test";
|
||||
import { authedFetch, loginCookie, makeTestEnv } from "./helpers.ts";
|
||||
import { installCloudflareMock, type CfMock } from "./cf-mock";
|
||||
|
||||
describe("Cloudflare OAuth(sim 模式,Connect Cloudflare Account)", () => {
|
||||
test("start 自动建账号;approve 完成自动接入并同步域名;setup 配置 Email Workers", async () => {
|
||||
/** # 成员变量: 当前 Cloudflare 桩(afterEach 恢复原始 fetch) **/
|
||||
let mock: CfMock | null = null;
|
||||
afterEach(() => {
|
||||
mock?.restore();
|
||||
mock = null;
|
||||
});
|
||||
|
||||
/** # 函数
|
||||
* 走完 start -> callback,返回账号 id 与桩(不携带会话 cookie 调 callback,模拟跨站重定向)
|
||||
*/
|
||||
async function connectAccount(
|
||||
t: Awaited<ReturnType<typeof makeTestEnv>>,
|
||||
cookie: string,
|
||||
): Promise<{ accountId: string; cf: CfMock; resp: Response }> {
|
||||
const cf = installCloudflareMock();
|
||||
mock = cf;
|
||||
const start = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start")
|
||||
).json()) as { url: string; state: string };
|
||||
expect(start.url).toContain("https://dash.cloudflare.com/oauth2/auth");
|
||||
expect(start.url).toContain("client_id=test-client-id");
|
||||
|
||||
// 授权回调:浏览器从 Cloudflare 跨站跳回,不带 SameSite=Strict 会话 cookie
|
||||
const resp = await t.app.fetch(
|
||||
new Request(`http://localhost/api/oauth/cloudflare/callback?code=auth-code-1&state=${start.state}`),
|
||||
t.env,
|
||||
);
|
||||
const location = resp.headers.get("Location") ?? "";
|
||||
const accountId = new URLSearchParams(location.split("?")[1] ?? "").get("provision") ?? "";
|
||||
return { accountId, cf, resp };
|
||||
}
|
||||
|
||||
describe("Cloudflare OAuth(真实授权,Connect Cloudflare Account)", () => {
|
||||
test("start 自动建账号;callback 完成自动接入并同步域名;setup 配置 Email Workers", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
|
||||
// Connect Cloudflare Account:start 自动创建账号记录
|
||||
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; account_id: string };
|
||||
expect(approve.ok).toBe(true);
|
||||
const { accountId, cf, resp } = await connectAccount(t, cookie);
|
||||
expect(resp.status).toBe(302);
|
||||
expect(resp.headers.get("Location")).toContain("ok=1");
|
||||
|
||||
// 接入结果:发现 + 域名同步 + Email Worker 部署 + Worker 凭据签发
|
||||
const detail = (await (
|
||||
await authedFetch(t, cookie, `http://localhost/api/accounts/${approve.account_id}`)
|
||||
await authedFetch(t, cookie, `http://localhost/api/accounts/${accountId}`)
|
||||
).json()) as {
|
||||
account: { status: string; cloudflare_account_id: string | null; name: string; worker_id: string | null };
|
||||
steps: Record<string, { status: string }>;
|
||||
@@ -33,44 +58,54 @@ describe("Cloudflare OAuth(sim 模式,Connect Cloudflare Account)", () => {
|
||||
credential: { worker_id: string; secret_last4: string; status: string } | null;
|
||||
};
|
||||
expect(detail.account.status).toBe("active"); // 接入完成(无心跳概念)
|
||||
expect(detail.account.cloudflare_account_id).toBe("sim-account-0001");
|
||||
expect(detail.account.name).toBe("WmailHub Sim Account"); // 自动命名替换为 CF 账户名
|
||||
expect(detail.account.cloudflare_account_id).toBe("cf-acct-real-1");
|
||||
expect(detail.account.name).toBe("Real Cloudflare Account"); // 自动命名替换为 CF 账户名
|
||||
for (const key of ["oauth", "discover", "domains", "worker"]) {
|
||||
expect(detail.steps[key].status).toBe("done");
|
||||
}
|
||||
expect(detail.account.worker_id).toContain("wmailhub-email-"); // Worker credential 已签发(库中不存明文 secret)
|
||||
expect(detail.account.worker_id).toContain("wmailhub-email-"); // Worker credential 已签发
|
||||
expect(detail.credential).not.toBeNull();
|
||||
expect(detail.credential!.worker_id).toBe(detail.account.worker_id!);
|
||||
const credRow = await t.env.DB.prepare(
|
||||
"SELECT secret_hash, secret_enc, secret_last4 FROM worker_credentials WHERE cloudflare_account_id = ?",
|
||||
)
|
||||
.bind(approve.account_id)
|
||||
.bind(accountId)
|
||||
.first<{ secret_hash: string; secret_enc: string; secret_last4: string }>();
|
||||
expect(credRow?.secret_enc).toBeTruthy();
|
||||
expect(credRow!.secret_last4.length).toBe(4);
|
||||
|
||||
// 真实发出的 Cloudflare 调用:换 token -> 账户发现 -> 域名发现 -> 上传 Email Worker 脚本
|
||||
expect(cf.hit("POST", "/oauth2/token")).toBe(true);
|
||||
expect(cf.hit("GET", "/accounts")).toBe(true);
|
||||
expect(cf.hit("GET", "/zones?account.id=")).toBe(true);
|
||||
expect(cf.hit("PUT", "/workers/scripts/")).toBe(true);
|
||||
// token 兑换请求带真实 code 与 client 凭据
|
||||
const tokenBody = cf.bodyOf("POST", "/oauth2/token") ?? "";
|
||||
expect(tokenBody).toContain("grant_type=authorization_code");
|
||||
expect(tokenBody).toContain("code=auth-code-1");
|
||||
expect(tokenBody).toContain("client_secret=test-client-secret");
|
||||
|
||||
// OAuth 只同步域名,Email Routing 按域名手动触发
|
||||
expect(detail.domains.map((d) => d.name)).toEqual(
|
||||
expect.arrayContaining(["example.test", "demo.test", "mailbox.dev"]),
|
||||
);
|
||||
expect(detail.domains.map((d) => d.name)).toEqual(expect.arrayContaining(["example.com", "demo.com"]));
|
||||
const domList = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/domains")
|
||||
).json()) as { items: { id: string; name: string; status: string; account_id: string | null }[] };
|
||||
// 种子环境自带一个 cfacct-1 下的 example.test(active);OAuth 同步出的新域名均为 pending
|
||||
const synced = domList.items.filter((d) => ["demo.test", "mailbox.dev"].includes(d.name));
|
||||
const synced = domList.items.filter((d) => ["example.com", "demo.com"].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");
|
||||
expect(mb.results.map((r) => r.address)).toContain("*@example.com");
|
||||
|
||||
// setup:为同步出的 example.test 配置 Email Workers(Worker 幂等重部署 + Routing + catch-all 规则)
|
||||
const example = domList.items.find((d) => d.name === "example.test" && d.status === "pending")!;
|
||||
// setup:为同步出的 example.com 配置 Email Workers(Worker 幂等重部署 + Routing + catch-all 规则)
|
||||
const example = domList.items.find((d) => d.name === "example.com" && 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);
|
||||
expect(cf.hit("POST", "/zones/zone-example/email/routing")).toBe(true);
|
||||
expect(cf.hit("PUT", "/zones/zone-example/email/routing/rules/catch_all")).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 }[] };
|
||||
@@ -78,72 +113,107 @@ describe("Cloudflare OAuth(sim 模式,Connect Cloudflare Account)", () => {
|
||||
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);
|
||||
expect(conn!.encrypted_access_token).not.toContain("cf-oauth-access-token-xyz");
|
||||
});
|
||||
|
||||
test("state 一次性:重放 approve 被拒(OAuth CSRF 防护)", async () => {
|
||||
test("state 一次性:重放授权回调被拒(OAuth CSRF 防护)", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
mock = installCloudflareMock();
|
||||
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);
|
||||
const first = await t.app.fetch(
|
||||
new Request(`http://localhost/api/oauth/cloudflare/callback?code=c1&state=${state}`),
|
||||
t.env,
|
||||
);
|
||||
expect(first.status).toBe(302);
|
||||
// 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 }),
|
||||
});
|
||||
const second = await t.app.fetch(
|
||||
new Request(`http://localhost/api/oauth/cloudflare/callback?code=c1&state=${state}`),
|
||||
t.env,
|
||||
);
|
||||
expect(second.status).toBe(400);
|
||||
});
|
||||
|
||||
test("伪造/过期 state 被拒;真实 callback 端点同样拒绝", async () => {
|
||||
test("伪造 state 被拒;授权回调不依赖会话 cookie(跨站重定向场景)", 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);
|
||||
});
|
||||
mock = installCloudflareMock();
|
||||
// 无 Cookie:SameSite=Strict 下浏览器从 Cloudflare 跳回时就是这种请求
|
||||
const forged = await t.app.fetch(
|
||||
new Request("http://localhost/api/oauth/cloudflare/callback?code=x&state=cfstate_forged"),
|
||||
t.env,
|
||||
);
|
||||
expect(forged.status).toBe(400);
|
||||
|
||||
test("rotate:Worker credential 轮换,secret 仅本次可见且已配置域名自动重部署", async () => {
|
||||
const t = await makeTestEnv();
|
||||
// 合法 state + 无 Cookie 依然放行(CSRF 由一次性 state 承担)
|
||||
const cookie = await loginCookie(t);
|
||||
// 自动接入一个账号(approve 走完 worker 步骤:部署 + 签发凭据)
|
||||
await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start");
|
||||
const start = (await (
|
||||
const { state } = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start")
|
||||
).json()) as { state: string };
|
||||
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; account_id: string };
|
||||
const accountId = approve.account_id;
|
||||
const okResp = await t.app.fetch(
|
||||
new Request(`http://localhost/api/oauth/cloudflare/callback?code=c2&state=${state}`),
|
||||
t.env,
|
||||
);
|
||||
expect(okResp.status).toBe(302);
|
||||
});
|
||||
|
||||
test("未配置 Cloudflare OAuth 时:mode=unconfigured,start/callback 返回 503(无降级通道)", async () => {
|
||||
const t = await makeTestEnv();
|
||||
delete t.env.CF_OAUTH_CLIENT_ID;
|
||||
delete t.env.CF_OAUTH_CLIENT_SECRET;
|
||||
const cookie = await loginCookie(t);
|
||||
|
||||
const mode = (await (await authedFetch(t, cookie, "http://localhost/api/oauth/mode")).json()) as {
|
||||
mode: string;
|
||||
authorize_url_base: string | null;
|
||||
};
|
||||
expect(mode.mode).toBe("unconfigured");
|
||||
expect(mode.authorize_url_base).toBeNull();
|
||||
|
||||
const start = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start");
|
||||
expect(start.status).toBe(503);
|
||||
// 不会凭空建账号
|
||||
const rows = await t.env.DB.prepare("SELECT id FROM cloudflare_accounts WHERE status = 'pending_oauth'").all();
|
||||
expect(rows.results.length).toBe(0);
|
||||
|
||||
const cb = await t.app.fetch(
|
||||
new Request("http://localhost/api/oauth/cloudflare/callback?code=x&state=y"),
|
||||
t.env,
|
||||
);
|
||||
expect(cb.status).toBe(503);
|
||||
});
|
||||
|
||||
test("token 兑换失败时不落库连接,账号停留在待授权", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
mock = installCloudflareMock({ tokenStatus: 400 });
|
||||
const { state } = (await (
|
||||
await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start")
|
||||
).json()) as { state: string };
|
||||
|
||||
const resp = await t.app.fetch(
|
||||
new Request(`http://localhost/api/oauth/cloudflare/callback?code=bad&state=${state}`),
|
||||
t.env,
|
||||
);
|
||||
expect(resp.status).toBe(500);
|
||||
// 兑换失败发生在接入之前:不写 oauth_connections,账号仍是待授权
|
||||
const conns = await t.env.DB.prepare("SELECT id FROM oauth_connections").all();
|
||||
expect(conns.results.length).toBe(0);
|
||||
const acct = await t.env.DB.prepare("SELECT status FROM cloudflare_accounts WHERE status = 'pending_oauth'").all();
|
||||
expect(acct.results.length).toBe(1);
|
||||
});
|
||||
|
||||
test("rotate:Worker credential 轮换,secret 仅本次可见且旧 credential 撤销", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
const { accountId } = await connectAccount(t, cookie);
|
||||
|
||||
const rotated = (await (
|
||||
await authedFetch(t, cookie, `http://localhost/api/accounts/${accountId}/rotate`, { method: "POST" })
|
||||
@@ -161,6 +231,21 @@ describe("Cloudflare OAuth(sim 模式,Connect Cloudflare Account)", () => {
|
||||
expect(creds.results.filter((r) => r.status === "revoked").length).toBe(1);
|
||||
});
|
||||
|
||||
test("sim 通道已彻底移除:无 sim 授权端点、无 cf_sim_resources 表", async () => {
|
||||
const t = await makeTestEnv();
|
||||
const cookie = await loginCookie(t);
|
||||
const table = await t.db
|
||||
.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'cf_sim_resources'")
|
||||
.get();
|
||||
expect(table).toBeNull();
|
||||
const resp = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/sim/approve", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ state: "cfstate_x" }),
|
||||
});
|
||||
expect(resp.status).not.toBe(200);
|
||||
});
|
||||
|
||||
test("旧 Node 协议接口已移除(/api/internal/enroll 不再可用)", async () => {
|
||||
const t = await makeTestEnv();
|
||||
// 该路径已无路由;管理面中间件按未授权拒绝,不会返回 enrollment 结果
|
||||
|
||||
Reference in New Issue
Block a user