- 设置新增「Cloudflare 接入」tab:Client ID / Client Secret(Secret AES-GCM 加密入库,
只回显末 4 位),并展示可复制的授权回调地址;settings 优先、env 变量兜底,
保存后立即生效无需重新部署;DELETE /api/settings/cf-oauth 清除
- 四个资源列表(账号/域名/邮箱/收件箱)加复选框批量选择与批量删除
- 新增 POST /api/{accounts,domains,mailboxes,emails}/batch-delete:
账号软删+撤销凭据;域名/邮箱级联删除其下邮件;邮件连 R2 原文与附件一并清理
- 抽出 deleteEmailById / deleteEmailsByMailbox / revokeAccount 供单条与批量共用
- 点击复选框不再触发行点击(避免误开详情抽屉)
- 新增 tests/batch.test.ts(6 例)与 settings OAuth 凭据用例(2 例);ui-smoke 扩到 17 步
189 lines
8.2 KiB
TypeScript
189 lines
8.2 KiB
TypeScript
/** 设置测试:全局 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<string, string> } };
|
|
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);
|
|
});
|
|
});
|