Files
openclaw/extensions/synology-chat/src/client.test.ts
Jean-Marc 03586e3d00 feat(channels): add Synology Chat native channel (#23012)
* feat(channels): add Synology Chat native channel

Webhook-based integration with Synology NAS Chat (DSM 7+).
Supports outgoing webhooks, incoming messages, multi-account,
DM policies, rate limiting, and input sanitization.

- HMAC-based constant-time token validation
- Configurable SSL verification (allowInsecureSsl) for self-signed NAS certs
- 54 unit tests across 5 test suites
- Follows the same ChannelPlugin pattern as LINE/Discord/Telegram

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(synology-chat): add pairing, warnings, messaging, agent hints

- Enable media capability (file_url already supported by client)
- Add pairing.notifyApproval to message approved users
- Add security.collectWarnings for missing token/URL, insecure SSL, open DM policy
- Add messaging.normalizeTarget and targetResolver for user ID resolution
- Add directory stubs (self, listPeers, listGroups)
- Add agentPrompt.messageToolHints with Synology Chat formatting guide
- 63 tests (up from 54), all passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 00:09:58 +01:00

105 lines
3.0 KiB
TypeScript

import { EventEmitter } from "node:events";
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock http and https modules before importing the client
vi.mock("node:https", () => {
const mockRequest = vi.fn();
return { default: { request: mockRequest }, request: mockRequest };
});
vi.mock("node:http", () => {
const mockRequest = vi.fn();
return { default: { request: mockRequest }, request: mockRequest };
});
// Import after mocks are set up
const { sendMessage, sendFileUrl } = await import("./client.js");
const https = await import("node:https");
function mockSuccessResponse() {
const httpsRequest = vi.mocked(https.request);
httpsRequest.mockImplementation((_url: any, _opts: any, callback: any) => {
const res = new EventEmitter() as any;
res.statusCode = 200;
process.nextTick(() => {
callback(res);
res.emit("data", Buffer.from('{"success":true}'));
res.emit("end");
});
const req = new EventEmitter() as any;
req.write = vi.fn();
req.end = vi.fn();
req.destroy = vi.fn();
return req;
});
}
function mockFailureResponse(statusCode = 500) {
const httpsRequest = vi.mocked(https.request);
httpsRequest.mockImplementation((_url: any, _opts: any, callback: any) => {
const res = new EventEmitter() as any;
res.statusCode = statusCode;
process.nextTick(() => {
callback(res);
res.emit("data", Buffer.from("error"));
res.emit("end");
});
const req = new EventEmitter() as any;
req.write = vi.fn();
req.end = vi.fn();
req.destroy = vi.fn();
return req;
});
}
describe("sendMessage", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns true on successful send", async () => {
mockSuccessResponse();
const result = await sendMessage("https://nas.example.com/incoming", "Hello");
expect(result).toBe(true);
});
it("returns false on server error after retries", async () => {
mockFailureResponse(500);
const result = await sendMessage("https://nas.example.com/incoming", "Hello");
expect(result).toBe(false);
});
it("includes user_ids when userId is numeric", async () => {
mockSuccessResponse();
await sendMessage("https://nas.example.com/incoming", "Hello", 42);
const httpsRequest = vi.mocked(https.request);
expect(httpsRequest).toHaveBeenCalled();
const callArgs = httpsRequest.mock.calls[0];
expect(callArgs[0]).toBe("https://nas.example.com/incoming");
});
});
describe("sendFileUrl", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("returns true on success", async () => {
mockSuccessResponse();
const result = await sendFileUrl(
"https://nas.example.com/incoming",
"https://example.com/file.png",
);
expect(result).toBe(true);
});
it("returns false on failure", async () => {
mockFailureResponse(500);
const result = await sendFileUrl(
"https://nas.example.com/incoming",
"https://example.com/file.png",
);
expect(result).toBe(false);
});
});