test: 网络与消息冒烟回归: HTTP三件套(参数/JSON表单/PUT Cookie落盘/5xx超时报错)+钉钉飞书企微webhook消息体+真实SMTP会话+AI未配置引导报错 10项通过
This commit is contained in:
parent
d4de0ec245
commit
cb349ec979
229
src/main/capabilities/net-message-smoke.test.ts
Normal file
229
src/main/capabilities/net-message-smoke.test.ts
Normal file
@ -0,0 +1,229 @@
|
||||
import { createServer, type IncomingMessage, type ServerResponse, type Server } from "node:http";
|
||||
import net from "node:net";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getHandler } from "../engine/registry";
|
||||
import type { CatalogComponent, FlowNode } from "../../shared/types";
|
||||
import type { RunContext } from "../engine/types";
|
||||
import "./http";
|
||||
import "./message";
|
||||
import "./common-final";
|
||||
import "./ai-caps";
|
||||
|
||||
const ctx = {
|
||||
signal: new AbortController().signal,
|
||||
log: () => {},
|
||||
vars: {},
|
||||
evidenceDir: "D:/office-test2",
|
||||
runId: "smoke-run",
|
||||
flow: { id: "smoke-flow", name: "冒烟", description: "", enabled: true, trigger: null, meta: { appVersion: "dev", createdAt: "", updatedAt: "" }, nodes: [] }
|
||||
} as unknown as RunContext;
|
||||
|
||||
const node = { id: "smoke-net", componentId: "smoke", position: { x: 0, y: 0 }, values: {}, options: {} } as unknown as FlowNode;
|
||||
|
||||
async function run(componentId: string, args: Record<string, unknown>): Promise<unknown> {
|
||||
const handler = getHandler(componentId);
|
||||
if (!handler) throw new Error(`组件 ${componentId} 未注册运行时`);
|
||||
return handler(ctx, { ...node, componentId } as unknown as FlowNode, args, {} as CatalogComponent);
|
||||
}
|
||||
|
||||
// ---- 本地 HTTP 测试服务器:记录请求并回 JSON ----
|
||||
interface Recorded { method: string; url: string; headers: IncomingMessage["headers"]; body: string; }
|
||||
function startHttpServer(responder?: (req: Recorded, res: ServerResponse) => void): Promise<{ server: Server; port: number; requests: Recorded[] }> {
|
||||
const requests: Recorded[] = [];
|
||||
const server = createServer((req, res) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on("data", (c: Buffer) => chunks.push(c));
|
||||
req.on("end", () => {
|
||||
const record: Recorded = { method: req.method ?? "", url: req.url ?? "", headers: req.headers, body: Buffer.concat(chunks).toString("utf8") };
|
||||
requests.push(record);
|
||||
if (responder) return responder(record, res);
|
||||
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
res.end(JSON.stringify({ ok: true, echo: record.body, path: record.url }));
|
||||
});
|
||||
});
|
||||
return new Promise((resolve) => server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address() as { port: number };
|
||||
resolve({ server, port: addr.port, requests });
|
||||
}));
|
||||
}
|
||||
|
||||
// ---- 最小 SMTP 服务器:实现 nodemailer 客户端需要的会话 ----
|
||||
interface SmtpMessage { from: string; to: string[]; data: string; }
|
||||
function startSmtpServer(): Promise<{ server: net.Server; port: number; messages: SmtpMessage[] }> {
|
||||
const messages: SmtpMessage[] = [];
|
||||
const server = net.createServer((socket) => {
|
||||
let stage: "cmd" | "data" = "cmd";
|
||||
let from = "", to: string[] = [], data = "";
|
||||
socket.write("220 smtp.smoke.test ESMTP\r\n");
|
||||
socket.on("data", (chunk: Buffer) => {
|
||||
const text = chunk.toString("utf8");
|
||||
if (stage === "data") {
|
||||
data += text;
|
||||
if (/\r\n\.\r\n$/.test(data)) {
|
||||
stage = "cmd";
|
||||
messages.push({ from, to, data });
|
||||
socket.write("250 Ok: queued\r\n");
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const line of text.split("\r\n").filter(Boolean)) {
|
||||
const cmd = line.toUpperCase();
|
||||
if (cmd.startsWith("EHLO") || cmd.startsWith("HELO")) socket.write("250-smtp.smoke.test\r\n250 SIZE 10485760\r\n");
|
||||
else if (cmd.startsWith("MAIL FROM")) { from = line.slice(line.indexOf("<") + 1, line.lastIndexOf(">")); socket.write("250 Ok\r\n"); }
|
||||
else if (cmd.startsWith("RCPT TO")) { to.push(line.slice(line.indexOf("<") + 1, line.lastIndexOf(">"))); socket.write("250 Ok\r\n"); }
|
||||
else if (cmd === "DATA") { stage = "data"; data = ""; socket.write("354 End data with <CR><LF>.<CR><LF>\r\n"); }
|
||||
else if (cmd === "QUIT") { socket.write("221 Bye\r\n"); socket.end(); }
|
||||
else socket.write("250 Ok\r\n");
|
||||
}
|
||||
});
|
||||
});
|
||||
return new Promise((resolve) => server.listen(0, "127.0.0.1", () => {
|
||||
const addr = server.address() as { port: number };
|
||||
resolve({ server, port: addr.port, messages });
|
||||
}));
|
||||
}
|
||||
|
||||
describe("网络与消息组件冒烟", () => {
|
||||
it("Http_Get_V2: 参数拼接+JSON解析", async () => {
|
||||
const { server, port, requests } = await startHttpServer();
|
||||
const out = await run("Http_Get_V2", {
|
||||
url: `http://127.0.0.1:${port}/api/search`,
|
||||
param: JSON.stringify({ q: "冒烟测试", page: "2" }),
|
||||
headers: JSON.stringify({ "X-Trace": "rpa-smoke" })
|
||||
}) as { ok: boolean };
|
||||
server.close();
|
||||
expect(out.ok).toBe(true);
|
||||
expect(requests[0].method).toBe("GET");
|
||||
expect(requests[0].url).toContain("q=%E5%86%92%E7%83%9F%E6%B5%8B%E8%AF%95");
|
||||
expect(requests[0].url).toContain("page=2");
|
||||
expect(requests[0].headers["x-trace"]).toBe("rpa-smoke");
|
||||
console.log("[Http_Get_V2] 通过, 服务端收到:", requests[0].url);
|
||||
});
|
||||
|
||||
it("Http_Post_V3: JSON体与表单体", async () => {
|
||||
const { server, port, requests } = await startHttpServer();
|
||||
await run("Http_Post_V3", {
|
||||
url: `http://127.0.0.1:${port}/submit`,
|
||||
body: JSON.stringify({ name: "张三", amount: 99.5 }),
|
||||
headers: JSON.stringify({ "Content-Type": "application/json" })
|
||||
});
|
||||
await run("Http_Post_V3", {
|
||||
url: `http://127.0.0.1:${port}/form`,
|
||||
param: JSON.stringify({ user: "li", token: "abc123" })
|
||||
});
|
||||
server.close();
|
||||
expect(requests[0].headers["content-type"]).toContain("application/json");
|
||||
expect(JSON.parse(requests[0].body)).toEqual({ name: "张三", amount: 99.5 });
|
||||
expect(requests[1].body).toContain("user=li");
|
||||
expect(requests[1].body).toContain("token=abc123");
|
||||
console.log("[Http_Post_V3] 通过: JSON与表单两种提交均正确");
|
||||
});
|
||||
|
||||
it("Http_Http_Request: PUT方法+Cookie+保存响应", async () => {
|
||||
const { server, port, requests } = await startHttpServer();
|
||||
const saveTo = "D:/office-test2/http-smoke-response.json";
|
||||
await run("Http_Http_Request", {
|
||||
url: `http://127.0.0.1:${port}/update`,
|
||||
method: "PUT",
|
||||
body: "raw-payload",
|
||||
cookies: JSON.stringify({ session: "s-77", lang: "zh" }),
|
||||
file_path: saveTo
|
||||
});
|
||||
server.close();
|
||||
expect(requests[0].method).toBe("PUT");
|
||||
expect(requests[0].headers.cookie).toBe("session=s-77; lang=zh");
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const saved = JSON.parse(readFileSync(saveTo, "utf8")) as { echo: string };
|
||||
expect(saved.echo).toBe("raw-payload");
|
||||
console.log("[Http_Http_Request] 通过: PUT+Cookie+响应落盘均正确");
|
||||
});
|
||||
|
||||
it("Http 错误路径: 5xx 明确报错 + 超时报错", async () => {
|
||||
const { server, port } = await startHttpServer((_req, res) => { res.statusCode = 500; res.end("boom"); });
|
||||
await expect(run("Http_Get_V2", { url: `http://127.0.0.1:${port}/x` })).rejects.toThrow(/HTTP 500/);
|
||||
server.close();
|
||||
|
||||
const slow = await startHttpServer(() => { /* 不响应,触发超时 */ });
|
||||
await expect(run("Http_Get_V2", { url: `http://127.0.0.1:${slow.port}/slow`, timeout: 1100 })).rejects.toThrow(/超时/);
|
||||
slow.server.close();
|
||||
console.log("[Http 错误路径] 通过: 500与超时均有明确中文报错");
|
||||
}, 60000);
|
||||
|
||||
it("钉钉推送: 文本/Markdown/加签", async () => {
|
||||
const { server, port, requests } = await startHttpServer((_req, res) => res.end(JSON.stringify({ errcode: 0, errmsg: "ok" })));
|
||||
await run("MessageNotification_DingTalk_Message", { webhook: `http://127.0.0.1:${port}/hook`, content_text: "RPA冒烟通知" });
|
||||
await run("MessageNotification_DingTalk_Message", { webhook: `http://127.0.0.1:${port}/hook2`, message_type: "1", content_markdown: "**加粗**通知" });
|
||||
await run("MessageNotification_DingTalk_Message", { webhook: `http://127.0.0.1:${port}/hook3`, key: "SECcret", content_text: "签名消息" });
|
||||
server.close();
|
||||
const b0 = JSON.parse(requests[0].body) as { msgtype: string; text: { content: string } };
|
||||
expect(b0.msgtype).toBe("text");
|
||||
expect(b0.text.content).toBe("RPA冒烟通知");
|
||||
const b1 = JSON.parse(requests[1].body) as { msgtype: string; markdown: { text: string } };
|
||||
expect(b1.msgtype).toBe("markdown");
|
||||
expect(b1.markdown.text).toContain("加粗");
|
||||
expect(requests[2].url).toContain("timestamp=");
|
||||
expect(requests[2].url).toContain("sign=");
|
||||
console.log("[钉钉推送] 通过: 文本/Markdown/加签三种请求体正确");
|
||||
});
|
||||
|
||||
it("飞书与企微推送: 消息体结构", async () => {
|
||||
const { server, port, requests } = await startHttpServer((_req, res) => res.end(JSON.stringify({ code: 0 })));
|
||||
await run("MessageNotification_Feishu_Message", { webhook: `http://127.0.0.1:${port}/fs`, content_text: "飞书文本" });
|
||||
await run("MessageNotification_Feishu_Message", { webhook: `http://127.0.0.1:${port}/fs2`, message_type: "1", content_markdown: "飞书卡片" });
|
||||
await run("MessageNotification_WXWork_Message", { webhook: `http://127.0.0.1:${port}/wx`, content_text: "企微文本", mentioned_mobile_list: "13800138000,13900139000" });
|
||||
server.close();
|
||||
const fs0 = JSON.parse(requests[0].body) as { msg_type: string; content: { text: string } };
|
||||
expect(fs0.msg_type).toBe("text");
|
||||
expect(fs0.content.text).toBe("飞书文本");
|
||||
const fs1 = JSON.parse(requests[1].body) as { msg_type: string };
|
||||
expect(fs1.msg_type).toBe("interactive");
|
||||
const wx = JSON.parse(requests[2].body) as { msgtype: string; text: { content: string; mentioned_mobile_list: string[] } };
|
||||
expect(wx.msgtype).toBe("text");
|
||||
expect(wx.text.content).toBe("企微文本");
|
||||
expect(wx.text.mentioned_mobile_list).toEqual(["13800138000", "13900139000"]);
|
||||
console.log("[飞书/企微推送] 通过");
|
||||
});
|
||||
|
||||
it("Webhook 校验: 非法地址给明确报错", async () => {
|
||||
await expect(run("MessageNotification_Feishu_Message", { webhook: "not-a-url" })).rejects.toThrow(/Webhook 地址无效/);
|
||||
await expect(run("MessageNotification_DingTalk_Message", { webhook: "http://x.test/h", content_custom: "{bad json" })).rejects.toThrow(/JSON 无效/);
|
||||
console.log("[Webhook 校验] 通过");
|
||||
});
|
||||
|
||||
it("Mail_Send_V1: 真实SMTP会话(本地最小服务器)", async () => {
|
||||
const { server, port, messages } = await startSmtpServer();
|
||||
const out = await run("Mail_Send_V1", {
|
||||
host: "127.0.0.1",
|
||||
port: String(port),
|
||||
account: "rpa-bot@smoke.test",
|
||||
to: "alice@smoke.test, bob@smoke.test",
|
||||
subject: "冒烟测试邮件",
|
||||
content: "这是 RPA Studio 发出的测试正文"
|
||||
}) as { messageId: string; accepted: string[] };
|
||||
server.close();
|
||||
expect(messages.length).toBe(1);
|
||||
expect(messages[0].from).toBe("rpa-bot@smoke.test");
|
||||
expect(messages[0].to).toEqual(["alice@smoke.test", "bob@smoke.test"]);
|
||||
// nodemailer 按 RFC 2047 将中文主题编码为 =?UTF-8?B?...?=,正文 base64,按标准解码后校验
|
||||
expect(messages[0].data).toMatch(/Subject: =\?UTF-8\?B\?/);
|
||||
const subjectDecoded = Buffer.from(messages[0].data.match(/Subject: =\?UTF-8\?B\?([^?]+)\?=/)![1], "base64").toString("utf8");
|
||||
expect(subjectDecoded).toBe("冒烟测试邮件");
|
||||
const bodyB64 = messages[0].data.split(/\r\n\r\n/)[1]?.split("\r\n.").find((l) => l.trim()) ?? "";
|
||||
expect(Buffer.from(bodyB64, "base64").toString("utf8")).toContain("测试正文");
|
||||
expect(out.accepted).toEqual(["alice@smoke.test", "bob@smoke.test"]);
|
||||
console.log("[Mail_Send_V1] 通过: SMTP会话完整, 收件人与正文均正确送达");
|
||||
}, 60000);
|
||||
|
||||
it("邮件校验: 配置不完整与收件连接缺参报错", async () => {
|
||||
await expect(run("Mail_Send_V1", { to: "a@b.test" })).rejects.toThrow(/邮件配置不完整/);
|
||||
await expect(run("Mail_RecvConnect", {})).rejects.toThrow();
|
||||
console.log("[邮件校验] 通过");
|
||||
}, 60000);
|
||||
|
||||
it("NLP/AI 组件: 未配置时给可操作的引导报错", async () => {
|
||||
await expect(run("NLP_KeyWordsAnalysis", { text: "这是一个测试句子" })).rejects.toThrow(/AI 服务未配置/);
|
||||
await expect(run("DataService_LargeModel", { input: "你好" })).rejects.toThrow(/AI 服务未配置/);
|
||||
await expect(run("Scenario_TextComparison", { text: "甲", input: "乙" })).rejects.toThrow(/AI 服务未配置/);
|
||||
console.log("[NLP/AI] 报错路径通过(3个代表): 需在 设置→AI服务 配置后可用");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user