test(googlechat): cover webhook auth branches

This commit is contained in:
Vincent Koc 2026-03-22 17:54:27 -07:00
parent d38cda5aab
commit bb8e2fceff
1 changed files with 167 additions and 0 deletions

View File

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