@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.
- package/README.md +277 -3
- package/package.json +13 -2
- package/src/application.test.ts +119 -0
- package/src/application.ts +112 -5
- package/src/docs-examples.test.ts +2919 -0
- package/src/index.ts +96 -0
- package/src/module.ts +10 -4
- package/src/redis-client.ts +502 -0
- package/src/shared-redis.ts +231 -0
- package/src/types.ts +50 -0
- package/src/ws-base-gateway.test.ts +479 -0
- package/src/ws-base-gateway.ts +514 -0
- package/src/ws-client.test.ts +511 -0
- package/src/ws-client.ts +628 -0
- package/src/ws-client.types.ts +129 -0
- package/src/ws-decorators.test.ts +331 -0
- package/src/ws-decorators.ts +417 -0
- package/src/ws-guards.test.ts +334 -0
- package/src/ws-guards.ts +298 -0
- package/src/ws-handler.ts +658 -0
- package/src/ws-integration.test.ts +517 -0
- package/src/ws-pattern-matcher.test.ts +152 -0
- package/src/ws-pattern-matcher.ts +240 -0
- package/src/ws-service-definition.ts +223 -0
- package/src/ws-socketio-protocol.test.ts +344 -0
- package/src/ws-socketio-protocol.ts +567 -0
- package/src/ws-storage-memory.test.ts +246 -0
- package/src/ws-storage-memory.ts +222 -0
- package/src/ws-storage-redis.ts +302 -0
- package/src/ws-storage.ts +210 -0
- package/src/ws.types.ts +342 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for WebSocket Gateway + Client
|
|
3
|
+
*
|
|
4
|
+
* Tests the full cycle of WebSocket communication including:
|
|
5
|
+
* - Server startup with WebSocket Gateway
|
|
6
|
+
* - Client connection and message exchange
|
|
7
|
+
* - Room management
|
|
8
|
+
* - Event patterns
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
describe,
|
|
13
|
+
it,
|
|
14
|
+
expect,
|
|
15
|
+
beforeAll,
|
|
16
|
+
afterAll,
|
|
17
|
+
beforeEach,
|
|
18
|
+
afterEach,
|
|
19
|
+
} from 'bun:test';
|
|
20
|
+
|
|
21
|
+
import type { WsClientData } from './ws.types';
|
|
22
|
+
|
|
23
|
+
import { OneBunApplication } from './application';
|
|
24
|
+
import { Module } from './decorators';
|
|
25
|
+
import { BaseWebSocketGateway } from './ws-base-gateway';
|
|
26
|
+
import { createWsClient } from './ws-client';
|
|
27
|
+
import {
|
|
28
|
+
WebSocketGateway,
|
|
29
|
+
OnConnect,
|
|
30
|
+
OnDisconnect,
|
|
31
|
+
OnMessage,
|
|
32
|
+
OnJoinRoom,
|
|
33
|
+
OnLeaveRoom,
|
|
34
|
+
Client,
|
|
35
|
+
MessageData,
|
|
36
|
+
RoomName,
|
|
37
|
+
PatternParams,
|
|
38
|
+
UseWsGuards,
|
|
39
|
+
} from './ws-decorators';
|
|
40
|
+
import { WsAuthGuard } from './ws-guards';
|
|
41
|
+
import { createWsServiceDefinition } from './ws-service-definition';
|
|
42
|
+
|
|
43
|
+
// Test port - use a high port to avoid conflicts
|
|
44
|
+
const TEST_PORT = 19876;
|
|
45
|
+
const TEST_URL = `ws://localhost:${TEST_PORT}/ws`;
|
|
46
|
+
|
|
47
|
+
// Test Gateway implementation
|
|
48
|
+
@WebSocketGateway({ path: '/ws' })
|
|
49
|
+
class TestGateway extends BaseWebSocketGateway {
|
|
50
|
+
public connectCount = 0;
|
|
51
|
+
public disconnectCount = 0;
|
|
52
|
+
public lastMessage: unknown = null;
|
|
53
|
+
public lastClient: WsClientData | null = null;
|
|
54
|
+
|
|
55
|
+
@OnConnect()
|
|
56
|
+
handleConnect(@Client() client: WsClientData) {
|
|
57
|
+
this.connectCount++;
|
|
58
|
+
this.lastClient = client;
|
|
59
|
+
|
|
60
|
+
return { event: 'welcome', data: { message: 'Welcome!', clientId: client.id } };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@OnDisconnect()
|
|
64
|
+
handleDisconnect(@Client() client: WsClientData) {
|
|
65
|
+
this.disconnectCount++;
|
|
66
|
+
this.lastClient = client;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@OnMessage('echo')
|
|
70
|
+
handleEcho(@MessageData() data: unknown) {
|
|
71
|
+
this.lastMessage = data;
|
|
72
|
+
|
|
73
|
+
return { event: 'echo:response', data };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@OnMessage('broadcast')
|
|
77
|
+
handleBroadcast(@Client() client: WsClientData, @MessageData() data: { text: string }) {
|
|
78
|
+
this.broadcast('broadcast:message', { from: client.id, text: data.text });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@OnMessage('chat:{roomId}:message')
|
|
82
|
+
handleChatMessage(
|
|
83
|
+
@Client() client: WsClientData,
|
|
84
|
+
@MessageData() data: { text: string },
|
|
85
|
+
@PatternParams() params: { roomId: string },
|
|
86
|
+
) {
|
|
87
|
+
this.emitToRoom(`room:${params.roomId}`, 'chat:message', {
|
|
88
|
+
roomId: params.roomId,
|
|
89
|
+
from: client.id,
|
|
90
|
+
text: data.text,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return { event: 'chat:sent', data: { roomId: params.roomId } };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@OnJoinRoom()
|
|
97
|
+
async handleJoinRoom(@Client() client: WsClientData, @RoomName() room: string) {
|
|
98
|
+
await this.joinRoom(client.id, room);
|
|
99
|
+
|
|
100
|
+
return { event: 'room:joined', data: { room } };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
@OnLeaveRoom()
|
|
104
|
+
async handleLeaveRoom(@Client() client: WsClientData, @RoomName() room: string) {
|
|
105
|
+
await this.leaveRoom(client.id, room);
|
|
106
|
+
|
|
107
|
+
return { event: 'room:left', data: { room } };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@UseWsGuards(WsAuthGuard)
|
|
111
|
+
@OnMessage('protected')
|
|
112
|
+
handleProtected(@Client() client: WsClientData) {
|
|
113
|
+
return { event: 'protected:response', data: { userId: client.auth?.userId } };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Test Module
|
|
118
|
+
@Module({
|
|
119
|
+
controllers: [TestGateway],
|
|
120
|
+
})
|
|
121
|
+
class TestModule {}
|
|
122
|
+
|
|
123
|
+
describe('WebSocket Integration', () => {
|
|
124
|
+
let app: OneBunApplication;
|
|
125
|
+
let definition: ReturnType<typeof createWsServiceDefinition>;
|
|
126
|
+
|
|
127
|
+
beforeAll(async () => {
|
|
128
|
+
// Start the application
|
|
129
|
+
app = new OneBunApplication(TestModule, {
|
|
130
|
+
port: TEST_PORT,
|
|
131
|
+
development: true,
|
|
132
|
+
});
|
|
133
|
+
await app.start();
|
|
134
|
+
|
|
135
|
+
// Create service definition for typed client
|
|
136
|
+
definition = createWsServiceDefinition(TestModule);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
afterAll(async () => {
|
|
140
|
+
await app.stop();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('basic connection', () => {
|
|
144
|
+
it('should connect and receive welcome message', async () => {
|
|
145
|
+
const client = createWsClient(definition, { url: TEST_URL });
|
|
146
|
+
|
|
147
|
+
let welcomeReceived = false;
|
|
148
|
+
let welcomeData: unknown = null;
|
|
149
|
+
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
+
(client as any).TestGateway.on('welcome', (data: unknown) => {
|
|
152
|
+
welcomeReceived = true;
|
|
153
|
+
welcomeData = data;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await client.connect();
|
|
157
|
+
|
|
158
|
+
// Wait for welcome message
|
|
159
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
160
|
+
|
|
161
|
+
expect(welcomeReceived).toBe(true);
|
|
162
|
+
expect(welcomeData).toHaveProperty('message', 'Welcome!');
|
|
163
|
+
expect(welcomeData).toHaveProperty('clientId');
|
|
164
|
+
|
|
165
|
+
client.disconnect();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('should disconnect cleanly', async () => {
|
|
169
|
+
const client = createWsClient(definition, { url: TEST_URL });
|
|
170
|
+
|
|
171
|
+
await client.connect();
|
|
172
|
+
expect(client.isConnected()).toBe(true);
|
|
173
|
+
|
|
174
|
+
client.disconnect();
|
|
175
|
+
expect(client.isConnected()).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('message exchange', () => {
|
|
180
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
181
|
+
let client: any;
|
|
182
|
+
|
|
183
|
+
beforeEach(async () => {
|
|
184
|
+
client = createWsClient(definition, { url: TEST_URL, timeout: 2000 });
|
|
185
|
+
await client.connect();
|
|
186
|
+
// Wait for connection to stabilize
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
afterEach(() => {
|
|
191
|
+
if (client?.isConnected()) {
|
|
192
|
+
client.disconnect();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should echo messages', async () => {
|
|
197
|
+
const testData = { hello: 'world', number: 42 };
|
|
198
|
+
|
|
199
|
+
let responseReceived = false;
|
|
200
|
+
let responseData: unknown = null;
|
|
201
|
+
|
|
202
|
+
client.TestGateway.on('echo:response', (data: unknown) => {
|
|
203
|
+
responseReceived = true;
|
|
204
|
+
responseData = data;
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
client.TestGateway.send('echo', testData);
|
|
208
|
+
|
|
209
|
+
// Wait for response
|
|
210
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
211
|
+
|
|
212
|
+
expect(responseReceived).toBe(true);
|
|
213
|
+
expect(responseData).toEqual(testData);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should handle emit with acknowledgement', async () => {
|
|
217
|
+
const testData = { test: 'data' };
|
|
218
|
+
|
|
219
|
+
const response = await client.TestGateway.emit('echo', testData);
|
|
220
|
+
|
|
221
|
+
// Response should be the acknowledgement
|
|
222
|
+
expect(response).toBeDefined();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('room management', () => {
|
|
227
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
|
+
let client1: any;
|
|
229
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
230
|
+
let client2: any;
|
|
231
|
+
|
|
232
|
+
beforeEach(async () => {
|
|
233
|
+
client1 = createWsClient(definition, { url: TEST_URL, timeout: 2000 });
|
|
234
|
+
client2 = createWsClient(definition, { url: TEST_URL, timeout: 2000 });
|
|
235
|
+
await client1.connect();
|
|
236
|
+
await client2.connect();
|
|
237
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
afterEach(() => {
|
|
241
|
+
if (client1?.isConnected()) {
|
|
242
|
+
client1.disconnect();
|
|
243
|
+
}
|
|
244
|
+
if (client2?.isConnected()) {
|
|
245
|
+
client2.disconnect();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should join and leave rooms', async () => {
|
|
250
|
+
// Use emit with acknowledgement for more reliable testing
|
|
251
|
+
const joinResponse = await client1.TestGateway.emit('join', 'room:test');
|
|
252
|
+
// Handler returns { event: 'room:joined', data: { room } }
|
|
253
|
+
// With ack, we get the full response
|
|
254
|
+
expect(joinResponse).toBeDefined();
|
|
255
|
+
|
|
256
|
+
const leaveResponse = await client1.TestGateway.emit('leave', 'room:test');
|
|
257
|
+
expect(leaveResponse).toBeDefined();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should broadcast messages to room members', async () => {
|
|
261
|
+
const roomName = 'room:chat';
|
|
262
|
+
let client2ReceivedMessage = false;
|
|
263
|
+
let receivedData: unknown = null;
|
|
264
|
+
|
|
265
|
+
// Both clients join the same room
|
|
266
|
+
client1.TestGateway.send('join', roomName);
|
|
267
|
+
client2.TestGateway.send('join', roomName);
|
|
268
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
269
|
+
|
|
270
|
+
// Client2 listens for messages
|
|
271
|
+
client2.TestGateway.on('chat:message', (data: unknown) => {
|
|
272
|
+
client2ReceivedMessage = true;
|
|
273
|
+
receivedData = data;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Client1 sends a message to the room
|
|
277
|
+
client1.TestGateway.send('chat:chat:message', { text: 'Hello room!' });
|
|
278
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
279
|
+
|
|
280
|
+
expect(client2ReceivedMessage).toBe(true);
|
|
281
|
+
expect(receivedData).toHaveProperty('text', 'Hello room!');
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('pattern matching', () => {
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
287
|
+
let client: any;
|
|
288
|
+
|
|
289
|
+
beforeEach(async () => {
|
|
290
|
+
client = createWsClient(definition, { url: TEST_URL, timeout: 2000 });
|
|
291
|
+
await client.connect();
|
|
292
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
afterEach(() => {
|
|
296
|
+
if (client?.isConnected()) {
|
|
297
|
+
client.disconnect();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should match parameterized patterns', async () => {
|
|
302
|
+
let sentResponse: unknown = null;
|
|
303
|
+
|
|
304
|
+
client.TestGateway.on('chat:sent', (data: unknown) => {
|
|
305
|
+
sentResponse = data;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Send message to chat:room123:message pattern
|
|
309
|
+
client.TestGateway.send('chat:room123:message', { text: 'Hello!' });
|
|
310
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
311
|
+
|
|
312
|
+
expect(sentResponse).toHaveProperty('roomId', 'room123');
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe('multiple clients', () => {
|
|
317
|
+
it('should handle multiple simultaneous connections', async () => {
|
|
318
|
+
const clients = [];
|
|
319
|
+
const connectedClients: string[] = [];
|
|
320
|
+
|
|
321
|
+
// Create 5 clients
|
|
322
|
+
for (let i = 0; i < 5; i++) {
|
|
323
|
+
const client = createWsClient(definition, { url: TEST_URL });
|
|
324
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
325
|
+
(client as any).TestGateway.on('welcome', (data: { clientId: string }) => {
|
|
326
|
+
connectedClients.push(data.clientId);
|
|
327
|
+
});
|
|
328
|
+
clients.push(client);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Connect all clients
|
|
332
|
+
await Promise.all(clients.map((c) => c.connect()));
|
|
333
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
334
|
+
|
|
335
|
+
expect(connectedClients.length).toBe(5);
|
|
336
|
+
|
|
337
|
+
// All client IDs should be unique
|
|
338
|
+
const uniqueIds = new Set(connectedClients);
|
|
339
|
+
expect(uniqueIds.size).toBe(5);
|
|
340
|
+
|
|
341
|
+
// Disconnect all
|
|
342
|
+
clients.forEach((c) => c.disconnect());
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('should broadcast to all clients', async () => {
|
|
346
|
+
const client1 = createWsClient(definition, { url: TEST_URL });
|
|
347
|
+
const client2 = createWsClient(definition, { url: TEST_URL });
|
|
348
|
+
|
|
349
|
+
let client1Received = false;
|
|
350
|
+
let client2Received = false;
|
|
351
|
+
|
|
352
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
353
|
+
(client1 as any).TestGateway.on('broadcast:message', () => {
|
|
354
|
+
client1Received = true;
|
|
355
|
+
});
|
|
356
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
357
|
+
(client2 as any).TestGateway.on('broadcast:message', () => {
|
|
358
|
+
client2Received = true;
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
await client1.connect();
|
|
362
|
+
await client2.connect();
|
|
363
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
364
|
+
|
|
365
|
+
// Client1 broadcasts
|
|
366
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
367
|
+
(client1 as any).TestGateway.send('broadcast', { text: 'Hello everyone!' });
|
|
368
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
369
|
+
|
|
370
|
+
expect(client1Received).toBe(true);
|
|
371
|
+
expect(client2Received).toBe(true);
|
|
372
|
+
|
|
373
|
+
client1.disconnect();
|
|
374
|
+
client2.disconnect();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('authentication and guards', () => {
|
|
379
|
+
it('should reject protected handler without auth', async () => {
|
|
380
|
+
// Client without auth token
|
|
381
|
+
const client = createWsClient(definition, {
|
|
382
|
+
url: TEST_URL,
|
|
383
|
+
timeout: 2000,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
await client.connect();
|
|
387
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
388
|
+
|
|
389
|
+
// Try to access protected endpoint without auth - set up error listener
|
|
390
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
391
|
+
(client as any).TestGateway.on('error', () => {
|
|
392
|
+
// Error expected when auth fails
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Send to protected endpoint - should not receive response
|
|
396
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
397
|
+
(client as any).TestGateway.send('protected', {});
|
|
398
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
399
|
+
|
|
400
|
+
// We should not receive protected:response since guard blocks it
|
|
401
|
+
let protectedResponseReceived = false;
|
|
402
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
403
|
+
(client as any).TestGateway.on('protected:response', () => {
|
|
404
|
+
protectedResponseReceived = true;
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect(protectedResponseReceived).toBe(false);
|
|
408
|
+
|
|
409
|
+
client.disconnect();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should allow protected handler with valid auth token', async () => {
|
|
413
|
+
// Client with auth token
|
|
414
|
+
const client = createWsClient(definition, {
|
|
415
|
+
url: TEST_URL,
|
|
416
|
+
timeout: 2000,
|
|
417
|
+
auth: {
|
|
418
|
+
token: 'valid-test-token',
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
await client.connect();
|
|
423
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
424
|
+
|
|
425
|
+
let responseReceived = false;
|
|
426
|
+
let responseData: { userId?: string } | null = null;
|
|
427
|
+
|
|
428
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
429
|
+
(client as any).TestGateway.on('protected:response', (data: { userId?: string }) => {
|
|
430
|
+
responseReceived = true;
|
|
431
|
+
responseData = data;
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Send to protected endpoint with auth
|
|
435
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
436
|
+
(client as any).TestGateway.send('protected', {});
|
|
437
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
438
|
+
|
|
439
|
+
// With auth token, the request should be allowed
|
|
440
|
+
expect(responseReceived).toBe(true);
|
|
441
|
+
expect(responseData).toBeDefined();
|
|
442
|
+
|
|
443
|
+
client.disconnect();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should pass auth data to handler via client object', async () => {
|
|
447
|
+
const testToken = 'test-user-token-123';
|
|
448
|
+
const client = createWsClient(definition, {
|
|
449
|
+
url: TEST_URL,
|
|
450
|
+
timeout: 2000,
|
|
451
|
+
auth: {
|
|
452
|
+
token: testToken,
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
await client.connect();
|
|
457
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
458
|
+
|
|
459
|
+
let responseData: { userId?: string } | null = null;
|
|
460
|
+
|
|
461
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
462
|
+
(client as any).TestGateway.on('protected:response', (data: { userId?: string }) => {
|
|
463
|
+
responseData = data;
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
467
|
+
(client as any).TestGateway.send('protected', {});
|
|
468
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
469
|
+
|
|
470
|
+
// Auth data should be available in handler
|
|
471
|
+
expect(responseData).toBeDefined();
|
|
472
|
+
|
|
473
|
+
client.disconnect();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('should handle multiple authenticated clients independently', async () => {
|
|
477
|
+
const client1 = createWsClient(definition, {
|
|
478
|
+
url: TEST_URL,
|
|
479
|
+
auth: { token: 'user1-token' },
|
|
480
|
+
});
|
|
481
|
+
const client2 = createWsClient(definition, {
|
|
482
|
+
url: TEST_URL,
|
|
483
|
+
auth: { token: 'user2-token' },
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
await client1.connect();
|
|
487
|
+
await client2.connect();
|
|
488
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
489
|
+
|
|
490
|
+
let client1Response = false;
|
|
491
|
+
let client2Response = false;
|
|
492
|
+
|
|
493
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
494
|
+
(client1 as any).TestGateway.on('protected:response', () => {
|
|
495
|
+
client1Response = true;
|
|
496
|
+
});
|
|
497
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
498
|
+
(client2 as any).TestGateway.on('protected:response', () => {
|
|
499
|
+
client2Response = true;
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// Both clients access protected endpoint
|
|
503
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
504
|
+
(client1 as any).TestGateway.send('protected', {});
|
|
505
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
506
|
+
(client2 as any).TestGateway.send('protected', {});
|
|
507
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
508
|
+
|
|
509
|
+
// Both should receive responses
|
|
510
|
+
expect(client1Response).toBe(true);
|
|
511
|
+
expect(client2Response).toBe(true);
|
|
512
|
+
|
|
513
|
+
client1.disconnect();
|
|
514
|
+
client2.disconnect();
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Pattern Matcher Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
describe,
|
|
7
|
+
it,
|
|
8
|
+
expect,
|
|
9
|
+
} from 'bun:test';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
matchPattern,
|
|
13
|
+
isPatternMatch,
|
|
14
|
+
createPatternMatcher,
|
|
15
|
+
isPattern,
|
|
16
|
+
getPatternParams,
|
|
17
|
+
buildFromPattern,
|
|
18
|
+
findMatchingValues,
|
|
19
|
+
} from './ws-pattern-matcher';
|
|
20
|
+
|
|
21
|
+
describe('ws-pattern-matcher', () => {
|
|
22
|
+
describe('matchPattern', () => {
|
|
23
|
+
it('should match exact strings', () => {
|
|
24
|
+
const result = matchPattern('chat:message', 'chat:message');
|
|
25
|
+
expect(result.matched).toBe(true);
|
|
26
|
+
expect(result.params).toEqual({});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should not match different strings', () => {
|
|
30
|
+
const result = matchPattern('chat:message', 'chat:other');
|
|
31
|
+
expect(result.matched).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should match wildcard (*) patterns', () => {
|
|
35
|
+
const result = matchPattern('chat:*', 'chat:general');
|
|
36
|
+
expect(result.matched).toBe(true);
|
|
37
|
+
expect(result.params).toEqual({});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should match wildcard in the middle', () => {
|
|
41
|
+
const result = matchPattern('user:*:action', 'user:123:action');
|
|
42
|
+
expect(result.matched).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should not match wildcard with extra segments', () => {
|
|
46
|
+
const result = matchPattern('chat:*', 'chat:general:extra');
|
|
47
|
+
expect(result.matched).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should extract named parameters', () => {
|
|
51
|
+
const result = matchPattern('chat:{roomId}', 'chat:general');
|
|
52
|
+
expect(result.matched).toBe(true);
|
|
53
|
+
expect(result.params).toEqual({ roomId: 'general' });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should extract multiple parameters', () => {
|
|
57
|
+
const result = matchPattern('user:{userId}:room:{roomId}', 'user:123:room:general');
|
|
58
|
+
expect(result.matched).toBe(true);
|
|
59
|
+
expect(result.params).toEqual({ userId: '123', roomId: 'general' });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle combined wildcard and parameters', () => {
|
|
63
|
+
const result = matchPattern('service:{service}:*', 'service:auth:login');
|
|
64
|
+
expect(result.matched).toBe(true);
|
|
65
|
+
expect(result.params).toEqual({ service: 'auth' });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should not match when parameter value contains separator', () => {
|
|
69
|
+
const result = matchPattern('chat:{roomId}', 'chat:room:sub');
|
|
70
|
+
expect(result.matched).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('isPatternMatch', () => {
|
|
75
|
+
it('should return true for matching patterns', () => {
|
|
76
|
+
expect(isPatternMatch('chat:*', 'chat:general')).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return false for non-matching patterns', () => {
|
|
80
|
+
expect(isPatternMatch('chat:*', 'room:general')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('createPatternMatcher', () => {
|
|
85
|
+
it('should create reusable matcher function', () => {
|
|
86
|
+
const matcher = createPatternMatcher('chat:{roomId}:message');
|
|
87
|
+
|
|
88
|
+
const result1 = matcher('chat:general:message');
|
|
89
|
+
expect(result1.matched).toBe(true);
|
|
90
|
+
expect(result1.params).toEqual({ roomId: 'general' });
|
|
91
|
+
|
|
92
|
+
const result2 = matcher('chat:private:message');
|
|
93
|
+
expect(result2.matched).toBe(true);
|
|
94
|
+
expect(result2.params).toEqual({ roomId: 'private' });
|
|
95
|
+
|
|
96
|
+
const result3 = matcher('chat:other:action');
|
|
97
|
+
expect(result3.matched).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('isPattern', () => {
|
|
102
|
+
it('should return true for wildcard patterns', () => {
|
|
103
|
+
expect(isPattern('chat:*')).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should return true for parameter patterns', () => {
|
|
107
|
+
expect(isPattern('chat:{roomId}')).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should return false for literal strings', () => {
|
|
111
|
+
expect(isPattern('chat:general')).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('getPatternParams', () => {
|
|
116
|
+
it('should extract parameter names', () => {
|
|
117
|
+
const params = getPatternParams('chat:{roomId}:user:{userId}');
|
|
118
|
+
expect(params).toEqual(['roomId', 'userId']);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should return empty array for patterns without parameters', () => {
|
|
122
|
+
const params = getPatternParams('chat:*');
|
|
123
|
+
expect(params).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('buildFromPattern', () => {
|
|
128
|
+
it('should build value from pattern and params', () => {
|
|
129
|
+
const result = buildFromPattern('chat:{roomId}:message', { roomId: 'general' });
|
|
130
|
+
expect(result).toBe('chat:general:message');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle multiple parameters', () => {
|
|
134
|
+
const result = buildFromPattern('user:{id}:room:{room}', { id: '123', room: 'test' });
|
|
135
|
+
expect(result).toBe('user:123:room:test');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('findMatchingValues', () => {
|
|
140
|
+
it('should find all matching values', () => {
|
|
141
|
+
const values = ['chat:general', 'chat:private', 'room:lobby', 'chat:admin'];
|
|
142
|
+
const matching = findMatchingValues('chat:*', values);
|
|
143
|
+
expect(matching).toEqual(['chat:general', 'chat:private', 'chat:admin']);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should return empty array when nothing matches', () => {
|
|
147
|
+
const values = ['room:lobby', 'room:admin'];
|
|
148
|
+
const matching = findMatchingValues('chat:*', values);
|
|
149
|
+
expect(matching).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|