@yanhaidao/wecom 2.2.3 → 2.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,26 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { sendActiveMessage, handleWecomWebhookRequest, registerWecomWebhookTarget } from "./monitor.js";
3
3
  import * as cryptoHelpers from "./crypto.js";
4
4
  import * as runtime from "./runtime.js";
5
- import axios from "axios";
5
+ import * as agentApi from "./agent/api-client.js";
6
6
  import { IncomingMessage, ServerResponse } from "node:http";
7
7
  import { Socket } from "node:net";
8
8
  import * as crypto from "node:crypto";
9
9
 
10
- vi.mock("axios");
10
+ const { undiciFetch } = vi.hoisted(() => {
11
+ const undiciFetch = vi.fn();
12
+ return { undiciFetch };
13
+ });
14
+
15
+ vi.mock("undici", () => ({
16
+ fetch: undiciFetch,
17
+ ProxyAgent: class ProxyAgent { },
18
+ }));
19
+
20
+ vi.mock("./agent/api-client.js", () => ({
21
+ sendText: vi.fn(),
22
+ sendMedia: vi.fn(),
23
+ uploadMedia: vi.fn(),
24
+ }));
11
25
 
12
26
  // Helpers
13
27
  function createMockRequest(bodyObj: any): IncomingMessage {
@@ -32,6 +46,9 @@ function createMockResponse(): ServerResponse {
32
46
  describe("Monitor Active Features", () => {
33
47
  let capturedDeliver: ((payload: { text: string }) => Promise<void>) | undefined;
34
48
  let mockCore: any;
49
+ let msgSeq = 0;
50
+ let senderUserId = "";
51
+ let senderChatId = "";
35
52
  // Valid 32-byte AES Key (Base64 encoded)
36
53
  const validKey = "jWmYm7qr5nMoCAstdRmNjt3p7vsH8HkK+qiJqQ0aaaa=";
37
54
 
@@ -39,6 +56,10 @@ describe("Monitor Active Features", () => {
39
56
  vi.useFakeTimers();
40
57
  capturedDeliver = undefined;
41
58
  vi.restoreAllMocks();
59
+ undiciFetch.mockClear();
60
+ msgSeq += 1;
61
+ senderUserId = `zhangsan-${msgSeq}`;
62
+ senderChatId = `wr123-${msgSeq}`;
42
63
 
43
64
  // Spy on crypto.randomBytes (default export in monitor.ts usage)
44
65
  vi.spyOn(crypto.default, "randomBytes").mockImplementation((size) => {
@@ -55,7 +76,11 @@ describe("Monitor Active Features", () => {
55
76
  // If this fails, we will know.
56
77
  vi.spyOn(cryptoHelpers, "decryptWecomEncrypted").mockImplementation((opts) => {
57
78
  return JSON.stringify({
58
- msgid: "test-msg-id",
79
+ msgid: `test-msg-id-${msgSeq}`,
80
+ aibotid: "bot-1",
81
+ chattype: "group",
82
+ chatid: senderChatId,
83
+ from: { userid: senderUserId },
59
84
  msgtype: "text",
60
85
  text: { content: "hello" },
61
86
  response_url: "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=test-key"
@@ -98,7 +123,20 @@ describe("Monitor Active Features", () => {
98
123
 
99
124
  registerWecomWebhookTarget({
100
125
  account: { accountId: "1", enabled: true, configured: true, token: "T", encodingAESKey: validKey, receiveId: "R", config: {} as any },
101
- config: {} as any,
126
+ config: {
127
+ channels: {
128
+ wecom: {
129
+ enabled: true,
130
+ agent: {
131
+ corpId: "corp",
132
+ corpSecret: "secret",
133
+ agentId: 1000002,
134
+ token: "token",
135
+ encodingAESKey: "aes",
136
+ },
137
+ },
138
+ },
139
+ } as any,
102
140
  runtime: { log: () => { } },
103
141
  core: mockCore,
104
142
  path: "/wecom"
@@ -117,7 +155,7 @@ describe("Monitor Active Features", () => {
117
155
  // The WeCom monitor debounces inbound messages before starting the agent.
118
156
  // `flushPending` triggers async agent start without awaiting it, so give the
119
157
  // microtask queue a chance to run after the timer fires.
120
- await vi.advanceTimersByTimeAsync(600);
158
+ await vi.runOnlyPendingTimersAsync();
121
159
  await Promise.resolve();
122
160
  await Promise.resolve();
123
161
 
@@ -146,11 +184,56 @@ describe("Monitor Active Features", () => {
146
184
 
147
185
  const streamId = Buffer.alloc(16, 0x11).toString("hex");
148
186
 
187
+ undiciFetch.mockResolvedValue(new Response("ok", { status: 200 }));
149
188
  await sendActiveMessage(streamId, "Active Hello");
150
189
 
151
- expect(axios.post).toHaveBeenCalledWith(
190
+ expect(undiciFetch).toHaveBeenCalledWith(
152
191
  "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=test-key",
153
- { msgtype: "text", text: { content: "Active Hello" } }
192
+ expect.objectContaining({
193
+ method: "POST",
194
+ headers: expect.objectContaining({ "Content-Type": "application/json" }),
195
+ body: JSON.stringify({ msgtype: "text", text: { content: "Active Hello" } }),
196
+ }),
154
197
  );
155
198
  });
199
+
200
+ it("should fallback non-image media to agent DM (and push a Chinese prompt)", async () => {
201
+ const { uploadMedia, sendMedia } = agentApi as any;
202
+ uploadMedia.mockResolvedValue("media-id-1");
203
+ sendMedia.mockResolvedValue(undefined);
204
+
205
+ const req = createMockRequest({ encrypt: "mock-encrypt" });
206
+ const res = createMockResponse();
207
+ await handleWecomWebhookRequest(req, res);
208
+
209
+ await vi.advanceTimersByTimeAsync(600);
210
+ await Promise.resolve();
211
+ await Promise.resolve();
212
+
213
+ expect(capturedDeliver).toBeDefined();
214
+
215
+ // Create a local PDF to force non-image content-type inference.
216
+ const fs = await import("node:fs/promises");
217
+ const os = await import("node:os");
218
+ const path = await import("node:path");
219
+ const tmp = path.join(os.tmpdir(), `wecom-test-${Date.now()}.pdf`);
220
+ await fs.writeFile(tmp, Buffer.from("pdf"));
221
+
222
+ undiciFetch.mockResolvedValue(new Response("ok", { status: 200 }));
223
+
224
+ await capturedDeliver!({ text: "here", mediaUrls: [tmp] } as any);
225
+
226
+ expect(uploadMedia).toHaveBeenCalled();
227
+ expect(sendMedia).toHaveBeenCalledWith(
228
+ expect.objectContaining({
229
+ toUser: senderUserId,
230
+ mediaType: "file",
231
+ }),
232
+ );
233
+ // Ensure we attempted to push a prompt to response_url (uses undici fetch).
234
+ expect(undiciFetch).toHaveBeenCalled();
235
+ });
236
+
237
+ // 注:本机路径(/Users/... 或 /tmp/...)短路发图逻辑属于运行态特性,
238
+ // 单测在 fake timers + module singleton 状态下容易引入脆弱性;这里优先覆盖更关键的兜底链路与去重逻辑。
156
239
  });
@@ -2,12 +2,19 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { handleWecomWebhookRequest, registerWecomWebhookTarget } from "./monitor.js";
3
3
  import { encryptWecomPlaintext, computeWecomMsgSignature, WECOM_PKCS7_BLOCK_SIZE } from "./crypto.js";
4
4
  import * as runtime from "./runtime.js";
5
- import axios from "axios";
6
5
  import crypto from "node:crypto";
7
6
  import { IncomingMessage, ServerResponse } from "node:http";
8
7
  import { Socket } from "node:net";
9
8
 
10
- vi.mock("axios");
9
+ const { undiciFetch } = vi.hoisted(() => {
10
+ const undiciFetch = vi.fn();
11
+ return { undiciFetch };
12
+ });
13
+
14
+ vi.mock("undici", () => ({
15
+ fetch: undiciFetch,
16
+ ProxyAgent: class ProxyAgent { },
17
+ }));
11
18
 
12
19
  // Helpers to simulate HTTP request
13
20
  function createMockRequest(bodyObj: any, query: URLSearchParams): IncomingMessage {
@@ -126,8 +133,8 @@ describe("Monitor Integration: Inbound Image", () => {
126
133
  cipher.setAutoPadding(false);
127
134
  const encryptedMedia = Buffer.concat([cipher.update(pkcs7Pad(fileContent, WECOM_PKCS7_BLOCK_SIZE)), cipher.final()]);
128
135
 
129
- // Mock Axios to return this encrypted media
130
- (axios.get as any).mockResolvedValue({ data: encryptedMedia, headers: { "content-length": "100" } });
136
+ // Mock HTTP fetch to return this encrypted media
137
+ undiciFetch.mockResolvedValue(new Response(encryptedMedia));
131
138
 
132
139
  // 2. Prepare Inbound Message (The Webhook JSON)
133
140
  const imageUrl = "http://wecom.server/media/123";
@@ -192,6 +199,9 @@ describe("Monitor Integration: Inbound Image", () => {
192
199
  expect(ctx.MediaPath).toBe("/tmp/saved-image.jpg");
193
200
  expect(ctx.MediaType).toBe("image/jpeg");
194
201
 
195
- expect(axios.get).toHaveBeenCalledWith(imageUrl, expect.objectContaining({ responseType: "arraybuffer" }));
202
+ expect(undiciFetch).toHaveBeenCalledWith(
203
+ imageUrl,
204
+ expect.objectContaining({ signal: expect.anything() }),
205
+ );
196
206
  });
197
207
  });