Skip to content

Commit ef6d165

Browse files
committed
feat(tests): add unit tests for MonitoringWebSocketServer and MonitoringHandlers
1 parent 306b724 commit ef6d165

3 files changed

Lines changed: 270 additions & 1 deletion

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { describe, expect, test, jest, beforeEach } from "@jest/globals";
2+
import { MonitoringHandlers } from "../src/handlers/monitoringHandlers";
3+
import { DBType } from "../src/types";
4+
import type { Rpc } from "../src/types";
5+
6+
function createMockRpc(): Rpc & { _responses: any[]; _errors: any[] } {
7+
const responses: any[] = [];
8+
const errors: any[] = [];
9+
10+
return {
11+
sendResponse: jest.fn((id: number | string, payload: any) => {
12+
responses.push({ id, payload });
13+
}),
14+
sendError: jest.fn((id: number | string, err: any) => {
15+
errors.push({ id, err });
16+
}),
17+
_responses: responses,
18+
_errors: errors,
19+
};
20+
}
21+
22+
function createMockLogger(): any {
23+
return {
24+
info: jest.fn(),
25+
warn: jest.fn(),
26+
error: jest.fn(),
27+
debug: jest.fn(),
28+
child: jest.fn().mockReturnThis(),
29+
};
30+
}
31+
32+
function createMockDbService() {
33+
return {
34+
getDatabaseConnection: jest.fn(),
35+
};
36+
}
37+
38+
function createMockMonitoringService() {
39+
return {
40+
getSnapshot: jest.fn(),
41+
};
42+
}
43+
44+
describe("MonitoringHandlers", () => {
45+
let rpc: ReturnType<typeof createMockRpc>;
46+
let logger: any;
47+
let dbService: ReturnType<typeof createMockDbService>;
48+
let monitoringService: ReturnType<typeof createMockMonitoringService>;
49+
let handlers: MonitoringHandlers;
50+
51+
beforeEach(() => {
52+
rpc = createMockRpc();
53+
logger = createMockLogger();
54+
dbService = createMockDbService();
55+
monitoringService = createMockMonitoringService();
56+
handlers = new MonitoringHandlers(rpc, logger, dbService as any, monitoringService as any);
57+
});
58+
59+
test("returns BAD_REQUEST when db id is missing", async () => {
60+
await handlers.handleGetSnapshot({}, 1);
61+
62+
expect(rpc.sendError).toHaveBeenCalledWith(1, {
63+
code: "BAD_REQUEST",
64+
message: "Missing id",
65+
});
66+
expect(dbService.getDatabaseConnection).not.toHaveBeenCalled();
67+
});
68+
69+
test("returns monitoring snapshot for a valid database", async () => {
70+
const conn = { connection: true };
71+
const snapshot = {
72+
databaseType: DBType.POSTGRES,
73+
sampledAt: "2026-05-16T00:00:00.000Z",
74+
health: { ok: true, latencyMs: 12 },
75+
connections: { active: 2, max: 100, usagePct: 2 },
76+
throughput: { qps: 3.5, totalQueries: 1000 },
77+
cacheHitRatio: 99.2,
78+
activeQueries: [],
79+
};
80+
81+
(dbService.getDatabaseConnection as any).mockResolvedValue({ conn, dbType: DBType.POSTGRES });
82+
(monitoringService.getSnapshot as any).mockResolvedValue(snapshot);
83+
84+
await handlers.handleGetSnapshot({ id: "db-1" }, 2);
85+
86+
expect(dbService.getDatabaseConnection).toHaveBeenCalledWith("db-1");
87+
expect(monitoringService.getSnapshot).toHaveBeenCalledWith("db-1", conn, DBType.POSTGRES);
88+
expect(rpc.sendResponse).toHaveBeenCalledWith(2, {
89+
ok: true,
90+
data: snapshot,
91+
});
92+
});
93+
94+
test("returns MONITORING_ERROR when snapshot generation fails", async () => {
95+
(dbService.getDatabaseConnection as any).mockResolvedValue({ conn: {}, dbType: DBType.POSTGRES });
96+
(monitoringService.getSnapshot as any).mockRejectedValue(new Error("snapshot failed"));
97+
98+
await handlers.handleGetSnapshot({ id: "db-1" }, 3);
99+
100+
expect(logger.error).toHaveBeenCalled();
101+
expect(rpc.sendError).toHaveBeenCalledWith(3, {
102+
code: "MONITORING_ERROR",
103+
message: "snapshot failed",
104+
});
105+
});
106+
});
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { describe, expect, test, jest, beforeEach, afterEach } from "@jest/globals";
2+
import { MonitoringWebSocketServer } from "../src/services/monitoringWebSocketServer";
3+
import { DBType } from "../src/types";
4+
5+
const mockWebSocketServerInstances: any[] = [];
6+
7+
jest.mock("ws", () => ({
8+
WebSocketServer: jest.fn().mockImplementation((options) => {
9+
const listeners: Record<string, (...args: any[]) => void> = {};
10+
11+
const instance = {
12+
options,
13+
on: jest.fn((event: string, handler: (...args: any[]) => void) => {
14+
listeners[event] = handler;
15+
}),
16+
close: jest.fn(),
17+
address: jest.fn(() => ({ port: 4567 })),
18+
emit: async (event: string, ...args: any[]) => {
19+
const handler = listeners[event];
20+
if (!handler) return undefined;
21+
return handler(...args);
22+
},
23+
_listeners: listeners,
24+
};
25+
26+
mockWebSocketServerInstances.push(instance);
27+
return instance;
28+
}),
29+
}));
30+
31+
function createMockLogger(): any {
32+
return {
33+
info: jest.fn(),
34+
warn: jest.fn(),
35+
error: jest.fn(),
36+
debug: jest.fn(),
37+
child: jest.fn().mockReturnThis(),
38+
};
39+
}
40+
41+
function createMockDbService() {
42+
return {
43+
getDatabaseConnection: jest.fn(),
44+
};
45+
}
46+
47+
function createMockMonitoringService() {
48+
return {
49+
getSnapshot: jest.fn(),
50+
};
51+
}
52+
53+
function createMockSocket() {
54+
const listeners: Record<string, (...args: any[]) => void> = {};
55+
return {
56+
readyState: 1,
57+
send: jest.fn(),
58+
close: jest.fn(),
59+
on: jest.fn((event: string, handler: (...args: any[]) => void) => {
60+
listeners[event] = handler;
61+
}),
62+
emit: async (event: string, ...args: any[]) => {
63+
const handler = listeners[event];
64+
if (!handler) return undefined;
65+
return handler(...args);
66+
},
67+
_listeners: listeners,
68+
};
69+
}
70+
71+
describe("MonitoringWebSocketServer", () => {
72+
let logger: any;
73+
let dbService: ReturnType<typeof createMockDbService>;
74+
let monitoringService: ReturnType<typeof createMockMonitoringService>;
75+
let server: MonitoringWebSocketServer;
76+
let setIntervalSpy: jest.SpiedFunction<typeof setInterval>;
77+
let clearIntervalSpy: jest.SpiedFunction<typeof clearInterval>;
78+
79+
beforeEach(() => {
80+
mockWebSocketServerInstances.length = 0;
81+
logger = createMockLogger();
82+
dbService = createMockDbService();
83+
monitoringService = createMockMonitoringService();
84+
server = new MonitoringWebSocketServer(dbService as any, monitoringService as any, logger);
85+
setIntervalSpy = jest.spyOn(global, "setInterval").mockReturnValue(123 as any);
86+
clearIntervalSpy = jest.spyOn(global, "clearInterval").mockImplementation(() => undefined);
87+
});
88+
89+
afterEach(() => {
90+
jest.restoreAllMocks();
91+
server.close();
92+
});
93+
94+
test("starts once and exposes websocket info after listening", () => {
95+
expect(() => server.getInfo()).toThrow("Monitoring WebSocket server is not ready");
96+
97+
server.start();
98+
server.start();
99+
100+
expect(mockWebSocketServerInstances).toHaveLength(1);
101+
expect(mockWebSocketServerInstances[0].options).toEqual({
102+
host: "127.0.0.1",
103+
port: 0,
104+
path: "/monitoring",
105+
});
106+
107+
mockWebSocketServerInstances[0].emit("listening");
108+
109+
expect(logger.info).toHaveBeenCalledWith({ port: 4567 }, "Monitoring WebSocket server started");
110+
expect(server.getInfo()).toEqual({
111+
url: "ws://127.0.0.1:4567/monitoring",
112+
intervalMs: 5000,
113+
});
114+
});
115+
116+
test("closes unsupported websocket connections immediately", async () => {
117+
server.start();
118+
const wsServer = mockWebSocketServerInstances[0];
119+
const socket = createMockSocket();
120+
121+
(dbService.getDatabaseConnection as any).mockResolvedValue({ conn: { id: "conn" }, dbType: DBType.SQLITE });
122+
123+
await wsServer.emit("connection", socket, { url: "/monitoring?dbId=db-1" } as any);
124+
125+
expect(dbService.getDatabaseConnection).toHaveBeenCalledWith("db-1");
126+
expect(socket.send).toHaveBeenCalledWith(
127+
JSON.stringify({
128+
type: "unsupported",
129+
message: "Monitoring is not supported for sqlite",
130+
})
131+
);
132+
expect(socket.close).toHaveBeenCalledWith(1008, "Unsupported database type");
133+
});
134+
135+
test("sends snapshots over websocket and enforces minimum interval", async () => {
136+
server.start();
137+
const wsServer = mockWebSocketServerInstances[0];
138+
const socket = createMockSocket();
139+
140+
(dbService.getDatabaseConnection as any).mockResolvedValue({ conn: { id: "conn" }, dbType: DBType.POSTGRES });
141+
(monitoringService.getSnapshot as any).mockResolvedValue({
142+
databaseType: DBType.POSTGRES,
143+
sampledAt: "2026-05-16T00:00:00.000Z",
144+
health: { ok: true, latencyMs: 11 },
145+
connections: { active: 1, max: 50, usagePct: 2 },
146+
throughput: { qps: 5, totalQueries: 100 },
147+
cacheHitRatio: 98.5,
148+
activeQueries: [],
149+
});
150+
151+
await wsServer.emit("connection", socket, { url: "/monitoring?dbId=db-1&intervalMs=1000" } as any);
152+
153+
expect(dbService.getDatabaseConnection).toHaveBeenCalledWith("db-1");
154+
expect(monitoringService.getSnapshot).toHaveBeenCalledWith("db-1", { id: "conn" }, DBType.POSTGRES);
155+
expect(JSON.parse(socket.send.mock.calls[0][0] as string)).toMatchObject({
156+
type: "snapshot",
157+
data: expect.objectContaining({ databaseType: DBType.POSTGRES }),
158+
});
159+
expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 2000);
160+
expect(clearIntervalSpy).not.toHaveBeenCalled();
161+
});
162+
});

bridge/src/services/monitoringWebSocketServer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ export class MonitoringWebSocketServer {
4444
});
4545

4646
this.server.on("connection", (socket: WebSocketClient, request: IncomingMessage) => {
47-
this.handleConnection(socket, request);
47+
// return the promise from handler so test harness can await completion
48+
return this.handleConnection(socket, request);
4849
});
4950

5051
this.server.on("error", (error: Error) => {

0 commit comments

Comments
 (0)