import type { IncomingMessage, ServerResponse } from "node:http"; import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk"; import { EventEmitter } from "node:events"; import { describe, expect, it, vi } from "vitest"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import { verifyGoogleChatRequest } from "./auth.js"; import { handleGoogleChatWebhookRequest, registerGoogleChatWebhookTarget } from "./monitor.js"; vi.mock("./auth.js", () => ({ verifyGoogleChatRequest: vi.fn(), })); function createWebhookRequest(params: { authorization?: string; payload: unknown; path?: string; }): IncomingMessage { const req = new EventEmitter() as IncomingMessage & { destroyed?: boolean; destroy: () => void }; req.method = "POST"; req.url = params.path ?? "/googlechat"; req.headers = { authorization: params.authorization ?? "", "content-type": "application/json", }; req.destroyed = false; req.destroy = () => { req.destroyed = true; }; void Promise.resolve().then(() => { req.emit("data", Buffer.from(JSON.stringify(params.payload), "utf-8")); if (!req.destroyed) { req.emit("end"); } }); return req; } function createWebhookResponse(): ServerResponse & { body?: string } { const headers: Record = {}; const res = { headersSent: false, statusCode: 200, setHeader: (key: string, value: string) => { headers[key.toLowerCase()] = value; return res; }, end: (body?: string) => { res.headersSent = true; res.body = body; return res; }, } as unknown as ServerResponse & { body?: string }; return res; } const baseAccount = (accountId: string) => ({ accountId, enabled: true, credentialSource: "none", config: {}, }) as ResolvedGoogleChatAccount; describe("Google Chat webhook routing", () => { it("rejects ambiguous routing when multiple targets on the same path verify successfully", async () => { vi.mocked(verifyGoogleChatRequest).mockResolvedValue({ ok: true }); const sinkA = vi.fn(); const sinkB = vi.fn(); const core = {} as PluginRuntime; const config = {} as OpenClawConfig; const unregisterA = registerGoogleChatWebhookTarget({ account: baseAccount("A"), config, runtime: {}, core, path: "/googlechat", statusSink: sinkA, mediaMaxMb: 5, }); const unregisterB = registerGoogleChatWebhookTarget({ account: baseAccount("B"), config, runtime: {}, core, path: "/googlechat", statusSink: sinkB, mediaMaxMb: 5, }); try { const res = createWebhookResponse(); const handled = await handleGoogleChatWebhookRequest( createWebhookRequest({ authorization: "Bearer test-token", payload: { type: "ADDED_TO_SPACE", space: { name: "spaces/AAA" } }, }), res, ); expect(handled).toBe(true); expect(res.statusCode).toBe(401); expect(sinkA).not.toHaveBeenCalled(); expect(sinkB).not.toHaveBeenCalled(); } finally { unregisterA(); unregisterB(); } }); it("routes to the single verified target when earlier targets fail verification", async () => { vi.mocked(verifyGoogleChatRequest) .mockResolvedValueOnce({ ok: false, reason: "invalid" }) .mockResolvedValueOnce({ ok: true }); const sinkA = vi.fn(); const sinkB = vi.fn(); const core = {} as PluginRuntime; const config = {} as OpenClawConfig; const unregisterA = registerGoogleChatWebhookTarget({ account: baseAccount("A"), config, runtime: {}, core, path: "/googlechat", statusSink: sinkA, mediaMaxMb: 5, }); const unregisterB = registerGoogleChatWebhookTarget({ account: baseAccount("B"), config, runtime: {}, core, path: "/googlechat", statusSink: sinkB, mediaMaxMb: 5, }); try { const res = createWebhookResponse(); const handled = await handleGoogleChatWebhookRequest( createWebhookRequest({ authorization: "Bearer test-token", payload: { type: "ADDED_TO_SPACE", space: { name: "spaces/BBB" } }, }), res, ); expect(handled).toBe(true); expect(res.statusCode).toBe(200); expect(sinkA).not.toHaveBeenCalled(); expect(sinkB).toHaveBeenCalledTimes(1); } finally { unregisterA(); unregisterB(); } }); });