/** API v1 / MCP 测试:单一 Key(共用)、过期邮件 410、MCP 协议与工具(v2.1.0) */ import { describe, expect, test } from "bun:test"; import { ingestEmail } from "../console/src/services/email_ingest.ts"; import { authedFetch, loginCookie, makeTestEnv, TEST_MAIL } from "./helpers.ts"; /** # 函数 * 创建(轮换)单一 API/MCP Key 并返回明文 */ async function createKey(t: Awaited>, cookie: string) { const resp = await authedFetch(t, cookie, "http://localhost/api/settings/api-key", { method: "POST" }); expect(resp.status).toBe(201); return ((await resp.json()) as { key: string }).key; } describe("Console API v1(Bearer wmh_ Key)", () => { test("单一 Key 同时可访问 API 与 MCP(不再区分 kind)", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const key = await createKey(t, cookie); const v1 = await t.app.fetch( new Request("http://localhost/api/v1/mailboxes", { headers: { Authorization: `Bearer ${key}` }, }), t.env, ); expect(v1.status).toBe(200); const data = (await v1.json()) as { items: { address: string }[] }; expect(data.items.map((m) => m.address)).toContain("*@example.test"); const mcp = await t.app.fetch( new Request("http://localhost/api/v1/mcp", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }), }), t.env, ); expect(mcp.status).toBe(200); }); test("无 key / 伪造 key / 已撤销 key 均被拒", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const noKey = await t.app.fetch(new Request("http://localhost/api/v1/mailboxes"), t.env); expect(noKey.status).toBe(401); const fake = await t.app.fetch( new Request("http://localhost/api/v1/mailboxes", { headers: { Authorization: `Bearer wmh_${"A".repeat(28)}` }, }), t.env, ); expect(fake.status).toBe(401); const key = await createKey(t, cookie); // 撤销(删除 settings 中的 Key) const del = await authedFetch(t, cookie, "http://localhost/api/settings/api-key", { method: "DELETE" }); expect(del.status).toBe(200); const revoked = await t.app.fetch( new Request("http://localhost/api/v1/mailboxes", { headers: { Authorization: `Bearer ${key}` }, }), t.env, ); expect(revoked.status).toBe(401); // 重新生成后旧 Key 依旧失效(轮换语义) await createKey(t, cookie); const rotated = await t.app.fetch( new Request("http://localhost/api/v1/mailboxes", { headers: { Authorization: `Bearer ${key}` }, }), t.env, ); expect(rotated.status).toBe(401); }); test("v1 可创建与删除 mailbox", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const key = await createKey(t, cookie); // v2.9.2 起指定域名建邮箱要求域名可用(已绑 zone),先给种子域名绑上 await t.env.DB.prepare("UPDATE domains SET zone_id = 'zone-v1' WHERE id = ?").bind(t.domainId).run(); const create = await t.app.fetch( new Request("http://localhost/api/v1/mailboxes", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: JSON.stringify({ domain_id: t.domainId, address: "newbox" }), }), t.env, ); expect(create.status).toBe(201); const { id, address } = (await create.json()) as { id: string; address: string }; expect(address).toBe("newbox@example.test"); const del = await t.app.fetch( new Request(`http://localhost/api/v1/mailboxes/${id}`, { method: "DELETE", headers: { Authorization: `Bearer ${key}` }, }), t.env, ); expect(del.status).toBe(200); }); test("过期邮件:v1 详情返回 410,列表不出现", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const key = await createKey(t, cookie); const email = await ingestEmail(t.env, { raw: new TextEncoder().encode(TEST_MAIL), mailFrom: "a@b.c", rcptTo: "hello@example.test", accountId: t.accountId, }); await t.env.DB.prepare("UPDATE emails SET expires_at = '2000-01-01T00:00:00Z' WHERE id = ?") .bind(email.id) .run(); const detail = await t.app.fetch( new Request(`http://localhost/api/v1/emails/${email.id}`, { headers: { Authorization: `Bearer ${key}` }, }), t.env, ); expect(detail.status).toBe(410); const list = await t.app.fetch( new Request("http://localhost/api/v1/emails", { headers: { Authorization: `Bearer ${key}` }, }), t.env, ); const data = (await list.json()) as { items: { id: string }[] }; expect(data.items.find((e) => e.id === email.id)).toBeUndefined(); }); }); describe("MCP(/api/v1/mcp,JSON-RPC over HTTP)", () => { async function mcpCall(t: Awaited>, key: string, body: unknown) { return t.app.fetch( new Request("http://localhost/api/v1/mcp", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: JSON.stringify(body), }), t.env, ); } test("initialize + tools/list 返回六个工具与 serverInfo", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const key = await createKey(t, cookie); const init = await mcpCall(t, key, { jsonrpc: "2.0", id: 1, method: "initialize", params: {} }); const initData = (await init.json()) as { result: { serverInfo: { name: string }; protocolVersion: string } }; expect(initData.result.serverInfo.name).toBe("wmailhub"); expect(initData.result.protocolVersion).toBe("2025-06-18"); const tools = await mcpCall(t, key, { jsonrpc: "2.0", id: 2, method: "tools/list" }); const toolsData = (await tools.json()) as { result: { tools: { name: string }[] } }; expect(toolsData.result.tools.map((x) => x.name)).toEqual([ "list_domains", "list_mailboxes", "list_emails", "get_email", "create_mailbox", "delete_mailbox", ]); }); test("tools/call:create_mailbox -> list_mailboxes -> get_email -> delete_mailbox", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const key = await createKey(t, cookie); // v2.9.2 起指定域名建邮箱要求域名可用(已绑 zone),先给种子域名绑上 await t.env.DB.prepare("UPDATE domains SET zone_id = 'zone-mcp' WHERE id = ?").bind(t.domainId).run(); const email = await ingestEmail(t.env, { raw: new TextEncoder().encode(TEST_MAIL), mailFrom: "a@b.c", rcptTo: "hello@example.test", accountId: t.accountId, }); const created = await mcpCall(t, key, { jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "create_mailbox", arguments: { domain_id: t.domainId, address: "mcptest" } }, }); const createdData = (await created.json()) as { result: { content: { text: string }[] } }; expect(JSON.parse(createdData.result.content[0].text).address).toBe("mcptest@example.test"); const list = await mcpCall(t, key, { jsonrpc: "2.0", id: 4, method: "tools/call", params: { name: "list_mailboxes", arguments: {} }, }); const listData = (await list.json()) as { result: { content: { text: string }[] } }; const mailboxes = JSON.parse(listData.result.content[0].text) as { mailboxes: { id: string; address: string }[]; }; expect(mailboxes.mailboxes.map((m) => m.address)).toContain("mcptest@example.test"); const got = await mcpCall(t, key, { jsonrpc: "2.0", id: 5, method: "tools/call", params: { name: "get_email", arguments: { id: email.id } }, }); const gotData = (await got.json()) as { result: { content: { text: string }[] } }; const gotEmail = JSON.parse(gotData.result.content[0].text) as { email: { subject: string } }; expect(gotEmail.email.subject).toBe("Test Mail"); const target = mailboxes.mailboxes.find((m) => m.address === "mcptest@example.test")!.id; const del = await mcpCall(t, key, { jsonrpc: "2.0", id: 6, method: "tools/call", params: { name: "delete_mailbox", arguments: { id: target } }, }); const delData = (await del.json()) as { result: { content: { text: string }[] } }; expect(JSON.parse(delData.result.content[0].text).ok).toBe(true); }); test("未知方法返回 -32601;GET 返回 405", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const key = await createKey(t, cookie); const bad = await mcpCall(t, key, { jsonrpc: "2.0", id: 9, method: "no/such/method" }); const badData = (await bad.json()) as { error: { code: number } }; expect(badData.error.code).toBe(-32601); const get = await t.app.fetch( new Request("http://localhost/api/v1/mcp", { headers: { Authorization: `Bearer ${key}` }, }), t.env, ); expect(get.status).toBe(405); }); test("list_domains 返回域名;create_mailbox 省略 domain_id 随机分配 + webhook 入库", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const key = await createKey(t, cookie); // 种子域名无 zone_id(未启用 Email Workers,v2.9.2 起不进可用列表);再补一个已启用的域名 const now = new Date().toISOString(); await t.env.DB.prepare( "INSERT INTO domains (id, cloudflare_account_id, account_id, name, status, zone_id, created_at, updated_at) VALUES (?, 'cfacct-1', ?, 'ready.test', 'active', 'zone-ready', ?, ?)", ).bind("dom_ready", t.accountId, now, now).run(); const domains = await mcpCall(t, key, { jsonrpc: "2.0", id: 11, method: "tools/call", params: { name: "list_domains", arguments: {} } }); const domainsData = JSON.parse(((await domains.json()) as { result: { content: { text: string }[] } }).result.content[0].text) as { domains: { id: string; name: string }[]; }; // 未启用 Email Workers 的域名不可见 expect(domainsData.domains.map((d) => d.name)).toEqual(["ready.test"]); // 省略 domain_id:必须落在已启用 Email Workers 的域名上,且 webhook 参数生效 const created = await mcpCall(t, key, { jsonrpc: "2.0", id: 12, method: "tools/call", params: { name: "create_mailbox", arguments: { address: "lucky", webhook: "https://hooks.example.com/wmh" } }, }); const createdData = JSON.parse(((await created.json()) as { result: { content: { text: string }[] } }).result.content[0].text) as { address: string; }; expect(createdData.address).toBe("lucky@ready.test"); const row = await t.env.DB.prepare("SELECT webhook_url FROM mailboxes WHERE address = ?").bind("lucky@ready.test").first<{ webhook_url: string }>(); expect(row?.webhook_url).toBe("https://hooks.example.com/wmh"); // 带完整地址且省略 domain_id → 明确报错 const badAddr = await mcpCall(t, key, { jsonrpc: "2.0", id: 13, method: "tools/call", params: { name: "create_mailbox", arguments: { address: "x@example.test" } }, }); expect(((await badAddr.json()) as { error: { message: string } }).error.message).toContain("local part"); // 无已启用域名可用 → no available domain await t.env.DB.prepare("UPDATE domains SET zone_id = NULL WHERE id = 'dom_ready'").run(); const none = await mcpCall(t, key, { jsonrpc: "2.0", id: 14, method: "tools/call", params: { name: "create_mailbox", arguments: { address: "lucky2" } }, }); expect(((await none.json()) as { error: { message: string } }).error.message).toContain("no available domain"); }); test("webhook 校验失败必须整单拒绝,不留半截邮箱(API 与 MCP)", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const key = await createKey(t, cookie); await t.env.DB.prepare("UPDATE domains SET zone_id = 'zone-ready' WHERE id = ?").bind(t.domainId).run(); const restBad = await t.app.fetch( new Request("http://localhost/api/v1/mailboxes", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: JSON.stringify({ address: "halfbad", webhook: "ftp://example.com/hook" }), }), t.env, ); expect(restBad.status).toBe(400); const restRow = await t.env.DB.prepare("SELECT id FROM mailboxes WHERE address = ?").bind("halfbad@example.test").first(); expect(restRow).toBeNull(); const mcpBad = await t.app.fetch( new Request("http://localhost/api/v1/mcp", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 20, method: "tools/call", params: { name: "create_mailbox", arguments: { address: "mcpbad", webhook: "ftp://example.com/hook" } }, }), }), t.env, ); const mcpData = (await mcpBad.json()) as { error: { code: number } }; expect(mcpData.error.code).toBe(-32602); const mcpRow = await t.env.DB.prepare("SELECT id FROM mailboxes WHERE address = ?").bind("mcpbad@example.test").first(); expect(mcpRow).toBeNull(); }); test("域名可见性与建邮箱可用校验:未启用 Email Workers 的域名不可见、不可指定(v2.9.2)", async () => { const t = await makeTestEnv(); const cookie = await loginCookie(t); const key = await createKey(t, cookie); // 种子域名 active 但未绑 zone → GET /api/v1/domains 不返回它 const rest = await t.app.fetch( new Request("http://localhost/api/v1/domains", { headers: { Authorization: `Bearer ${key}` } }), t.env, ); expect(rest.status).toBe(200); expect(((await rest.json()) as { items: { name: string }[] }).items.map((d) => d.name)).not.toContain("example.test"); // 显式指定未启用域名建邮箱:REST 400 / MCP -32002 const restBad = await t.app.fetch( new Request("http://localhost/api/v1/mailboxes", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: JSON.stringify({ domain_id: t.domainId, address: "pendingbox" }), }), t.env, ); expect(restBad.status).toBe(400); expect(((await restBad.json()) as { error: string }).error).toContain("not available"); const mcpBad = await mcpCall(t, key, { jsonrpc: "2.0", id: 30, method: "tools/call", params: { name: "create_mailbox", arguments: { domain_id: t.domainId, address: "pendingmcp" } }, }); expect(((await mcpBad.json()) as { error: { message: string } }).error.message).toContain("not available"); const left = await t.env.DB.prepare("SELECT id FROM mailboxes WHERE address = 'pendingbox@example.test'").first(); expect(left).toBeNull(); // 绑上 zone 后变为可用:列表可见、创建成功 await t.env.DB.prepare("UPDATE domains SET zone_id = 'zone-ok' WHERE id = ?").bind(t.domainId).run(); const rest2 = await t.app.fetch( new Request("http://localhost/api/v1/domains", { headers: { Authorization: `Bearer ${key}` } }), t.env, ); expect(((await rest2.json()) as { items: { name: string }[] }).items.map((d) => d.name)).toContain("example.test"); const ok = await t.app.fetch( new Request("http://localhost/api/v1/mailboxes", { method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" }, body: JSON.stringify({ domain_id: t.domainId, address: "pendingbox" }), }), t.env, ); expect(ok.status).toBe(201); }); });