@onebun/core 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,479 @@
1
+ /**
2
+ * Unit tests for ws-base-gateway.ts
3
+ */
4
+
5
+ import {
6
+ describe,
7
+ it,
8
+ expect,
9
+ beforeEach,
10
+ mock,
11
+ } from 'bun:test';
12
+
13
+ import type { WsClientData } from './ws.types';
14
+
15
+ import { BaseWebSocketGateway, _resetClientSocketsForTesting } from './ws-base-gateway';
16
+ import { InMemoryWsStorage } from './ws-storage-memory';
17
+
18
+ // Mock WebSocket
19
+ interface MockWebSocket {
20
+ data: WsClientData;
21
+ send: ReturnType<typeof mock>;
22
+ close: ReturnType<typeof mock>;
23
+ subscribe: ReturnType<typeof mock>;
24
+ unsubscribe: ReturnType<typeof mock>;
25
+ }
26
+
27
+ function createMockSocket(clientData: WsClientData): MockWebSocket {
28
+ return {
29
+ data: clientData,
30
+ send: mock(() => undefined),
31
+ close: mock(() => undefined),
32
+ subscribe: mock(() => undefined),
33
+ unsubscribe: mock(() => undefined),
34
+ };
35
+ }
36
+
37
+ function createClientData(id: string, rooms: string[] = []): WsClientData {
38
+ return {
39
+ id,
40
+ rooms,
41
+ connectedAt: Date.now(),
42
+ auth: null,
43
+ metadata: {},
44
+ };
45
+ }
46
+
47
+ // Concrete implementation for testing
48
+ class TestGateway extends BaseWebSocketGateway {
49
+ // Expose internal methods for testing
50
+ public exposeRegisterSocket(clientId: string, socket: MockWebSocket): void {
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ (this as any)._registerSocket(clientId, socket);
53
+ }
54
+
55
+ public exposeUnregisterSocket(clientId: string): void {
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
+ (this as any)._unregisterSocket(clientId);
58
+ }
59
+
60
+ public exposeInitialize(storage: InMemoryWsStorage, server: unknown): void {
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ (this as any)._initialize(storage, server);
63
+ }
64
+
65
+ public exposeGetSocket(clientId: string): MockWebSocket | undefined {
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ return (this as any).getSocket(clientId);
68
+ }
69
+ }
70
+
71
+ describe('BaseWebSocketGateway', () => {
72
+ let gateway: TestGateway;
73
+ let storage: InMemoryWsStorage;
74
+
75
+ beforeEach(() => {
76
+ _resetClientSocketsForTesting();
77
+ gateway = new TestGateway();
78
+ storage = new InMemoryWsStorage();
79
+ gateway.exposeInitialize(storage, {});
80
+ });
81
+
82
+ describe('socket registration', () => {
83
+ it('should register a socket', () => {
84
+ const clientData = createClientData('client1');
85
+ const socket = createMockSocket(clientData);
86
+
87
+ gateway.exposeRegisterSocket('client1', socket);
88
+
89
+ expect(gateway.exposeGetSocket('client1')).toBe(socket);
90
+ });
91
+
92
+ it('should unregister a socket', () => {
93
+ const clientData = createClientData('client1');
94
+ const socket = createMockSocket(clientData);
95
+
96
+ gateway.exposeRegisterSocket('client1', socket);
97
+ gateway.exposeUnregisterSocket('client1');
98
+
99
+ expect(gateway.exposeGetSocket('client1')).toBeUndefined();
100
+ });
101
+ });
102
+
103
+ describe('clients getter', () => {
104
+ it('should return empty map when no clients', () => {
105
+ expect(gateway.clients.size).toBe(0);
106
+ });
107
+
108
+ it('should return map of connected clients', () => {
109
+ const client1 = createClientData('client1');
110
+ const client2 = createClientData('client2');
111
+
112
+ gateway.exposeRegisterSocket('client1', createMockSocket(client1));
113
+ gateway.exposeRegisterSocket('client2', createMockSocket(client2));
114
+
115
+ expect(gateway.clients.size).toBe(2);
116
+ expect(gateway.clients.get('client1')).toEqual(client1);
117
+ expect(gateway.clients.get('client2')).toEqual(client2);
118
+ });
119
+ });
120
+
121
+ describe('getClient', () => {
122
+ it('should return undefined when storage not initialized', async () => {
123
+ const uninitializedGateway = new TestGateway();
124
+ const result = await uninitializedGateway.getClient('client1');
125
+ expect(result).toBeUndefined();
126
+ });
127
+
128
+ it('should return client from storage', async () => {
129
+ const clientData = createClientData('client1');
130
+ await storage.addClient(clientData);
131
+
132
+ const result = await gateway.getClient('client1');
133
+ expect(result).toEqual(clientData);
134
+ });
135
+
136
+ it('should return undefined for non-existent client', async () => {
137
+ const result = await gateway.getClient('nonexistent');
138
+ expect(result).toBeUndefined();
139
+ });
140
+ });
141
+
142
+ describe('getRoom', () => {
143
+ it('should return undefined when storage not initialized', async () => {
144
+ const uninitializedGateway = new TestGateway();
145
+ const result = await uninitializedGateway.getRoom('room1');
146
+ expect(result).toBeUndefined();
147
+ });
148
+
149
+ it('should return room from storage', async () => {
150
+ // Rooms are created automatically when a client joins
151
+ const client = createClientData('client1');
152
+ await storage.addClient(client);
153
+ await storage.addClientToRoom('client1', 'room1');
154
+
155
+ const result = await gateway.getRoom('room1');
156
+ expect(result?.name).toBe('room1');
157
+ });
158
+
159
+ it('should return undefined for non-existent room', async () => {
160
+ const result = await gateway.getRoom('nonexistent');
161
+ expect(result).toBeUndefined();
162
+ });
163
+ });
164
+
165
+ describe('getClientsByRoom', () => {
166
+ it('should return empty array when storage not initialized', async () => {
167
+ const uninitializedGateway = new TestGateway();
168
+ const result = await uninitializedGateway.getClientsByRoom('room1');
169
+ expect(result).toEqual([]);
170
+ });
171
+
172
+ it('should return clients in room', async () => {
173
+ const client1 = createClientData('client1');
174
+ const client2 = createClientData('client2');
175
+
176
+ await storage.addClient(client1);
177
+ await storage.addClient(client2);
178
+ await storage.addClientToRoom('client1', 'room1');
179
+ await storage.addClientToRoom('client2', 'room1');
180
+
181
+ const result = await gateway.getClientsByRoom('room1');
182
+ expect(result.length).toBe(2);
183
+ expect(result.map(c => c.id).sort()).toEqual(['client1', 'client2']);
184
+ });
185
+ });
186
+
187
+ describe('emit', () => {
188
+ it('should send message to specific client', () => {
189
+ const clientData = createClientData('client1');
190
+ const socket = createMockSocket(clientData);
191
+
192
+ gateway.exposeRegisterSocket('client1', socket);
193
+ gateway.emit('client1', 'test:event', { foo: 'bar' });
194
+
195
+ expect(socket.send).toHaveBeenCalledTimes(1);
196
+ const sentMessage = JSON.parse(socket.send.mock.calls[0][0]);
197
+ expect(sentMessage).toEqual({ event: 'test:event', data: { foo: 'bar' } });
198
+ });
199
+
200
+ it('should not throw for non-existent client', () => {
201
+ expect(() => gateway.emit('nonexistent', 'test:event', {})).not.toThrow();
202
+ });
203
+ });
204
+
205
+ describe('broadcast', () => {
206
+ it('should send message to all clients', () => {
207
+ const client1 = createClientData('client1');
208
+ const client2 = createClientData('client2');
209
+ const socket1 = createMockSocket(client1);
210
+ const socket2 = createMockSocket(client2);
211
+
212
+ gateway.exposeRegisterSocket('client1', socket1);
213
+ gateway.exposeRegisterSocket('client2', socket2);
214
+ gateway.broadcast('test:event', { message: 'hello' });
215
+
216
+ expect(socket1.send).toHaveBeenCalledTimes(1);
217
+ expect(socket2.send).toHaveBeenCalledTimes(1);
218
+
219
+ const sent1 = JSON.parse(socket1.send.mock.calls[0][0]);
220
+ const sent2 = JSON.parse(socket2.send.mock.calls[0][0]);
221
+ expect(sent1).toEqual({ event: 'test:event', data: { message: 'hello' } });
222
+ expect(sent2).toEqual({ event: 'test:event', data: { message: 'hello' } });
223
+ });
224
+
225
+ it('should exclude specified clients', () => {
226
+ const client1 = createClientData('client1');
227
+ const client2 = createClientData('client2');
228
+ const socket1 = createMockSocket(client1);
229
+ const socket2 = createMockSocket(client2);
230
+
231
+ gateway.exposeRegisterSocket('client1', socket1);
232
+ gateway.exposeRegisterSocket('client2', socket2);
233
+ gateway.broadcast('test:event', { message: 'hello' }, ['client1']);
234
+
235
+ expect(socket1.send).not.toHaveBeenCalled();
236
+ expect(socket2.send).toHaveBeenCalledTimes(1);
237
+ });
238
+ });
239
+
240
+ describe('emitToRoom', () => {
241
+ it('should send message to clients in room', async () => {
242
+ const client1 = createClientData('client1');
243
+ const client2 = createClientData('client2');
244
+ const client3 = createClientData('client3');
245
+ const socket1 = createMockSocket(client1);
246
+ const socket2 = createMockSocket(client2);
247
+ const socket3 = createMockSocket(client3);
248
+
249
+ gateway.exposeRegisterSocket('client1', socket1);
250
+ gateway.exposeRegisterSocket('client2', socket2);
251
+ gateway.exposeRegisterSocket('client3', socket3);
252
+
253
+ await storage.addClient(client1);
254
+ await storage.addClient(client2);
255
+ await storage.addClient(client3);
256
+ await storage.addClientToRoom('client1', 'room1');
257
+ await storage.addClientToRoom('client2', 'room1');
258
+ // client3 is not in room1
259
+
260
+ gateway.emitToRoom('room1', 'room:message', { text: 'hi' });
261
+
262
+ // Need a small delay for async operation
263
+ await new Promise(resolve => setTimeout(resolve, 10));
264
+
265
+ expect(socket1.send).toHaveBeenCalled();
266
+ expect(socket2.send).toHaveBeenCalled();
267
+ expect(socket3.send).not.toHaveBeenCalled();
268
+ });
269
+
270
+ it('should exclude specified clients in room', async () => {
271
+ const client1 = createClientData('client1');
272
+ const client2 = createClientData('client2');
273
+ const socket1 = createMockSocket(client1);
274
+ const socket2 = createMockSocket(client2);
275
+
276
+ gateway.exposeRegisterSocket('client1', socket1);
277
+ gateway.exposeRegisterSocket('client2', socket2);
278
+
279
+ await storage.addClient(client1);
280
+ await storage.addClient(client2);
281
+ await storage.addClientToRoom('client1', 'room1');
282
+ await storage.addClientToRoom('client2', 'room1');
283
+
284
+ gateway.emitToRoom('room1', 'room:message', { text: 'hi' }, ['client1']);
285
+
286
+ await new Promise(resolve => setTimeout(resolve, 10));
287
+
288
+ expect(socket1.send).not.toHaveBeenCalled();
289
+ expect(socket2.send).toHaveBeenCalled();
290
+ });
291
+ });
292
+
293
+ describe('emitToRooms', () => {
294
+ it('should send to clients in multiple rooms without duplicates', async () => {
295
+ const client1 = createClientData('client1');
296
+ const client2 = createClientData('client2');
297
+ const socket1 = createMockSocket(client1);
298
+ const socket2 = createMockSocket(client2);
299
+
300
+ gateway.exposeRegisterSocket('client1', socket1);
301
+ gateway.exposeRegisterSocket('client2', socket2);
302
+
303
+ await storage.addClient(client1);
304
+ await storage.addClient(client2);
305
+ await storage.addClientToRoom('client1', 'room1');
306
+ await storage.addClientToRoom('client1', 'room2'); // client1 in both rooms
307
+ await storage.addClientToRoom('client2', 'room2');
308
+
309
+ await gateway.emitToRooms(['room1', 'room2'], 'multi:message', { data: 'test' });
310
+
311
+ // client1 should receive only once even though in both rooms
312
+ expect(socket1.send).toHaveBeenCalledTimes(1);
313
+ expect(socket2.send).toHaveBeenCalledTimes(1);
314
+ });
315
+ });
316
+
317
+ describe('disconnectClient', () => {
318
+ it('should close client socket', () => {
319
+ const clientData = createClientData('client1');
320
+ const socket = createMockSocket(clientData);
321
+
322
+ gateway.exposeRegisterSocket('client1', socket);
323
+ gateway.disconnectClient('client1', 'test reason');
324
+
325
+ expect(socket.close).toHaveBeenCalledWith(1000, 'test reason');
326
+ });
327
+
328
+ it('should use default reason if not provided', () => {
329
+ const clientData = createClientData('client1');
330
+ const socket = createMockSocket(clientData);
331
+
332
+ gateway.exposeRegisterSocket('client1', socket);
333
+ gateway.disconnectClient('client1');
334
+
335
+ expect(socket.close).toHaveBeenCalledWith(1000, 'Disconnected by server');
336
+ });
337
+
338
+ it('should not throw for non-existent client', () => {
339
+ expect(() => gateway.disconnectClient('nonexistent')).not.toThrow();
340
+ });
341
+ });
342
+
343
+ describe('disconnectAll', () => {
344
+ it('should close all client sockets', () => {
345
+ const client1 = createClientData('client1');
346
+ const client2 = createClientData('client2');
347
+ const socket1 = createMockSocket(client1);
348
+ const socket2 = createMockSocket(client2);
349
+
350
+ gateway.exposeRegisterSocket('client1', socket1);
351
+ gateway.exposeRegisterSocket('client2', socket2);
352
+ gateway.disconnectAll('server shutdown');
353
+
354
+ expect(socket1.close).toHaveBeenCalledWith(1000, 'server shutdown');
355
+ expect(socket2.close).toHaveBeenCalledWith(1000, 'server shutdown');
356
+ });
357
+ });
358
+
359
+ describe('disconnectRoom', () => {
360
+ it('should disconnect all clients in room', async () => {
361
+ const client1 = createClientData('client1');
362
+ const client2 = createClientData('client2');
363
+ const socket1 = createMockSocket(client1);
364
+ const socket2 = createMockSocket(client2);
365
+
366
+ gateway.exposeRegisterSocket('client1', socket1);
367
+ gateway.exposeRegisterSocket('client2', socket2);
368
+
369
+ await storage.addClient(client1);
370
+ await storage.addClient(client2);
371
+ await storage.addClientToRoom('client1', 'room1');
372
+ await storage.addClientToRoom('client2', 'room1');
373
+
374
+ await gateway.disconnectRoom('room1', 'room closed');
375
+
376
+ expect(socket1.close).toHaveBeenCalledWith(1000, 'room closed');
377
+ expect(socket2.close).toHaveBeenCalledWith(1000, 'room closed');
378
+ });
379
+ });
380
+
381
+ describe('joinRoom', () => {
382
+ it('should add client to room in storage', async () => {
383
+ const clientData = createClientData('client1');
384
+ const socket = createMockSocket(clientData);
385
+
386
+ gateway.exposeRegisterSocket('client1', socket);
387
+ await storage.addClient(clientData);
388
+
389
+ await gateway.joinRoom('client1', 'room1');
390
+
391
+ const room = await storage.getRoom('room1');
392
+ expect(room?.clientIds).toContain('client1');
393
+ });
394
+
395
+ it('should subscribe socket to room topic', async () => {
396
+ const clientData = createClientData('client1');
397
+ const socket = createMockSocket(clientData);
398
+
399
+ gateway.exposeRegisterSocket('client1', socket);
400
+ await storage.addClient(clientData);
401
+
402
+ await gateway.joinRoom('client1', 'room1');
403
+
404
+ expect(socket.subscribe).toHaveBeenCalledWith('room1');
405
+ });
406
+ });
407
+
408
+ describe('leaveRoom', () => {
409
+ it('should remove client from room in storage', async () => {
410
+ const clientData = createClientData('client1');
411
+ const socket = createMockSocket(clientData);
412
+
413
+ gateway.exposeRegisterSocket('client1', socket);
414
+ await storage.addClient(clientData);
415
+ await storage.addClientToRoom('client1', 'room1');
416
+
417
+ await gateway.leaveRoom('client1', 'room1');
418
+
419
+ const clientsInRoom = await storage.getClientsInRoom('room1');
420
+ expect(clientsInRoom).not.toContain('client1');
421
+ });
422
+
423
+ it('should unsubscribe socket from room topic', async () => {
424
+ const clientData = createClientData('client1');
425
+ const socket = createMockSocket(clientData);
426
+
427
+ gateway.exposeRegisterSocket('client1', socket);
428
+ await storage.addClient(clientData);
429
+ await storage.addClientToRoom('client1', 'room1');
430
+
431
+ await gateway.leaveRoom('client1', 'room1');
432
+
433
+ expect(socket.unsubscribe).toHaveBeenCalledWith('room1');
434
+ });
435
+ });
436
+
437
+ describe('getRoomsByPattern', () => {
438
+ it('should return empty array when storage not initialized', async () => {
439
+ const uninitializedGateway = new TestGateway();
440
+ const result = await uninitializedGateway.getRoomsByPattern('room:*');
441
+ expect(result).toEqual([]);
442
+ });
443
+
444
+ it('should return rooms matching pattern', async () => {
445
+ // Create rooms by adding clients to them
446
+ const client1 = createClientData('client1');
447
+ const client2 = createClientData('client2');
448
+ const client3 = createClientData('client3');
449
+ await storage.addClient(client1);
450
+ await storage.addClient(client2);
451
+ await storage.addClient(client3);
452
+ await storage.addClientToRoom('client1', 'room:general');
453
+ await storage.addClientToRoom('client2', 'room:private');
454
+ await storage.addClientToRoom('client3', 'other:room');
455
+
456
+ const result = await gateway.getRoomsByPattern('room:*');
457
+ expect(result.length).toBe(2);
458
+ expect(result.map(r => r.name).sort()).toEqual(['room:general', 'room:private']);
459
+ });
460
+ });
461
+
462
+ describe('getWsServer', () => {
463
+ it('should return null when server not initialized', () => {
464
+ const uninitializedGateway = new TestGateway();
465
+ expect(uninitializedGateway.getWsServer()).toBeNull();
466
+ });
467
+
468
+ it('should return server wrapper with publish method', () => {
469
+ const mockServer = {
470
+ publish: mock(() => undefined),
471
+ };
472
+ gateway.exposeInitialize(storage, mockServer);
473
+
474
+ const wsServer = gateway.getWsServer();
475
+ expect(wsServer).not.toBeNull();
476
+ expect(typeof wsServer?.publish).toBe('function');
477
+ });
478
+ });
479
+ });