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>
86 lines
2.6 KiB
TypeScript
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);
|
|
});
|
|
});
|