import type { IncomingMessage, ServerResponse } from "node:http"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { dispatchReplyWithBufferedBlockDispatcher, finalizeInboundContextMock, registerPluginHttpRouteMock, resolveAgentRouteMock, } from "./channel.test-mocks.js"; import { makeFormBody, makeReq, makeRes } from "./test-http-utils.js"; type RegisteredRoute = { path: string; accountId: string; handler: (req: IncomingMessage, res: ServerResponse) => Promise; }; const { createSynologyChatPlugin } = await import("./channel.js"); describe("Synology channel wiring integration", () => { beforeEach(() => { registerPluginHttpRouteMock.mockClear(); dispatchReplyWithBufferedBlockDispatcher.mockClear(); finalizeInboundContextMock.mockClear(); resolveAgentRouteMock.mockClear(); }); it("registers real webhook handler with resolved account config and enforces allowlist", async () => { const plugin = createSynologyChatPlugin(); const abortController = new AbortController(); const ctx = { cfg: { channels: { "synology-chat": { enabled: true, accounts: { alerts: { enabled: true, token: "valid-token", incomingUrl: "https://nas.example.com/incoming", webhookPath: "/webhook/synology-alerts", dmPolicy: "allowlist", allowedUserIds: ["456"], }, }, }, }, }, accountId: "alerts", log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, abortSignal: abortController.signal, }; const started = plugin.gateway.startAccount(ctx); expect(registerPluginHttpRouteMock).toHaveBeenCalledTimes(1); const firstCall = registerPluginHttpRouteMock.mock.calls[0]; expect(firstCall).toBeTruthy(); if (!firstCall) throw new Error("Expected registerPluginHttpRoute to be called"); const registered = firstCall[0]; expect(registered.path).toBe("/webhook/synology-alerts"); expect(registered.accountId).toBe("alerts"); const req = makeReq( "POST", makeFormBody({ token: "valid-token", user_id: "123", username: "unauthorized-user", text: "Hello", }), ); const res = makeRes(); await registered.handler(req, res); expect(res._status).toBe(403); expect(res._body).toContain("not authorized"); expect(dispatchReplyWithBufferedBlockDispatcher).not.toHaveBeenCalled(); abortController.abort(); await started; }); it("isolates same user_id across different accounts", async () => { const plugin = createSynologyChatPlugin(); const alphaAbortController = new AbortController(); const betaAbortController = new AbortController(); const cfg = { channels: { "synology-chat": { enabled: true, accounts: { alpha: { enabled: true, token: "token-alpha", incomingUrl: "https://nas.example.com/incoming-alpha", webhookPath: "/webhook/synology-alpha", dmPolicy: "open", }, beta: { enabled: true, token: "token-beta", incomingUrl: "https://nas.example.com/incoming-beta", webhookPath: "/webhook/synology-beta", dmPolicy: "open", }, }, }, }, session: { dmScope: "main", }, }; const alphaStarted = plugin.gateway.startAccount({ cfg, accountId: "alpha", log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, abortSignal: alphaAbortController.signal, }); const betaStarted = plugin.gateway.startAccount({ cfg, accountId: "beta", log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() }, abortSignal: betaAbortController.signal, }); expect(registerPluginHttpRouteMock).toHaveBeenCalledTimes(2); const alphaRoute = registerPluginHttpRouteMock.mock.calls[0]?.[0]; const betaRoute = registerPluginHttpRouteMock.mock.calls[1]?.[0]; if (!alphaRoute || !betaRoute) { throw new Error("Expected both Synology Chat routes to register"); } const alphaReq = makeReq( "POST", makeFormBody({ token: "token-alpha", user_id: "123", username: "alice", text: "alpha secret", }), ); const alphaRes = makeRes(); await alphaRoute.handler(alphaReq, alphaRes); const betaReq = makeReq( "POST", makeFormBody({ token: "token-beta", user_id: "123", username: "bob", text: "beta secret", }), ); const betaRes = makeRes(); await betaRoute.handler(betaReq, betaRes); expect(alphaRes._status).toBe(204); expect(betaRes._status).toBe(204); expect(dispatchReplyWithBufferedBlockDispatcher).toHaveBeenCalledTimes(2); expect(finalizeInboundContextMock).toHaveBeenCalledTimes(2); const alphaCtx = finalizeInboundContextMock.mock.calls[0]?.[0]; const betaCtx = finalizeInboundContextMock.mock.calls[1]?.[0]; expect(alphaCtx).toMatchObject({ AccountId: "alpha", SessionKey: "agent:agent-alpha:synology-chat:alpha:direct:123", }); expect(betaCtx).toMatchObject({ AccountId: "beta", SessionKey: "agent:agent-beta:synology-chat:beta:direct:123", }); alphaAbortController.abort(); betaAbortController.abort(); await alphaStarted; await betaStarted; }); });