@onebun/core 0.1.1 → 0.1.3

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