import { IncomingMessage, ServerResponse } from "node:http";
import { Socket } from "node:net";
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "openclaw/plugin-sdk";
import type { ResolvedWecomAccount } from "./types.js";
import { computeWecomMsgSignature, decryptWecomEncrypted, encryptWecomPlaintext } from "./crypto.js";
import { handleWecomWebhookRequest, registerWecomWebhookTarget } from "./monitor.js";
function createMockRequest(params: {
method: "GET" | "POST";
url: string;
body?: unknown;
}): IncomingMessage {
const socket = new Socket();
const req = new IncomingMessage(socket);
req.method = params.method;
req.url = params.url;
if (params.method === "POST") {
req.push(JSON.stringify(params.body ?? {}));
}
req.push(null);
return req;
}
function createMockResponse(): ServerResponse & {
_getData: () => string;
_getStatusCode: () => number;
} {
const req = new IncomingMessage(new Socket());
const res = new ServerResponse(req);
let data = "";
res.write = (chunk: any) => {
data += String(chunk);
return true;
};
res.end = (chunk: any) => {
if (chunk) data += String(chunk);
return res;
};
(res as any)._getData = () => data;
(res as any)._getStatusCode = () => res.statusCode;
return res as any;
}
describe("handleWecomWebhookRequest", () => {
const token = "test-token";
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
it("handles GET url verification", async () => {
const account: ResolvedWecomAccount = {
accountId: "default",
name: "Test",
enabled: true,
configured: true,
token,
encodingAESKey,
receiveId: "",
config: { webhookPath: "/hook", token, encodingAESKey },
};
const unregister = registerWecomWebhookTarget({
account,
config: {} as OpenClawConfig,
runtime: {},
core: {} as any,
path: "/hook",
});
try {
const timestamp = "13500001234";
const nonce = "123412323";
const echostr = encryptWecomPlaintext({
encodingAESKey,
receiveId: "",
plaintext: "ping",
});
const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt: echostr });
const req = createMockRequest({
method: "GET",
url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}&echostr=${encodeURIComponent(echostr)}`,
});
const res = createMockResponse();
const handled = await handleWecomWebhookRequest(req, res);
expect(handled).toBe(true);
expect(res._getStatusCode()).toBe(200);
expect(res._getData()).toBe("ping");
} finally {
unregister();
}
});
it("handles POST callback and returns encrypted stream placeholder", async () => {
const account: ResolvedWecomAccount = {
accountId: "default",
name: "Test",
enabled: true,
configured: true,
token,
encodingAESKey,
receiveId: "",
config: { webhookPath: "/hook", token, encodingAESKey },
};
const unregister = registerWecomWebhookTarget({
account,
config: {} as OpenClawConfig,
runtime: {},
core: {} as any,
path: "/hook",
});
try {
const timestamp = "1700000000";
const nonce = "nonce";
const plain = JSON.stringify({
msgid: "MSGID",
aibotid: "AIBOTID",
chattype: "single",
from: { userid: "USERID" },
response_url: "RESPONSEURL",
msgtype: "text",
text: { content: "hello" },
});
const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
const req = createMockRequest({
method: "POST",
url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
body: { encrypt },
});
const res = createMockResponse();
const handled = await handleWecomWebhookRequest(req, res);
expect(handled).toBe(true);
expect(res._getStatusCode()).toBe(200);
const json = JSON.parse(res._getData()) as any;
expect(typeof json.encrypt).toBe("string");
expect(typeof json.msgsignature).toBe("string");
expect(typeof json.timestamp).toBe("string");
expect(typeof json.nonce).toBe("string");
const replyPlain = decryptWecomEncrypted({
encodingAESKey,
receiveId: "",
encrypt: json.encrypt,
});
const reply = JSON.parse(replyPlain) as any;
expect(reply.msgtype).toBe("stream");
expect(reply.stream?.content).toBe("1");
expect(reply.stream?.finish).toBe(false);
expect(typeof reply.stream?.id).toBe("string");
expect(reply.stream?.id.length).toBeGreaterThan(0);
const expectedSig = computeWecomMsgSignature({
token,
timestamp: String(json.timestamp),
nonce: String(json.nonce),
encrypt: String(json.encrypt),
});
expect(json.msgsignature).toBe(expectedSig);
} finally {
unregister();
}
});
it("supports custom streamPlaceholderContent", async () => {
const account: ResolvedWecomAccount = {
accountId: "default",
name: "Test",
enabled: true,
configured: true,
token,
encodingAESKey,
receiveId: "",
config: { webhookPath: "/hook", token, encodingAESKey, streamPlaceholderContent: "正在思考..." },
};
const unregister = registerWecomWebhookTarget({
account,
config: {} as OpenClawConfig,
runtime: {},
core: {} as any,
path: "/hook",
});
try {
const timestamp = "1700000001";
const nonce = "nonce2";
const plain = JSON.stringify({
msgid: "MSGID2",
aibotid: "AIBOTID",
chattype: "single",
from: { userid: "USERID2" },
response_url: "RESPONSEURL",
msgtype: "text",
text: { content: "hello" },
});
const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
const req = createMockRequest({
method: "POST",
url: `/hook?msg_signature=${encodeURIComponent(msg_signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
body: { encrypt },
});
const res = createMockResponse();
const handled = await handleWecomWebhookRequest(req, res);
expect(handled).toBe(true);
expect(res._getStatusCode()).toBe(200);
const json = JSON.parse(res._getData()) as any;
const replyPlain = decryptWecomEncrypted({
encodingAESKey,
receiveId: "",
encrypt: json.encrypt,
});
const reply = JSON.parse(replyPlain) as any;
expect(reply.msgtype).toBe("stream");
expect(reply.stream?.content).toBe("正在思考...");
expect(reply.stream?.finish).toBe(false);
} finally {
unregister();
}
});
it("returns a queued stream for 2, and an ack stream for merged follow-ups", async () => {
const token = "TOKEN";
const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
const account: any = {
accountId: "default",
enabled: true,
configured: true,
token,
encodingAESKey,
receiveId: "",
config: {
streamPlaceholderContent: "正在思考...",
debounceMs: 10_000,
},
};
const unregister = registerWecomWebhookTarget({
account,
config: {} as OpenClawConfig,
runtime: {},
core: {} as any,
path: "/hook-merge",
});
try {
const timestamp = "1700000002";
const nonce = "nonce-merge";
const makeReq = (msgid: string) => {
const plain = JSON.stringify({
msgid,
aibotid: "AIBOTID",
chattype: "single",
from: { userid: "USERID_QUEUE" },
response_url: "RESPONSEURL",
msgtype: "text",
text: { content: "hello" },
});
const encrypt = encryptWecomPlaintext({ encodingAESKey, receiveId: "", plaintext: plain });
const msg_signature = computeWecomMsgSignature({ token, timestamp, nonce, encrypt });
return createMockRequest({
method: "POST",
url: `/hook-merge?msg_signature=${encodeURIComponent(msg_signature)}×tamp=${encodeURIComponent(timestamp)}&nonce=${encodeURIComponent(nonce)}`,
body: { encrypt },
});
};
const res1 = createMockResponse();
await handleWecomWebhookRequest(makeReq("MSGID-M1"), res1);
const json1 = JSON.parse(res1._getData()) as any;
const replyPlain1 = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt: json1.encrypt });
const reply1 = JSON.parse(replyPlain1) as any;
expect(reply1.msgtype).toBe("stream");
expect(reply1.stream?.finish).toBe(false);
const res2 = createMockResponse();
await handleWecomWebhookRequest(makeReq("MSGID-M2"), res2);
const json2 = JSON.parse(res2._getData()) as any;
const replyPlain2 = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt: json2.encrypt });
const reply2 = JSON.parse(replyPlain2) as any;
expect(reply2.msgtype).toBe("stream");
expect(reply2.stream?.finish).toBe(false);
expect(reply2.stream?.id).not.toBe(reply1.stream?.id);
expect(reply2.stream?.content).toContain("排队");
const res3 = createMockResponse();
await handleWecomWebhookRequest(makeReq("MSGID-M3"), res3);
const json3 = JSON.parse(res3._getData()) as any;
const replyPlain3 = decryptWecomEncrypted({ encodingAESKey, receiveId: "", encrypt: json3.encrypt });
const reply3 = JSON.parse(replyPlain3) as any;
expect(reply3.msgtype).toBe("stream");
// merged follow-up should get its own ack stream (not finished yet);
// it will be updated to a final hint after the merged batch completes.
expect(reply3.stream?.finish).toBe(false);
expect(reply3.stream?.id).not.toBe(reply1.stream?.id);
expect(reply3.stream?.id).not.toBe(reply2.stream?.id);
expect(reply3.stream?.content).toContain("合并");
} finally {
unregister();
}
});
});