From 6464149031481767dae617fd4b16997a9a279fbe Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 13 Mar 2026 17:43:52 +0000 Subject: [PATCH] refactor: share feishu webhook monitor harness --- .../feishu/src/monitor.webhook-e2e.test.ts | 102 ++--------------- .../src/monitor.webhook-security.test.ts | 106 ++---------------- .../src/monitor.webhook.test-helpers.ts | 98 ++++++++++++++++ 3 files changed, 114 insertions(+), 192 deletions(-) create mode 100644 extensions/feishu/src/monitor.webhook.test-helpers.ts diff --git a/extensions/feishu/src/monitor.webhook-e2e.test.ts b/extensions/feishu/src/monitor.webhook-e2e.test.ts index 2e73f973408..451ebe0d2bf 100644 --- a/extensions/feishu/src/monitor.webhook-e2e.test.ts +++ b/extensions/feishu/src/monitor.webhook-e2e.test.ts @@ -1,9 +1,7 @@ import crypto from "node:crypto"; -import { createServer } from "node:http"; -import type { AddressInfo } from "node:net"; -import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createFeishuRuntimeMockModule } from "./monitor.test-mocks.js"; +import { withRunningWebhookMonitor } from "./monitor.webhook.test-helpers.js"; const probeFeishuMock = vi.hoisted(() => vi.fn()); @@ -23,61 +21,6 @@ vi.mock("./runtime.js", () => createFeishuRuntimeMockModule()); import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js"; -async function getFreePort(): Promise { - const server = createServer(); - await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve())); - const address = server.address() as AddressInfo | null; - if (!address) { - throw new Error("missing server address"); - } - await new Promise((resolve) => server.close(() => resolve())); - return address.port; -} - -async function waitUntilServerReady(url: string): Promise { - for (let i = 0; i < 50; i += 1) { - try { - const response = await fetch(url, { method: "GET" }); - if (response.status >= 200 && response.status < 500) { - return; - } - } catch { - // retry - } - await new Promise((resolve) => setTimeout(resolve, 20)); - } - throw new Error(`server did not start: ${url}`); -} - -function buildConfig(params: { - accountId: string; - path: string; - port: number; - verificationToken?: string; - encryptKey?: string; -}): ClawdbotConfig { - return { - channels: { - feishu: { - enabled: true, - accounts: { - [params.accountId]: { - enabled: true, - appId: "cli_test", - appSecret: "secret_test", // pragma: allowlist secret - connectionMode: "webhook", - webhookHost: "127.0.0.1", - webhookPort: params.port, - webhookPath: params.path, - encryptKey: params.encryptKey, - verificationToken: params.verificationToken, - }, - }, - }, - }, - } as ClawdbotConfig; -} - function signFeishuPayload(params: { encryptKey: string; payload: Record; @@ -107,43 +50,6 @@ function encryptFeishuPayload(encryptKey: string, payload: Record Promise, -) { - const port = await getFreePort(); - const cfg = buildConfig({ - accountId: params.accountId, - path: params.path, - port, - encryptKey: params.encryptKey, - verificationToken: params.verificationToken, - }); - - const abortController = new AbortController(); - const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() }; - const monitorPromise = monitorFeishuProvider({ - config: cfg, - runtime, - abortSignal: abortController.signal, - }); - - const url = `http://127.0.0.1:${port}${params.path}`; - await waitUntilServerReady(url); - - try { - await run(url); - } finally { - abortController.abort(); - await monitorPromise; - } -} - afterEach(() => { stopFeishuMonitor(); }); @@ -159,6 +65,7 @@ describe("Feishu webhook signed-request e2e", () => { verificationToken: "verify_token", encryptKey: "encrypt_key", }, + monitorFeishuProvider, async (url) => { const payload = { type: "url_verification", challenge: "challenge-token" }; const response = await fetch(url, { @@ -185,6 +92,7 @@ describe("Feishu webhook signed-request e2e", () => { verificationToken: "verify_token", encryptKey: "encrypt_key", }, + monitorFeishuProvider, async (url) => { const response = await fetch(url, { method: "POST", @@ -208,6 +116,7 @@ describe("Feishu webhook signed-request e2e", () => { verificationToken: "verify_token", encryptKey: "encrypt_key", }, + monitorFeishuProvider, async (url) => { const response = await fetch(url, { method: "POST", @@ -231,6 +140,7 @@ describe("Feishu webhook signed-request e2e", () => { verificationToken: "verify_token", encryptKey: "encrypt_key", }, + monitorFeishuProvider, async (url) => { const payload = { type: "url_verification", challenge: "challenge-token" }; const response = await fetch(url, { @@ -255,6 +165,7 @@ describe("Feishu webhook signed-request e2e", () => { verificationToken: "verify_token", encryptKey: "encrypt_key", }, + monitorFeishuProvider, async (url) => { const payload = { schema: "2.0", @@ -283,6 +194,7 @@ describe("Feishu webhook signed-request e2e", () => { verificationToken: "verify_token", encryptKey: "encrypt_key", }, + monitorFeishuProvider, async (url) => { const payload = { encrypt: encryptFeishuPayload("encrypt_key", { diff --git a/extensions/feishu/src/monitor.webhook-security.test.ts b/extensions/feishu/src/monitor.webhook-security.test.ts index e9bfa8bf008..957d874cc3a 100644 --- a/extensions/feishu/src/monitor.webhook-security.test.ts +++ b/extensions/feishu/src/monitor.webhook-security.test.ts @@ -1,11 +1,13 @@ -import { createServer } from "node:http"; -import type { AddressInfo } from "node:net"; -import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createFeishuClientMockModule, createFeishuRuntimeMockModule, } from "./monitor.test-mocks.js"; +import { + buildWebhookConfig, + getFreePort, + withRunningWebhookMonitor, +} from "./monitor.webhook.test-helpers.js"; const probeFeishuMock = vi.hoisted(() => vi.fn()); @@ -33,98 +35,6 @@ import { stopFeishuMonitor, } from "./monitor.js"; -async function getFreePort(): Promise { - const server = createServer(); - await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve())); - const address = server.address() as AddressInfo | null; - if (!address) { - throw new Error("missing server address"); - } - await new Promise((resolve) => server.close(() => resolve())); - return address.port; -} - -async function waitUntilServerReady(url: string): Promise { - for (let i = 0; i < 50; i += 1) { - try { - const response = await fetch(url, { method: "GET" }); - if (response.status >= 200 && response.status < 500) { - return; - } - } catch { - // retry - } - await new Promise((resolve) => setTimeout(resolve, 20)); - } - throw new Error(`server did not start: ${url}`); -} - -function buildConfig(params: { - accountId: string; - path: string; - port: number; - verificationToken?: string; - encryptKey?: string; -}): ClawdbotConfig { - return { - channels: { - feishu: { - enabled: true, - accounts: { - [params.accountId]: { - enabled: true, - appId: "cli_test", - appSecret: "secret_test", // pragma: allowlist secret - connectionMode: "webhook", - webhookHost: "127.0.0.1", - webhookPort: params.port, - webhookPath: params.path, - encryptKey: params.encryptKey, - verificationToken: params.verificationToken, - }, - }, - }, - }, - } as ClawdbotConfig; -} - -async function withRunningWebhookMonitor( - params: { - accountId: string; - path: string; - verificationToken: string; - encryptKey: string; - }, - run: (url: string) => Promise, -) { - const port = await getFreePort(); - const cfg = buildConfig({ - accountId: params.accountId, - path: params.path, - port, - encryptKey: params.encryptKey, - verificationToken: params.verificationToken, - }); - - const abortController = new AbortController(); - const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() }; - const monitorPromise = monitorFeishuProvider({ - config: cfg, - runtime, - abortSignal: abortController.signal, - }); - - const url = `http://127.0.0.1:${port}${params.path}`; - await waitUntilServerReady(url); - - try { - await run(url); - } finally { - abortController.abort(); - await monitorPromise; - } -} - afterEach(() => { clearFeishuWebhookRateLimitStateForTest(); stopFeishuMonitor(); @@ -134,7 +44,7 @@ describe("Feishu webhook security hardening", () => { it("rejects webhook mode without verificationToken", async () => { probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" }); - const cfg = buildConfig({ + const cfg = buildWebhookConfig({ accountId: "missing-token", path: "/hook-missing-token", port: await getFreePort(), @@ -148,7 +58,7 @@ describe("Feishu webhook security hardening", () => { it("rejects webhook mode without encryptKey", async () => { probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" }); - const cfg = buildConfig({ + const cfg = buildWebhookConfig({ accountId: "missing-encrypt-key", path: "/hook-missing-encrypt", port: await getFreePort(), @@ -167,6 +77,7 @@ describe("Feishu webhook security hardening", () => { verificationToken: "verify_token", encryptKey: "encrypt_key", }, + monitorFeishuProvider, async (url) => { const response = await fetch(url, { method: "POST", @@ -189,6 +100,7 @@ describe("Feishu webhook security hardening", () => { verificationToken: "verify_token", encryptKey: "encrypt_key", }, + monitorFeishuProvider, async (url) => { let saw429 = false; for (let i = 0; i < 130; i += 1) { diff --git a/extensions/feishu/src/monitor.webhook.test-helpers.ts b/extensions/feishu/src/monitor.webhook.test-helpers.ts new file mode 100644 index 00000000000..b9de2150bd4 --- /dev/null +++ b/extensions/feishu/src/monitor.webhook.test-helpers.ts @@ -0,0 +1,98 @@ +import { createServer } from "node:http"; +import type { AddressInfo } from "node:net"; +import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu"; +import { vi } from "vitest"; +import type { monitorFeishuProvider } from "./monitor.js"; + +export async function getFreePort(): Promise { + const server = createServer(); + await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve())); + const address = server.address() as AddressInfo | null; + if (!address) { + throw new Error("missing server address"); + } + await new Promise((resolve) => server.close(() => resolve())); + return address.port; +} + +async function waitUntilServerReady(url: string): Promise { + for (let i = 0; i < 50; i += 1) { + try { + const response = await fetch(url, { method: "GET" }); + if (response.status >= 200 && response.status < 500) { + return; + } + } catch { + // retry + } + await new Promise((resolve) => setTimeout(resolve, 20)); + } + throw new Error(`server did not start: ${url}`); +} + +export function buildWebhookConfig(params: { + accountId: string; + path: string; + port: number; + verificationToken?: string; + encryptKey?: string; +}): ClawdbotConfig { + return { + channels: { + feishu: { + enabled: true, + accounts: { + [params.accountId]: { + enabled: true, + appId: "cli_test", + appSecret: "secret_test", // pragma: allowlist secret + connectionMode: "webhook", + webhookHost: "127.0.0.1", + webhookPort: params.port, + webhookPath: params.path, + encryptKey: params.encryptKey, + verificationToken: params.verificationToken, + }, + }, + }, + }, + } as ClawdbotConfig; +} + +export async function withRunningWebhookMonitor( + params: { + accountId: string; + path: string; + verificationToken: string; + encryptKey: string; + }, + monitor: typeof monitorFeishuProvider, + run: (url: string) => Promise, +) { + const port = await getFreePort(); + const cfg = buildWebhookConfig({ + accountId: params.accountId, + path: params.path, + port, + encryptKey: params.encryptKey, + verificationToken: params.verificationToken, + }); + + const abortController = new AbortController(); + const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() }; + const monitorPromise = monitor({ + config: cfg, + runtime, + abortSignal: abortController.signal, + }); + + const url = `http://127.0.0.1:${port}${params.path}`; + await waitUntilServerReady(url); + + try { + await run(url); + } finally { + abortController.abort(); + await monitorPromise; + } +}