Skip to content

Commit 6464149

Browse files
committed
refactor: share feishu webhook monitor harness
1 parent 88b87d8 commit 6464149

File tree

3 files changed

+114
-192
lines changed

3 files changed

+114
-192
lines changed

extensions/feishu/src/monitor.webhook-e2e.test.ts

Lines changed: 7 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import crypto from "node:crypto";
2-
import { createServer } from "node:http";
3-
import type { AddressInfo } from "node:net";
4-
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
52
import { afterEach, describe, expect, it, vi } from "vitest";
63
import { createFeishuRuntimeMockModule } from "./monitor.test-mocks.js";
4+
import { withRunningWebhookMonitor } from "./monitor.webhook.test-helpers.js";
75

86
const probeFeishuMock = vi.hoisted(() => vi.fn());
97

@@ -23,61 +21,6 @@ vi.mock("./runtime.js", () => createFeishuRuntimeMockModule());
2321

2422
import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js";
2523

26-
async function getFreePort(): Promise<number> {
27-
const server = createServer();
28-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
29-
const address = server.address() as AddressInfo | null;
30-
if (!address) {
31-
throw new Error("missing server address");
32-
}
33-
await new Promise<void>((resolve) => server.close(() => resolve()));
34-
return address.port;
35-
}
36-
37-
async function waitUntilServerReady(url: string): Promise<void> {
38-
for (let i = 0; i < 50; i += 1) {
39-
try {
40-
const response = await fetch(url, { method: "GET" });
41-
if (response.status >= 200 && response.status < 500) {
42-
return;
43-
}
44-
} catch {
45-
// retry
46-
}
47-
await new Promise((resolve) => setTimeout(resolve, 20));
48-
}
49-
throw new Error(`server did not start: ${url}`);
50-
}
51-
52-
function buildConfig(params: {
53-
accountId: string;
54-
path: string;
55-
port: number;
56-
verificationToken?: string;
57-
encryptKey?: string;
58-
}): ClawdbotConfig {
59-
return {
60-
channels: {
61-
feishu: {
62-
enabled: true,
63-
accounts: {
64-
[params.accountId]: {
65-
enabled: true,
66-
appId: "cli_test",
67-
appSecret: "secret_test", // pragma: allowlist secret
68-
connectionMode: "webhook",
69-
webhookHost: "127.0.0.1",
70-
webhookPort: params.port,
71-
webhookPath: params.path,
72-
encryptKey: params.encryptKey,
73-
verificationToken: params.verificationToken,
74-
},
75-
},
76-
},
77-
},
78-
} as ClawdbotConfig;
79-
}
80-
8124
function signFeishuPayload(params: {
8225
encryptKey: string;
8326
payload: Record<string, unknown>;
@@ -107,43 +50,6 @@ function encryptFeishuPayload(encryptKey: string, payload: Record<string, unknow
10750
return Buffer.concat([iv, encrypted]).toString("base64");
10851
}
10952

110-
async function withRunningWebhookMonitor(
111-
params: {
112-
accountId: string;
113-
path: string;
114-
verificationToken: string;
115-
encryptKey: string;
116-
},
117-
run: (url: string) => Promise<void>,
118-
) {
119-
const port = await getFreePort();
120-
const cfg = buildConfig({
121-
accountId: params.accountId,
122-
path: params.path,
123-
port,
124-
encryptKey: params.encryptKey,
125-
verificationToken: params.verificationToken,
126-
});
127-
128-
const abortController = new AbortController();
129-
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
130-
const monitorPromise = monitorFeishuProvider({
131-
config: cfg,
132-
runtime,
133-
abortSignal: abortController.signal,
134-
});
135-
136-
const url = `http://127.0.0.1:${port}${params.path}`;
137-
await waitUntilServerReady(url);
138-
139-
try {
140-
await run(url);
141-
} finally {
142-
abortController.abort();
143-
await monitorPromise;
144-
}
145-
}
146-
14753
afterEach(() => {
14854
stopFeishuMonitor();
14955
});
@@ -159,6 +65,7 @@ describe("Feishu webhook signed-request e2e", () => {
15965
verificationToken: "verify_token",
16066
encryptKey: "encrypt_key",
16167
},
68+
monitorFeishuProvider,
16269
async (url) => {
16370
const payload = { type: "url_verification", challenge: "challenge-token" };
16471
const response = await fetch(url, {
@@ -185,6 +92,7 @@ describe("Feishu webhook signed-request e2e", () => {
18592
verificationToken: "verify_token",
18693
encryptKey: "encrypt_key",
18794
},
95+
monitorFeishuProvider,
18896
async (url) => {
18997
const response = await fetch(url, {
19098
method: "POST",
@@ -208,6 +116,7 @@ describe("Feishu webhook signed-request e2e", () => {
208116
verificationToken: "verify_token",
209117
encryptKey: "encrypt_key",
210118
},
119+
monitorFeishuProvider,
211120
async (url) => {
212121
const response = await fetch(url, {
213122
method: "POST",
@@ -231,6 +140,7 @@ describe("Feishu webhook signed-request e2e", () => {
231140
verificationToken: "verify_token",
232141
encryptKey: "encrypt_key",
233142
},
143+
monitorFeishuProvider,
234144
async (url) => {
235145
const payload = { type: "url_verification", challenge: "challenge-token" };
236146
const response = await fetch(url, {
@@ -255,6 +165,7 @@ describe("Feishu webhook signed-request e2e", () => {
255165
verificationToken: "verify_token",
256166
encryptKey: "encrypt_key",
257167
},
168+
monitorFeishuProvider,
258169
async (url) => {
259170
const payload = {
260171
schema: "2.0",
@@ -283,6 +194,7 @@ describe("Feishu webhook signed-request e2e", () => {
283194
verificationToken: "verify_token",
284195
encryptKey: "encrypt_key",
285196
},
197+
monitorFeishuProvider,
286198
async (url) => {
287199
const payload = {
288200
encrypt: encryptFeishuPayload("encrypt_key", {

extensions/feishu/src/monitor.webhook-security.test.ts

Lines changed: 9 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import { createServer } from "node:http";
2-
import type { AddressInfo } from "node:net";
3-
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
41
import { afterEach, describe, expect, it, vi } from "vitest";
52
import {
63
createFeishuClientMockModule,
74
createFeishuRuntimeMockModule,
85
} from "./monitor.test-mocks.js";
6+
import {
7+
buildWebhookConfig,
8+
getFreePort,
9+
withRunningWebhookMonitor,
10+
} from "./monitor.webhook.test-helpers.js";
911

1012
const probeFeishuMock = vi.hoisted(() => vi.fn());
1113

@@ -33,98 +35,6 @@ import {
3335
stopFeishuMonitor,
3436
} from "./monitor.js";
3537

36-
async function getFreePort(): Promise<number> {
37-
const server = createServer();
38-
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
39-
const address = server.address() as AddressInfo | null;
40-
if (!address) {
41-
throw new Error("missing server address");
42-
}
43-
await new Promise<void>((resolve) => server.close(() => resolve()));
44-
return address.port;
45-
}
46-
47-
async function waitUntilServerReady(url: string): Promise<void> {
48-
for (let i = 0; i < 50; i += 1) {
49-
try {
50-
const response = await fetch(url, { method: "GET" });
51-
if (response.status >= 200 && response.status < 500) {
52-
return;
53-
}
54-
} catch {
55-
// retry
56-
}
57-
await new Promise((resolve) => setTimeout(resolve, 20));
58-
}
59-
throw new Error(`server did not start: ${url}`);
60-
}
61-
62-
function buildConfig(params: {
63-
accountId: string;
64-
path: string;
65-
port: number;
66-
verificationToken?: string;
67-
encryptKey?: string;
68-
}): ClawdbotConfig {
69-
return {
70-
channels: {
71-
feishu: {
72-
enabled: true,
73-
accounts: {
74-
[params.accountId]: {
75-
enabled: true,
76-
appId: "cli_test",
77-
appSecret: "secret_test", // pragma: allowlist secret
78-
connectionMode: "webhook",
79-
webhookHost: "127.0.0.1",
80-
webhookPort: params.port,
81-
webhookPath: params.path,
82-
encryptKey: params.encryptKey,
83-
verificationToken: params.verificationToken,
84-
},
85-
},
86-
},
87-
},
88-
} as ClawdbotConfig;
89-
}
90-
91-
async function withRunningWebhookMonitor(
92-
params: {
93-
accountId: string;
94-
path: string;
95-
verificationToken: string;
96-
encryptKey: string;
97-
},
98-
run: (url: string) => Promise<void>,
99-
) {
100-
const port = await getFreePort();
101-
const cfg = buildConfig({
102-
accountId: params.accountId,
103-
path: params.path,
104-
port,
105-
encryptKey: params.encryptKey,
106-
verificationToken: params.verificationToken,
107-
});
108-
109-
const abortController = new AbortController();
110-
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
111-
const monitorPromise = monitorFeishuProvider({
112-
config: cfg,
113-
runtime,
114-
abortSignal: abortController.signal,
115-
});
116-
117-
const url = `http://127.0.0.1:${port}${params.path}`;
118-
await waitUntilServerReady(url);
119-
120-
try {
121-
await run(url);
122-
} finally {
123-
abortController.abort();
124-
await monitorPromise;
125-
}
126-
}
127-
12838
afterEach(() => {
12939
clearFeishuWebhookRateLimitStateForTest();
13040
stopFeishuMonitor();
@@ -134,7 +44,7 @@ describe("Feishu webhook security hardening", () => {
13444
it("rejects webhook mode without verificationToken", async () => {
13545
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
13646

137-
const cfg = buildConfig({
47+
const cfg = buildWebhookConfig({
13848
accountId: "missing-token",
13949
path: "/hook-missing-token",
14050
port: await getFreePort(),
@@ -148,7 +58,7 @@ describe("Feishu webhook security hardening", () => {
14858
it("rejects webhook mode without encryptKey", async () => {
14959
probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
15060

151-
const cfg = buildConfig({
61+
const cfg = buildWebhookConfig({
15262
accountId: "missing-encrypt-key",
15363
path: "/hook-missing-encrypt",
15464
port: await getFreePort(),
@@ -167,6 +77,7 @@ describe("Feishu webhook security hardening", () => {
16777
verificationToken: "verify_token",
16878
encryptKey: "encrypt_key",
16979
},
80+
monitorFeishuProvider,
17081
async (url) => {
17182
const response = await fetch(url, {
17283
method: "POST",
@@ -189,6 +100,7 @@ describe("Feishu webhook security hardening", () => {
189100
verificationToken: "verify_token",
190101
encryptKey: "encrypt_key",
191102
},
103+
monitorFeishuProvider,
192104
async (url) => {
193105
let saw429 = false;
194106
for (let i = 0; i < 130; i += 1) {

0 commit comments

Comments
 (0)