/** 设置测试:全局 Webhook 读写与 key 语义、修改密码、Cloudflare OAuth 客户端配置 */ import { describe, expect, test } from "bun:test"; import { authedFetch, loginCookie, makeTestEnv } from "./helpers.ts"; import { CF_OAUTH_SETTING_KEYS, getOAuthClient } from "../console/src/services/cfapi.ts"; describe("设置(/api/settings)", () => { test("全局 Webhook:保存 -> 读取(仅回 last4);key 缺省保留、空格清除", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); // 保存(带 key) const save = await authedFetch(t, cookie, "http://localhost/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ webhook: { url: "https://hooks.example.com/a", headers: { "X-A": "1" }, key: "wmh_key_abcdefgh" } }), }); expect(save.status).toBe(200); let info = (await ( await authedFetch(t, cookie, "http://localhost/api/settings") ).json()) as { webhook: { url: string; has_key: boolean; key_last4: string; headers: Record } }; expect(info.webhook.url).toBe("https://hooks.example.com/a"); expect(info.webhook.has_key).toBe(true); expect(info.webhook.key_last4).toBe("efgh"); expect(info.webhook.headers).toEqual({ "X-A": "1" }); // 再次保存不带 key:key 保留 await authedFetch(t, cookie, "http://localhost/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ webhook: { url: "https://hooks.example.com/b" } }), }); info = (await (await authedFetch(t, cookie, "http://localhost/api/settings")).json()) as typeof info; expect(info.webhook.url).toBe("https://hooks.example.com/b"); expect(info.webhook.has_key).toBe(true); // key 传空格:清除 key await authedFetch(t, cookie, "http://localhost/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ webhook: { url: "https://hooks.example.com/b", key: " " } }), }); info = (await (await authedFetch(t, cookie, "http://localhost/api/settings")).json()) as typeof info; expect(info.webhook.has_key).toBe(false); // url 置空:停用全局 webhook await authedFetch(t, cookie, "http://localhost/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ webhook: { url: "" } }), }); info = (await (await authedFetch(t, cookie, "http://localhost/api/settings")).json()) as typeof info; expect(info.webhook.url).toBe(""); }); test("Cloudflare OAuth 客户端:设置页保存(secret 加密) -> 生效;env 兜底;清除", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const info = async () => (await (await authedFetch(t, cookie, "http://localhost/api/settings")).json()) as { cf_oauth: { client_id: string; has_secret: boolean; secret_last4: string; source: string }; redirect_uri: string; }; // 测试环境默认带 env 凭据:mode 走 env 兜底 let cur = await info(); expect(cur.cf_oauth.source).toBe("env"); expect(cur.cf_oauth.client_id).toBe("test-client-id"); expect(cur.redirect_uri).toBe("http://localhost/api/oauth/cloudflare/callback"); // 在设置里保存:source 切到 settings,接口只回 last4 const save = await authedFetch(t, cookie, "http://localhost/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cf_oauth: { client_id: "settings-client", secret: "settings-secret-9876" } }), }); expect(save.status).toBe(200); cur = await info(); expect(cur.cf_oauth.source).toBe("settings"); expect(cur.cf_oauth.client_id).toBe("settings-client"); expect(cur.cf_oauth.secret_last4).toBe("9876"); // secret 加密入库,接口与库中都不出现明文 const encRow = await t.env.DB.prepare("SELECT value FROM settings WHERE key = ?") .bind(CF_OAUTH_SETTING_KEYS.secretEnc) .first<{ value: string }>(); expect(encRow?.value).toBeTruthy(); expect(encRow!.value).not.toContain("settings-secret-9876"); // OAuth 授权 URL 用设置里的 client id const start = (await ( await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start") ).json()) as { url: string }; expect(start.url).toContain("client_id=settings-client"); // secret 缺省 = 保留;只改 client id 也生效 await authedFetch(t, cookie, "http://localhost/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cf_oauth: { client_id: "settings-client-2" } }), }); expect((await getOAuthClient(t.env))?.clientSecret).toBe("settings-secret-9876"); cur = await info(); expect(cur.cf_oauth.client_id).toBe("settings-client-2"); // DELETE 清除后台凭据:回落到 env 兜底 const del = await authedFetch(t, cookie, "http://localhost/api/settings/cf-oauth", { method: "DELETE" }); expect(del.status).toBe(200); cur = await info(); expect(cur.cf_oauth.source).toBe("env"); expect(cur.cf_oauth.client_id).toBe("test-client-id"); }); test("Cloudflare OAuth 客户端:仅填 Client ID(无既有 secret)被拒;secret 传空格清除", async () => { const t = await makeTestEnv(); // 清空 env 兜底,模拟"完全未配置" delete t.env.CF_OAUTH_CLIENT_ID; delete t.env.CF_OAUTH_CLIENT_SECRET; const cookie = await loginCookie(t); const noSecret = await authedFetch(t, cookie, "http://localhost/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cf_oauth: { client_id: "only-id" } }), }); expect(noSecret.status).toBe(400); // 接入接口仍是 503(没有任何降级) const start = await authedFetch(t, cookie, "http://localhost/api/oauth/cloudflare/start"); expect(start.status).toBe(503); // 先正常保存,再用空格清除 await authedFetch(t, cookie, "http://localhost/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cf_oauth: { client_id: "cid", secret: "s1" } }), }); let mode = (await (await authedFetch(t, cookie, "http://localhost/api/oauth/mode")).json()) as { mode: string }; expect(mode.mode).toBe("cloudflare"); await authedFetch(t, cookie, "http://localhost/api/settings", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cf_oauth: { client_id: "cid", secret: " " } }), }); mode = (await (await authedFetch(t, cookie, "http://localhost/api/oauth/mode")).json()) as { mode: string }; expect(mode.mode).toBe("unconfigured"); }); test("修改密码:原密码错误被拒;成功后新密码可登录", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const bad = await authedFetch(t, cookie, "http://localhost/api/settings/password", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ old_password: "wrong", new_password: "newpass123" }), }); expect(bad.status).toBe(400); const ok = await authedFetch(t, cookie, "http://localhost/api/settings/password", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ old_password: "admin", new_password: "newpass123" }), }); expect(ok.status).toBe(200); // 旧密码登录失败,新密码登录成功 const oldLogin = await t.app.fetch( new Request("http://localhost/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "admin", password: "admin" }), }), t.env, ); expect(oldLogin.status).toBe(401); const newLogin = await t.app.fetch( new Request("http://localhost/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: "admin", password: "newpass123" }), }), t.env, ); expect(newLogin.status).toBe(200); }); });