From cb349ec9797a16edd4523fe33e8b36e53180dd1e Mon Sep 17 00:00:00 2001 From: yu <191972707@qq.com> Date: Tue, 3 Nov 2026 15:35:32 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E7=BD=91=E7=BB=9C=E4=B8=8E=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=86=92=E7=83=9F=E5=9B=9E=E5=BD=92:=20HTTP=E4=B8=89?= =?UTF-8?q?=E4=BB=B6=E5=A5=97(=E5=8F=82=E6=95=B0/JSON=E8=A1=A8=E5=8D=95/PU?= =?UTF-8?q?T=20Cookie=E8=90=BD=E7=9B=98/5xx=E8=B6=85=E6=97=B6=E6=8A=A5?= =?UTF-8?q?=E9=94=99)+=E9=92=89=E9=92=89=E9=A3=9E=E4=B9=A6=E4=BC=81?= =?UTF-8?q?=E5=BE=AEwebhook=E6=B6=88=E6=81=AF=E4=BD=93+=E7=9C=9F=E5=AE=9ES?= =?UTF-8?q?MTP=E4=BC=9A=E8=AF=9D+AI=E6=9C=AA=E9=85=8D=E7=BD=AE=E5=BC=95?= =?UTF-8?q?=E5=AF=BC=E6=8A=A5=E9=94=99=2010=E9=A1=B9=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../capabilities/net-message-smoke.test.ts | 229 ++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/main/capabilities/net-message-smoke.test.ts diff --git a/src/main/capabilities/net-message-smoke.test.ts b/src/main/capabilities/net-message-smoke.test.ts new file mode 100644 index 0000000..8c4f7ca --- /dev/null +++ b/src/main/capabilities/net-message-smoke.test.ts @@ -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): Promise { + 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 .\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服务 配置后可用"); + }); +});