import { type AddressInfo } from "node:net"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createNextcloudTalkWebhookServer } from "./monitor.js"; import { generateNextcloudTalkSignature } from "./signature.js"; type WebhookHarness = { webhookUrl: string; stop: () => Promise; }; const cleanupFns: Array<() => Promise> = []; afterEach(async () => { while (cleanupFns.length > 0) { const cleanup = cleanupFns.pop(); if (cleanup) { await cleanup(); } } }); async function startWebhookServer(params: { path: string; isBackendAllowed: (backend: string) => boolean; onMessage: () => void | Promise; }): Promise { const { server, start } = createNextcloudTalkWebhookServer({ port: 0, host: "127.0.0.1", path: params.path, secret: "nextcloud-secret", isBackendAllowed: params.isBackendAllowed, onMessage: params.onMessage, }); await start(); const address = server.address() as AddressInfo | null; if (!address) { throw new Error("missing server address"); } return { webhookUrl: `http://127.0.0.1:${address.port}${params.path}`, stop: () => new Promise((resolve) => { server.close(() => resolve()); }), }; } describe("createNextcloudTalkWebhookServer backend allowlist", () => { it("rejects requests from unexpected backend origins", async () => { const onMessage = vi.fn(async () => {}); const harness = await startWebhookServer({ path: "/nextcloud-backend-check", isBackendAllowed: (backend) => backend === "https://nextcloud.expected", onMessage, }); cleanupFns.push(harness.stop); const payload = { type: "Create", actor: { type: "Person", id: "alice", name: "Alice" }, object: { type: "Note", id: "msg-1", name: "hello", content: "hello", mediaType: "text/plain", }, target: { type: "Collection", id: "room-1", name: "Room 1" }, }; const body = JSON.stringify(payload); const { random, signature } = generateNextcloudTalkSignature({ body, secret: "nextcloud-secret", }); const response = await fetch(harness.webhookUrl, { method: "POST", headers: { "content-type": "application/json", "x-nextcloud-talk-random": random, "x-nextcloud-talk-signature": signature, "x-nextcloud-talk-backend": "https://nextcloud.unexpected", }, body, }); expect(response.status).toBe(401); expect(await response.json()).toEqual({ error: "Invalid backend" }); expect(onMessage).not.toHaveBeenCalled(); }); });