mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
139 lines
3.7 KiB
TypeScript
139 lines
3.7 KiB
TypeScript
import { EventEmitter } from "node:events";
|
|
import type { IncomingMessage } from "node:http";
|
|
import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { createMockServerResponse } from "../../../src/test-utils/mock-http-response.js";
|
|
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: (error?: Error) => IncomingMessage;
|
|
};
|
|
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;
|
|
return req;
|
|
};
|
|
|
|
void Promise.resolve().then(() => {
|
|
req.emit("data", Buffer.from(JSON.stringify(params.payload), "utf-8"));
|
|
if (!req.destroyed) {
|
|
req.emit("end");
|
|
}
|
|
});
|
|
|
|
return req;
|
|
}
|
|
|
|
const baseAccount = (accountId: string) =>
|
|
({
|
|
accountId,
|
|
enabled: true,
|
|
credentialSource: "none",
|
|
config: {},
|
|
}) as ResolvedGoogleChatAccount;
|
|
|
|
function registerTwoTargets() {
|
|
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,
|
|
});
|
|
|
|
return {
|
|
sinkA,
|
|
sinkB,
|
|
unregister: () => {
|
|
unregisterA();
|
|
unregisterB();
|
|
},
|
|
};
|
|
}
|
|
|
|
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, sinkB, unregister } = registerTwoTargets();
|
|
|
|
try {
|
|
const res = createMockServerResponse();
|
|
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 {
|
|
unregister();
|
|
}
|
|
});
|
|
|
|
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, sinkB, unregister } = registerTwoTargets();
|
|
|
|
try {
|
|
const res = createMockServerResponse();
|
|
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 {
|
|
unregister();
|
|
}
|
|
});
|
|
});
|