feat: WmailHub v1.0.0 —— Cloudflare Email Workers 邮件接收与转发平台(本地可用版本)
- Console:React 19 + antd 6.6.5 前端(登录 admin/admin)+ Hono 管理 API (Nodes/Domains/Mailboxes/Emails/Webhooks/Keys/OAuth/Admin) - Node:Bun 转发节点(HMAC 签名、有限重试、心跳、结构化日志、优雅退出) - Email Worker:Cloudflare email handler + 本地联调入口 - shared:HMAC 协议(docs/04)、wmh_ Key(32 位)、AES-GCM 加密、SSRF 防护 - 邮件管线:postal-mime 解析、附件、catch-all、限额、malformed 降级、TTL 1 天 - Webhook:签名投递、指数退避重试、SSRF 校验、投递日志 - API v1 + MCP(JSON-RPC,六工具,凭据 kind 隔离) - Cloudflare OAuth:真实 + 本地 sim 模式,自动安装管线(幂等,无需手工命令) - 测试:bun test 41 例(协议安全/管线/TTL/Webhook/SSRF/MCP/OAuth/鉴权)+ Playwright UI 冒烟 - LOGO/favicon 由 codex CLI 生成
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* UI 冒烟测试(Playwright headless):
|
||||
* 登录 admin/admin → 概览/节点/域名/邮箱/邮件/Webhooks/Keys 各页渲染 → 截图。
|
||||
* bun tests/ui-smoke.ts
|
||||
*/
|
||||
|
||||
import { chromium } from "playwright-core";
|
||||
|
||||
const BASE = process.env.WMAILHUI_BASE ?? "http://127.0.0.1:18787";
|
||||
const EXEC = `${process.env.HOME}/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome`;
|
||||
const SHOT_DIR = "/tmp/wmh-shots";
|
||||
await Bun.$`mkdir -p ${SHOT_DIR}`.quiet();
|
||||
|
||||
const browser = await chromium.launch({
|
||||
executablePath: EXEC,
|
||||
headless: true,
|
||||
args: ["--no-sandbox"],
|
||||
});
|
||||
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
|
||||
const errors: string[] = [];
|
||||
page.on("pageerror", (err) => errors.push(`pageerror: ${err.message}`));
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") errors.push(`console: ${msg.text()}`);
|
||||
});
|
||||
|
||||
const results: { step: string; ok: boolean; note?: string }[] = [];
|
||||
const check = (step: string, ok: boolean, note?: string) => {
|
||||
results.push({ step, ok, note });
|
||||
console.log(`${ok ? "✓" : "✗"} ${step}${note ? ` — ${note}` : ""}`);
|
||||
};
|
||||
|
||||
// 1. 登录页
|
||||
await page.goto(`${BASE}/login`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForSelector("input[placeholder='用户名']", { timeout: 8000 });
|
||||
check("登录页渲染(含 LOGO/表单)", (await page.title()).includes("WmailHub"));
|
||||
await page.screenshot({ path: `${SHOT_DIR}/01-login.png` });
|
||||
|
||||
// 2. 登录 admin/admin
|
||||
await page.fill("input[placeholder='用户名']", "admin");
|
||||
await page.fill("input[placeholder='密码']", "admin");
|
||||
await page.click("button[type=submit]");
|
||||
await page.waitForURL(`${BASE}/`, { timeout: 8000 });
|
||||
await page.waitForSelector("text=概览", { timeout: 8000 });
|
||||
check("登录成功进入概览页", true);
|
||||
await page.waitForTimeout(600);
|
||||
await page.screenshot({ path: `${SHOT_DIR}/02-dashboard.png` });
|
||||
|
||||
// 3. 各页面导航渲染
|
||||
const pages = [
|
||||
{ path: "/nodes", expect: "节点管理", shot: "03-nodes.png" },
|
||||
{ path: "/domains", expect: "域名管理", shot: "04-domains.png" },
|
||||
{ path: "/mailboxes", expect: "邮箱管理", shot: "05-mailboxes.png" },
|
||||
{ path: "/emails", expect: "邮件", shot: "06-emails.png" },
|
||||
{ path: "/webhooks", expect: "Webhooks", shot: "07-webhooks.png" },
|
||||
{ path: "/keys", expect: "API / MCP Keys", shot: "08-keys.png" },
|
||||
];
|
||||
for (const p of pages) {
|
||||
await page.goto(`${BASE}${p.path}`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForSelector(`text=${p.expect}`, { timeout: 8000 });
|
||||
await page.waitForTimeout(500);
|
||||
const hasTable = (await page.locator(".ant-table").count()) > 0;
|
||||
check(`页面 ${p.path} 渲染${hasTable ? "(含表格)" : ""}`, true);
|
||||
await page.screenshot({ path: `${SHOT_DIR}/${p.shot}` });
|
||||
}
|
||||
|
||||
// 4. 邮件详情(点击第一行)
|
||||
await page.goto(`${BASE}/emails`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForSelector(".ant-table-row", { timeout: 8000 });
|
||||
await page.locator(".ant-table-row").first().click();
|
||||
await page.waitForSelector(".ant-drawer-open", { timeout: 8000 });
|
||||
const drawerText = await page.locator(".ant-drawer-title").textContent();
|
||||
check("邮件详情抽屉打开", Boolean(drawerText && drawerText.includes("端到端")));
|
||||
await page.screenshot({ path: `${SHOT_DIR}/09-email-detail.png` });
|
||||
|
||||
// 5. 生成 API Key 流程
|
||||
await page.goto(`${BASE}/keys`, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForSelector("text=生成 Key", { timeout: 8000 });
|
||||
await page.click("button:has-text('生成 Key')");
|
||||
await page.click(".ant-dropdown-menu-item:has-text('生成 API Key')");
|
||||
await page.waitForSelector(".ant-modal:has-text('仅显示一次')", { timeout: 8000 });
|
||||
const keyText = await page.locator(".ant-modal .mono").first().textContent();
|
||||
check("生成 API Key 弹窗(wmh_ 32 位)", Boolean(keyText && keyText.startsWith("wmh_") && keyText.trim().length === 32), keyText?.trim());
|
||||
await page.screenshot({ path: `${SHOT_DIR}/10-key-created.png` });
|
||||
|
||||
await browser.close();
|
||||
|
||||
const failed = results.filter((r) => !r.ok);
|
||||
console.log(`\n==== UI 冒烟结果: ${results.length - failed.length}/${results.length} 通过 ====`);
|
||||
if (errors.length > 0) {
|
||||
console.log("浏览器错误:");
|
||||
for (const e of errors.slice(0, 10)) console.log(" ", e);
|
||||
}
|
||||
process.exit(failed.length > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user