openclaw/extensions/zoom/src/webhook.test.ts
Doug Ruby dcd1257edd feat: add Zoom Team Chat plugin
Add a new channel plugin for Zoom Team Chat integration:

- Server-to-Server OAuth with client_credentials grant
- Webhook signature verification (HMAC-SHA256)
- Support for both DMs and channel messages
- Policy controls for DMs and group messaging
- Onboarding wizard for credentials setup
- Connection health probe

Requires a Zoom Team Chat app with `imchat:bot` scope.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:50:21 -08:00

86 lines
2.6 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { verifyZoomWebhook, handleZoomChallenge } from "./webhook.js";
describe("verifyZoomWebhook", () => {
const secret = "test-secret-token";
it("returns true for valid signature", () => {
const timestamp = "1234567890";
const payload = '{"event":"bot_notification"}';
// Pre-computed: v0:1234567890:{"event":"bot_notification"} with secret "test-secret-token"
const crypto = require("node:crypto");
const message = `v0:${timestamp}:${payload}`;
const hash = crypto.createHmac("sha256", secret).update(message).digest("hex");
const signature = `v0=${hash}`;
const result = verifyZoomWebhook({ payload, signature, timestamp, secret });
expect(result).toBe(true);
});
it("returns false for invalid signature", () => {
const result = verifyZoomWebhook({
payload: '{"event":"bot_notification"}',
signature: "v0=invalid",
timestamp: "1234567890",
secret,
});
expect(result).toBe(false);
});
it("returns false for missing signature", () => {
const result = verifyZoomWebhook({
payload: '{"event":"bot_notification"}',
signature: "",
timestamp: "1234567890",
secret,
});
expect(result).toBe(false);
});
it("returns false for missing timestamp", () => {
const result = verifyZoomWebhook({
payload: '{"event":"bot_notification"}',
signature: "v0=abc",
timestamp: "",
secret,
});
expect(result).toBe(false);
});
it("returns false for missing secret", () => {
const result = verifyZoomWebhook({
payload: '{"event":"bot_notification"}',
signature: "v0=abc",
timestamp: "1234567890",
secret: "",
});
expect(result).toBe(false);
});
});
describe("handleZoomChallenge", () => {
it("returns correct encrypted token", () => {
const plainToken = "test-plain-token";
const secret = "test-secret";
const result = handleZoomChallenge({ plainToken, secret });
expect(result.plainToken).toBe(plainToken);
expect(result.encryptedToken).toBeDefined();
expect(result.encryptedToken.length).toBe(64); // SHA256 hex is 64 chars
// Verify it's deterministic
const result2 = handleZoomChallenge({ plainToken, secret });
expect(result2.encryptedToken).toBe(result.encryptedToken);
});
it("produces different tokens for different secrets", () => {
const plainToken = "test-plain-token";
const result1 = handleZoomChallenge({ plainToken, secret: "secret1" });
const result2 = handleZoomChallenge({ plainToken, secret: "secret2" });
expect(result1.encryptedToken).not.toBe(result2.encryptedToken);
});
});