- 单一 Key:API 与 MCP 共用一个 wmh_ Key,凭据迁移至 settings 表(0004 删除 api_credentials) - Markdown:入库时 HTML→Markdown(mailhook 同思路),webhook payload 新增 markdown 字段 - /emails 路由改为 /inboxs 并支持域名筛选;设置按钮移入头像下拉 - 主题切换按钮(头像左侧,亮/暗持久化);设置模态仅 全局 Webhook/账号密码 两 tab 且高度限屏 - 列表操作列按钮加 SVG 图标;mailboxes 去 Webhook 列;最近邮件加收件人列
135 lines
5.0 KiB
TypeScript
135 lines
5.0 KiB
TypeScript
/** Markdown 转换与 webhook markdown 字段测试(v2.1.0) */
|
|
import { describe, expect, test } from "bun:test";
|
|
import { htmlToMarkdown } from "../shared/src/markdown.ts";
|
|
import { ingestEmail } from "../console/src/services/email_ingest.ts";
|
|
import { buildWebhookPayload } from "../console/src/services/webhook_delivery.ts";
|
|
import { loginCookie, makeTestEnv, TEST_MAIL, authedFetch } from "./helpers.ts";
|
|
|
|
/** # 成员变量: 纯文本邮件样例(无 HTML part) **/
|
|
const TEST_MAIL_PLAIN = `From: Bob <bob@example.org>\r
|
|
To: hello@example.test\r
|
|
Subject: Plain Mail\r
|
|
Content-Type: text/plain\r
|
|
\r
|
|
plain body\r
|
|
`;
|
|
|
|
/** # 成员变量: HTML 邮件样例(text/html part 带 p/b 结构) **/
|
|
const TEST_MAIL_HTML = `From: Carl <carl@example.org>\r
|
|
To: hello@example.test\r
|
|
Subject: Html Mail\r
|
|
MIME-Version: 1.0\r
|
|
Content-Type: text/html\r
|
|
\r
|
|
<html><body><p>Hello <b>world</b> and <a href="https://example.com">link</a></p></body></html>\r
|
|
`;
|
|
|
|
describe("htmlToMarkdown(mailhook 同思路)", () => {
|
|
test("标题/加粗/链接/行内代码", () => {
|
|
expect(htmlToMarkdown("<h2>Hello</h2><p>World <b>bold</b> <a href='https://x.com'>link</a> <code>cfg</code></p>")).toBe(
|
|
"## Hello\n\nWorld **bold** [link](https://x.com) `cfg`",
|
|
);
|
|
});
|
|
|
|
test("列表与嵌套列表", () => {
|
|
expect(htmlToMarkdown("<ul><li>one</li><li>two<ol><li>nested</li></ol></li></ul>")).toBe(
|
|
"- one\n\n- two\n\n 1. nested",
|
|
);
|
|
});
|
|
|
|
test("表格转管道表(| 转义)", () => {
|
|
expect(htmlToMarkdown("<table><tr><th>A</th></tr><tr><td>1|2</td></tr></table>")).toBe(
|
|
"| A |\n| --- |\n| 1\\|2 |",
|
|
);
|
|
});
|
|
|
|
test("pre 代码块保留换行;blockquote 引用;hr 分隔线", () => {
|
|
expect(htmlToMarkdown("<pre>line1\nline2</pre>")).toBe("```\nline1\nline2\n```");
|
|
expect(htmlToMarkdown("<blockquote>quoted</blockquote>")).toBe("> quoted");
|
|
expect(htmlToMarkdown("<hr/>")).toBe("---");
|
|
});
|
|
|
|
test("script/style 丢弃;空内容返回空串", () => {
|
|
expect(htmlToMarkdown("<script>alert(1)</script><style>.x{}</style>")).toBe("");
|
|
expect(htmlToMarkdown("")).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("邮件入库 markdown_body 与 webhook markdown 字段", () => {
|
|
test("HTML 邮件入库时存 Markdown;纯文本邮件 markdown=纯文本", async () => {
|
|
const t = await makeTestEnv();
|
|
const html = await ingestEmail(t.env, {
|
|
raw: new TextEncoder().encode(TEST_MAIL_HTML),
|
|
mailFrom: "a@b.c",
|
|
rcptTo: "hello@example.test",
|
|
accountId: t.accountId,
|
|
});
|
|
const row = await t.env.DB.prepare("SELECT markdown_body, text_body, html_body FROM emails WHERE id = ?")
|
|
.bind(html.id)
|
|
.first<{ markdown_body: string | null; text_body: string | null; html_body: string | null }>();
|
|
expect(row?.html_body).toBeTruthy();
|
|
expect(row?.markdown_body).toContain("**");
|
|
expect(row?.markdown_body?.trim().length ?? 0).toBeGreaterThan(0);
|
|
|
|
const plain = await ingestEmail(t.env, {
|
|
raw: new TextEncoder().encode(TEST_MAIL_PLAIN),
|
|
mailFrom: "a@b.c",
|
|
rcptTo: "hello@example.test",
|
|
accountId: t.accountId,
|
|
});
|
|
const row2 = await t.env.DB.prepare("SELECT markdown_body FROM emails WHERE id = ?")
|
|
.bind(plain.id)
|
|
.first<{ markdown_body: string | null }>();
|
|
expect(row2?.markdown_body).toBeTruthy();
|
|
});
|
|
|
|
test("webhook payload 含 markdown 字段(优先存量 markdown_body)", () => {
|
|
const payload = buildWebhookPayload({
|
|
id: "msg_x",
|
|
mailboxAddress: "hello@example.test",
|
|
fromAddress: "a@b.c",
|
|
toAddresses: '["hello@example.test"]',
|
|
subject: "s",
|
|
textBody: "plain body",
|
|
htmlBody: "<p>hello <b>world</b></p>",
|
|
markdownBody: null,
|
|
receivedAt: new Date().toISOString(),
|
|
});
|
|
expect(payload.markdown).toBe("hello **world**");
|
|
|
|
const payload2 = buildWebhookPayload({
|
|
id: "msg_y",
|
|
mailboxAddress: null,
|
|
fromAddress: null,
|
|
toAddresses: "[]",
|
|
subject: null,
|
|
textBody: null,
|
|
htmlBody: null,
|
|
markdownBody: "stored md",
|
|
receivedAt: new Date().toISOString(),
|
|
});
|
|
expect(payload2.markdown).toBe("stored md");
|
|
});
|
|
|
|
test("设置接口返回 api_key 元信息;生成接口明文 32 位 wmh_", async () => {
|
|
const t = await makeTestEnv();
|
|
const cookie = await loginCookie(t);
|
|
const before = (await (await authedFetch(t, cookie, "http://localhost/api/settings")).json()) as {
|
|
api_key: { has_key: boolean; key_last4: string };
|
|
};
|
|
expect(before.api_key.has_key).toBe(false);
|
|
|
|
const created = (await (
|
|
await authedFetch(t, cookie, "http://localhost/api/settings/api-key", { method: "POST" })
|
|
).json()) as { key: string };
|
|
expect(created.key.startsWith("wmh_")).toBe(true);
|
|
expect(created.key.length).toBe(32);
|
|
|
|
const after = (await (await authedFetch(t, cookie, "http://localhost/api/settings")).json()) as {
|
|
api_key: { has_key: boolean; key_last4: string };
|
|
};
|
|
expect(after.api_key.has_key).toBe(true);
|
|
expect(created.key.endsWith(after.api_key.key_last4)).toBe(true);
|
|
});
|
|
});
|