- 账号化:节点更名账号,/nodes -> /accounts;添加账号直达 Cloudflare OAuth,自动创建账号并同步账号下全部域名(catch-all 邮箱随同步创建) - 域名:移除手工添加;点击域名『设置 Email Workers』= 部署账号级 Email Worker + Email Routing + catch-all;支持按账号筛选 - Webhook 三级模型:邮箱 -> 域名 -> 全局,收信取第一个命中者;支持自定义请求头与 key(默认 Authorization: Bearer <key>,兼做 X-WmailHub-Signature 签名);独立 webhooks 实体与 /webhooks、/keys 页面删除,Key 管理收进设置模态窗口 - 设置中心:全局 Webhook / 默认 Node Ingress 地址 / 修改密码 / API & MCP Keys(生成、撤销、删除) - UI:Header 标题副标题紧凑化(修复溢出),LOGO 加版本号 tag(v1.1.0 取自 package.json),各列表页搜索框 - 工具链:前端构建 Bun.build -> Vite(vite 8 + @vitejs/plugin-react) - 后端:/api/settings、POST /api/domains/:id/setup、0002 迁移(投递行固化三级目标配置);MCP 去 list_webhooks(5 工具);迁移执行器加 _migrations 记录 - 测试:44/44(新增 settings 套件、webhook 三级优先级/SSRF/签名);UI 冒烟 11/11
117 lines
4.8 KiB
TypeScript
117 lines
4.8 KiB
TypeScript
/** 设置测试:全局 Webhook 读写与 key 语义、默认 Ingress、修改密码 */
|
|
import { describe, expect, test } from "bun:test";
|
|
import { authedFetch, loginCookie, makeTestEnv } from "./helpers.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("默认 Node Ingress 地址保存与读取", async () => {
|
|
const t = await makeTestEnv();
|
|
const cookie = await loginCookie(t);
|
|
const save = await authedFetch(t, cookie, "http://localhost/api/settings", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ default_ingress_url: "https://node.example.com/ingress/email" }),
|
|
});
|
|
expect(save.status).toBe(200);
|
|
const info = (await (
|
|
await authedFetch(t, cookie, "http://localhost/api/settings")
|
|
).json()) as { default_ingress_url: string };
|
|
expect(info.default_ingress_url).toBe("https://node.example.com/ingress/email");
|
|
|
|
// 非法地址被拒
|
|
const bad = await authedFetch(t, cookie, "http://localhost/api/settings", {
|
|
method: "PUT",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ default_ingress_url: "not-a-url" }),
|
|
});
|
|
expect(bad.status).toBe(400);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|