diff --git a/extensions/googlechat/src/monitor-webhook.test.ts b/extensions/googlechat/src/monitor-webhook.test.ts new file mode 100644 index 00000000000..b59d6c026fd --- /dev/null +++ b/extensions/googlechat/src/monitor-webhook.test.ts @@ -0,0 +1,167 @@ +import type { IncomingMessage, ServerResponse } from "node:http"; +import { describe, expect, it, vi } from "vitest"; + +const readJsonWebhookBodyOrReject = vi.hoisted(() => vi.fn()); +const resolveWebhookTargetWithAuthOrReject = vi.hoisted(() => vi.fn()); +const withResolvedWebhookRequestPipeline = vi.hoisted(() => vi.fn()); +const verifyGoogleChatRequest = vi.hoisted(() => vi.fn()); + +vi.mock("../runtime-api.js", () => ({ + readJsonWebhookBodyOrReject, + resolveWebhookTargetWithAuthOrReject, + withResolvedWebhookRequestPipeline, +})); + +vi.mock("./auth.js", () => ({ + verifyGoogleChatRequest, +})); + +function createRequest(authorization?: string): IncomingMessage { + return { + method: "POST", + url: "/googlechat", + headers: { + authorization: authorization ?? "", + "content-type": "application/json", + }, + } as IncomingMessage; +} + +function createResponse() { + const res = { + statusCode: 0, + headers: {} as Record, + body: "", + setHeader(name: string, value: string) { + this.headers[name] = value; + }, + end(payload?: string) { + this.body = payload ?? ""; + return this; + }, + } as unknown as ServerResponse & { + headers: Record; + body: string; + }; + return res; +} + +function installSimplePipeline(targets: unknown[]) { + withResolvedWebhookRequestPipeline.mockImplementation( + async ({ handle, req, res }: Record) => + await handle({ + targets, + req, + res, + }), + ); +} + +describe("googlechat monitor webhook", () => { + it("accepts add-on payloads that carry systemIdToken in the body", async () => { + installSimplePipeline([ + { + account: { + accountId: "default", + config: { appPrincipal: "chat-app" }, + }, + runtime: { error: vi.fn() }, + statusSink: vi.fn(), + audienceType: "app-url", + audience: "https://example.com/googlechat", + }, + ]); + readJsonWebhookBodyOrReject.mockResolvedValue({ + ok: true, + value: { + commonEventObject: { hostApp: "CHAT" }, + authorizationEventObject: { systemIdToken: "addon-token" }, + chat: { + eventTime: "2026-03-22T00:00:00.000Z", + user: { name: "users/123" }, + messagePayload: { + space: { name: "spaces/AAA" }, + message: { name: "spaces/AAA/messages/1", text: "hello" }, + }, + }, + }, + }); + resolveWebhookTargetWithAuthOrReject.mockImplementation(async ({ isMatch, targets }) => { + for (const target of targets) { + if (await isMatch(target)) { + return target; + } + } + return null; + }); + verifyGoogleChatRequest.mockResolvedValue({ ok: true }); + const processEvent = vi.fn(async () => {}); + + const { createGoogleChatWebhookRequestHandler } = await import("./monitor-webhook.js"); + const handler = createGoogleChatWebhookRequestHandler({ + webhookTargets: new Map(), + webhookInFlightLimiter: {} as never, + processEvent, + }); + + const req = createRequest(); + const res = createResponse(); + await expect(handler(req, res)).resolves.toBe(true); + + expect(verifyGoogleChatRequest).toHaveBeenCalledWith( + expect.objectContaining({ + bearer: "addon-token", + expectedAddOnPrincipal: "chat-app", + }), + ); + expect(processEvent).toHaveBeenCalledWith( + expect.objectContaining({ + type: "MESSAGE", + space: { name: "spaces/AAA" }, + }), + expect.anything(), + ); + expect(res.statusCode).toBe(200); + expect(res.headers["Content-Type"]).toBe("application/json"); + }); + + it("rejects missing add-on bearer tokens before dispatch", async () => { + installSimplePipeline([ + { + account: { + accountId: "default", + config: { appPrincipal: "chat-app" }, + }, + runtime: { error: vi.fn() }, + }, + ]); + readJsonWebhookBodyOrReject.mockResolvedValue({ + ok: true, + value: { + commonEventObject: { hostApp: "CHAT" }, + chat: { + messagePayload: { + space: { name: "spaces/AAA" }, + message: { name: "spaces/AAA/messages/1", text: "hello" }, + }, + }, + }, + }); + const processEvent = vi.fn(async () => {}); + + const { createGoogleChatWebhookRequestHandler } = await import("./monitor-webhook.js"); + const handler = createGoogleChatWebhookRequestHandler({ + webhookTargets: new Map(), + webhookInFlightLimiter: {} as never, + processEvent, + }); + + const req = createRequest(); + const res = createResponse(); + await expect(handler(req, res)).resolves.toBe(true); + + expect(processEvent).not.toHaveBeenCalled(); + expect(res.statusCode).toBe(401); + expect(res.body).toBe("unauthorized"); + }); +});