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) => { res.headers[name] = value; }, end: (payload?: string) => { res.body = payload ?? ""; return res; }, } as ServerResponse & { headers: Record; body: string }; return res; } function installSimplePipeline(targets: unknown[]) { withResolvedWebhookRequestPipeline.mockImplementation( async ({ handle, req, res, }: { handle: (input: { targets: unknown[]; req: IncomingMessage; res: ServerResponse; }) => Promise; req: IncomingMessage; res: ServerResponse; }) => 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"); }); });