Skip to content

Commit 2bee92f

Browse files
committed
chore(test): add end-to-end tests
1 parent 67c6584 commit 2bee92f

File tree

2 files changed

+273
-1
lines changed

2 files changed

+273
-1
lines changed

router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import type {
1616
WebSocketRouterOptions,
1717
} from "./types";
1818

19-
export class WebSocketRouter<Metadata> {
19+
export class WebSocketRouter<Metadata extends Record<string, unknown> = {}> {
2020
private readonly server: Server;
2121
private readonly handlers = new WebSocketHandlers();
2222

test/server.test.ts

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/* SPDX-FileCopyrightText: 2025-present Kriasoft */
2+
/* SPDX-License-Identifier: MIT */
3+
4+
import {
5+
afterEach,
6+
beforeEach,
7+
describe,
8+
expect,
9+
it,
10+
mock,
11+
spyOn,
12+
} from "bun:test";
13+
import { z } from "zod";
14+
import { WebSocketRouter } from "../router";
15+
import { messageSchema } from "../schema";
16+
17+
// Mock console methods to prevent noise during tests
18+
const originalConsoleLog = console.log;
19+
const originalConsoleWarn = console.warn;
20+
const originalConsoleError = console.error;
21+
22+
beforeEach(() => {
23+
console.log = mock(() => {});
24+
console.warn = mock(() => {});
25+
console.error = mock(() => {});
26+
});
27+
28+
afterEach(() => {
29+
console.log = originalConsoleLog;
30+
console.warn = originalConsoleWarn;
31+
console.error = originalConsoleError;
32+
});
33+
34+
// Test message schemas
35+
const Ping = messageSchema("PING", {
36+
message: z.string(),
37+
});
38+
39+
const Pong = messageSchema("PONG", {
40+
message: z.string(),
41+
timestamp: z.number(),
42+
});
43+
44+
const Error = messageSchema("ERROR", {
45+
code: z.number(),
46+
message: z.string(),
47+
});
48+
49+
describe("WebSocketServer E2E", () => {
50+
let server: ReturnType<typeof Bun.serve>;
51+
let ws: WebSocketRouter;
52+
let port: number;
53+
54+
beforeEach(() => {
55+
// Use a random port for each test to avoid conflicts
56+
port = 50000 + Math.floor(Math.random() * 10000);
57+
58+
// Create a new router for each test
59+
ws = new WebSocketRouter();
60+
61+
// Set up message handlers
62+
ws.onMessage(Ping, (ctx) => {
63+
// Echo back a PONG with the same message and add a timestamp
64+
ctx.send(Pong, {
65+
message: ctx.payload.message,
66+
timestamp: Date.now(),
67+
});
68+
});
69+
70+
// Add an error message handler
71+
ws.onMessage(Error, (ctx) => {
72+
// Just for handling error messages in tests
73+
});
74+
75+
// Set up open handler
76+
const openHandlerMock = mock((ctx) => {
77+
// Optional: send a welcome message
78+
// ctx.send(...);
79+
});
80+
ws.onOpen(openHandlerMock);
81+
82+
// Set up close handler
83+
const closeHandlerMock = mock((ctx) => {});
84+
ws.onClose(closeHandlerMock);
85+
86+
// Start the server
87+
server = Bun.serve({
88+
port,
89+
90+
fetch(req, server) {
91+
const url = new URL(req.url);
92+
if (url.pathname === "/ws") {
93+
return ws.upgrade(req, { server });
94+
}
95+
return new Response("Not Found", { status: 404 });
96+
},
97+
98+
websocket: ws.websocket,
99+
});
100+
});
101+
102+
afterEach(() => {
103+
// Shutdown the server after each test
104+
server.stop();
105+
});
106+
107+
it("should establish a WebSocket connection and exchange messages", async () => {
108+
// Wait a bit to ensure server is ready
109+
await new Promise((resolve) => setTimeout(resolve, 100));
110+
111+
// Connect to the server
112+
const socket = new WebSocket(`ws://localhost:${port}/ws`);
113+
114+
// Keep track of messages received by the client
115+
const receivedMessages: any[] = [];
116+
117+
// Set up message handler
118+
socket.addEventListener("message", (event) => {
119+
receivedMessages.push(JSON.parse(event.data as string));
120+
});
121+
122+
// Wait for the connection to open
123+
await new Promise<void>((resolve) => {
124+
socket.addEventListener("open", () => resolve());
125+
});
126+
127+
// Send a PING message
128+
const pingMessage = {
129+
type: "PING",
130+
meta: { clientId: "test-client" },
131+
payload: { message: "Hello Server!" },
132+
};
133+
134+
socket.send(JSON.stringify(pingMessage));
135+
136+
// Wait for a response (PONG)
137+
await new Promise<void>((resolve) => {
138+
const checkInterval = setInterval(() => {
139+
if (receivedMessages.length > 0) {
140+
clearInterval(checkInterval);
141+
resolve();
142+
}
143+
}, 50);
144+
});
145+
146+
// Check that we received the expected PONG message
147+
expect(receivedMessages.length).toBe(1);
148+
expect(receivedMessages[0].type).toBe("PONG");
149+
expect(receivedMessages[0].payload.message).toBe("Hello Server!");
150+
expect(receivedMessages[0].payload.timestamp).toBeGreaterThan(0);
151+
152+
// Clean up
153+
socket.close();
154+
});
155+
156+
it("should handle multiple clients simultaneously", async () => {
157+
// Wait for server to be ready
158+
await new Promise((resolve) => setTimeout(resolve, 100));
159+
160+
// Connect multiple clients
161+
const clients = await Promise.all(
162+
Array.from({ length: 3 }, async (_, i) => {
163+
const socket = new WebSocket(`ws://localhost:${port}/ws`);
164+
const messages: any[] = [];
165+
166+
socket.addEventListener("message", (event) => {
167+
messages.push(JSON.parse(event.data as string));
168+
});
169+
170+
// Wait for connection to open
171+
await new Promise<void>((resolve) => {
172+
socket.addEventListener("open", () => resolve());
173+
});
174+
175+
return { socket, messages, id: `client-${i}` };
176+
})
177+
);
178+
179+
// Each client sends a message
180+
clients.forEach((client) => {
181+
const pingMessage = {
182+
type: "PING",
183+
meta: { clientId: client.id },
184+
payload: { message: `Hello from ${client.id}` },
185+
};
186+
client.socket.send(JSON.stringify(pingMessage));
187+
});
188+
189+
// Wait for all clients to receive responses
190+
await new Promise<void>((resolve) => {
191+
const checkInterval = setInterval(() => {
192+
if (clients.every((client) => client.messages.length > 0)) {
193+
clearInterval(checkInterval);
194+
resolve();
195+
}
196+
}, 50);
197+
});
198+
199+
// Verify each client received the correct response
200+
clients.forEach((client) => {
201+
expect(client.messages.length).toBe(1);
202+
expect(client.messages[0].type).toBe("PONG");
203+
expect(client.messages[0].payload.message).toBe(
204+
`Hello from ${client.id}`
205+
);
206+
});
207+
208+
// Clean up
209+
clients.forEach((client) => client.socket.close());
210+
});
211+
212+
it("should handle invalid message format gracefully", async () => {
213+
// Connect to the server
214+
await new Promise((resolve) => setTimeout(resolve, 100));
215+
const socket = new WebSocket(`ws://localhost:${port}/ws`);
216+
217+
const receivedMessages: any[] = [];
218+
socket.addEventListener("message", (event) => {
219+
receivedMessages.push(JSON.parse(event.data as string));
220+
});
221+
222+
// Wait for connection to open
223+
await new Promise<void>((resolve) => {
224+
socket.addEventListener("open", () => resolve());
225+
});
226+
227+
// Monitor console.error calls
228+
const errorSpy = spyOn(console, "error");
229+
230+
// Send an invalid message (not JSON)
231+
socket.send("This is not JSON");
232+
233+
// Send an invalid message (JSON but wrong format)
234+
socket.send(JSON.stringify({ notAValidMessage: true }));
235+
236+
// Wait a bit to ensure messages are processed
237+
await new Promise((resolve) => setTimeout(resolve, 200));
238+
239+
// Verify error handling
240+
expect(errorSpy).toHaveBeenCalled();
241+
242+
// Clean up
243+
socket.close();
244+
});
245+
246+
it("should handle client disconnection properly", async () => {
247+
// Create a mock for the close handler
248+
const closeHandlerMock = mock(() => {});
249+
250+
// Register our mock as a close handler
251+
ws.onClose(closeHandlerMock);
252+
253+
// Connect to the server
254+
await new Promise((resolve) => setTimeout(resolve, 100));
255+
const socket = new WebSocket(`ws://localhost:${port}/ws`);
256+
257+
// Wait for connection to open
258+
await new Promise<void>((resolve) => {
259+
socket.addEventListener("open", () => resolve());
260+
});
261+
262+
// Close the connection from the client side
263+
socket.close(1000, "Normal closure");
264+
265+
// Wait for close event to be processed
266+
// Need a bit more time to ensure the close handler is called
267+
await new Promise((resolve) => setTimeout(resolve, 300));
268+
269+
// Verify close handler was called
270+
expect(closeHandlerMock).toHaveBeenCalled();
271+
});
272+
});

0 commit comments

Comments
 (0)