@dangao/bun-server 1.5.0 → 1.6.0
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/dist/core/server.d.ts.map +1 -1
- package/dist/index.js +72 -21
- package/dist/websocket/registry.d.ts +19 -4
- package/dist/websocket/registry.d.ts.map +1 -1
- package/docs/zh/guide.md +23 -18
- package/docs/zh/migration.md +8 -2
- package/package.json +1 -1
- package/src/core/server.ts +15 -7
- package/src/websocket/registry.ts +113 -19
- package/tests/websocket/gateway.test.ts +1024 -0
|
@@ -4,7 +4,11 @@ import type { ServerWebSocket } from 'bun';
|
|
|
4
4
|
import { WebSocketGateway, OnOpen, OnMessage, OnClose } from '../../src/websocket/decorators';
|
|
5
5
|
import { WebSocketGatewayRegistry } from '../../src/websocket/registry';
|
|
6
6
|
import { ControllerRegistry } from '../../src/controller/controller';
|
|
7
|
+
import { Application } from '../../src/core/application';
|
|
8
|
+
import { Query, Context } from '../../src/controller/decorators';
|
|
9
|
+
import { getTestPort } from '../utils/test-port';
|
|
7
10
|
import type { WebSocketConnectionData } from '../../src/websocket/registry';
|
|
11
|
+
import type { Context as RequestContext } from '../../src/core/context';
|
|
8
12
|
|
|
9
13
|
function createFakeSocket(path: string): ServerWebSocket<WebSocketConnectionData> {
|
|
10
14
|
return {
|
|
@@ -65,4 +69,1024 @@ describe('WebSocketGatewayRegistry', () => {
|
|
|
65
69
|
});
|
|
66
70
|
});
|
|
67
71
|
|
|
72
|
+
@WebSocketGateway('/ws/chat')
|
|
73
|
+
class ChatGateway {
|
|
74
|
+
public static messages: string[] = [];
|
|
75
|
+
|
|
76
|
+
@OnOpen
|
|
77
|
+
public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
78
|
+
ChatGateway.messages.push('connected');
|
|
79
|
+
ws.send('Welcome to chat!');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@OnMessage
|
|
83
|
+
public handleMessage(
|
|
84
|
+
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
85
|
+
message: string,
|
|
86
|
+
): void {
|
|
87
|
+
ChatGateway.messages.push(message);
|
|
88
|
+
ws.send(`[echo] ${message}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@OnClose
|
|
92
|
+
public handleClose(_ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
93
|
+
ChatGateway.messages.push('disconnected');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@WebSocketGateway('/ws/echo')
|
|
98
|
+
class EchoGateway {
|
|
99
|
+
public static echoes: string[] = [];
|
|
100
|
+
|
|
101
|
+
@OnMessage
|
|
102
|
+
public handleMessage(
|
|
103
|
+
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
104
|
+
message: string,
|
|
105
|
+
): void {
|
|
106
|
+
EchoGateway.echoes.push(message);
|
|
107
|
+
ws.send(`Echo: ${message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
describe('WebSocket Gateway Integration', () => {
|
|
112
|
+
let app: Application;
|
|
113
|
+
let port: number;
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
port = getTestPort();
|
|
117
|
+
app = new Application({ port });
|
|
118
|
+
ChatGateway.messages = [];
|
|
119
|
+
EchoGateway.echoes = [];
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
afterEach(async () => {
|
|
123
|
+
if (app) {
|
|
124
|
+
await app.stop();
|
|
125
|
+
}
|
|
126
|
+
WebSocketGatewayRegistry.getInstance().clear();
|
|
127
|
+
ControllerRegistry.getInstance().clear();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should support @WebSocketGateway decorator to register gateway', () => {
|
|
131
|
+
app.registerWebSocketGateway(ChatGateway);
|
|
132
|
+
|
|
133
|
+
const registry = WebSocketGatewayRegistry.getInstance();
|
|
134
|
+
expect(registry.hasGateway('/ws/chat')).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should handle WebSocket connection on registered path', async () => {
|
|
138
|
+
app.registerWebSocketGateway(ChatGateway);
|
|
139
|
+
await app.listen();
|
|
140
|
+
|
|
141
|
+
const ws = new WebSocket(`ws://localhost:${port}/ws/chat`);
|
|
142
|
+
|
|
143
|
+
await new Promise<void>((resolve, reject) => {
|
|
144
|
+
const timeout = setTimeout(() => {
|
|
145
|
+
reject(new Error('WebSocket connection timeout'));
|
|
146
|
+
}, 2000);
|
|
147
|
+
|
|
148
|
+
ws.onopen = () => {
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
resolve();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
ws.onerror = (error) => {
|
|
154
|
+
clearTimeout(timeout);
|
|
155
|
+
reject(error);
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(ChatGateway.messages).toContain('connected');
|
|
160
|
+
ws.close();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should handle WebSocket messages', async () => {
|
|
164
|
+
app.registerWebSocketGateway(ChatGateway);
|
|
165
|
+
await app.listen();
|
|
166
|
+
|
|
167
|
+
const ws = new WebSocket(`ws://localhost:${port}/ws/chat`);
|
|
168
|
+
|
|
169
|
+
await new Promise<void>((resolve, reject) => {
|
|
170
|
+
const timeout = setTimeout(() => {
|
|
171
|
+
reject(new Error('WebSocket connection timeout'));
|
|
172
|
+
}, 2000);
|
|
173
|
+
|
|
174
|
+
ws.onopen = () => {
|
|
175
|
+
clearTimeout(timeout);
|
|
176
|
+
resolve();
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
ws.onerror = (error) => {
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
reject(error);
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// 等待连接建立
|
|
186
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
187
|
+
|
|
188
|
+
const testMessage = 'Hello, WebSocket!';
|
|
189
|
+
ws.send(testMessage);
|
|
190
|
+
|
|
191
|
+
// 等待消息处理
|
|
192
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
193
|
+
|
|
194
|
+
expect(ChatGateway.messages).toContain(testMessage);
|
|
195
|
+
ws.close();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('should handle WebSocket close event', async () => {
|
|
199
|
+
app.registerWebSocketGateway(ChatGateway);
|
|
200
|
+
await app.listen();
|
|
201
|
+
|
|
202
|
+
const ws = new WebSocket(`ws://localhost:${port}/ws/chat`);
|
|
203
|
+
|
|
204
|
+
await new Promise<void>((resolve, reject) => {
|
|
205
|
+
const timeout = setTimeout(() => {
|
|
206
|
+
reject(new Error('WebSocket connection timeout'));
|
|
207
|
+
}, 2000);
|
|
208
|
+
|
|
209
|
+
ws.onopen = () => {
|
|
210
|
+
clearTimeout(timeout);
|
|
211
|
+
resolve();
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
ws.onerror = (error) => {
|
|
215
|
+
clearTimeout(timeout);
|
|
216
|
+
reject(error);
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// 等待连接建立
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
222
|
+
|
|
223
|
+
ws.close();
|
|
224
|
+
|
|
225
|
+
// 等待关闭事件处理
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
227
|
+
|
|
228
|
+
expect(ChatGateway.messages).toContain('connected');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('should support multiple gateways on different paths', async () => {
|
|
232
|
+
app.registerWebSocketGateway(ChatGateway);
|
|
233
|
+
app.registerWebSocketGateway(EchoGateway);
|
|
234
|
+
await app.listen();
|
|
235
|
+
|
|
236
|
+
const registry = WebSocketGatewayRegistry.getInstance();
|
|
237
|
+
expect(registry.hasGateway('/ws/chat')).toBe(true);
|
|
238
|
+
expect(registry.hasGateway('/ws/echo')).toBe(true);
|
|
239
|
+
|
|
240
|
+
// 测试 chat gateway
|
|
241
|
+
const chatWs = new WebSocket(`ws://localhost:${port}/ws/chat`);
|
|
242
|
+
await new Promise<void>((resolve, reject) => {
|
|
243
|
+
const timeout = setTimeout(() => {
|
|
244
|
+
reject(new Error('WebSocket connection timeout'));
|
|
245
|
+
}, 2000);
|
|
246
|
+
|
|
247
|
+
chatWs.onopen = () => {
|
|
248
|
+
clearTimeout(timeout);
|
|
249
|
+
resolve();
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
chatWs.onerror = (error) => {
|
|
253
|
+
clearTimeout(timeout);
|
|
254
|
+
reject(error);
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
259
|
+
chatWs.send('chat message');
|
|
260
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
261
|
+
|
|
262
|
+
// 测试 echo gateway
|
|
263
|
+
const echoWs = new WebSocket(`ws://localhost:${port}/ws/echo`);
|
|
264
|
+
await new Promise<void>((resolve, reject) => {
|
|
265
|
+
const timeout = setTimeout(() => {
|
|
266
|
+
reject(new Error('WebSocket connection timeout'));
|
|
267
|
+
}, 2000);
|
|
268
|
+
|
|
269
|
+
echoWs.onopen = () => {
|
|
270
|
+
clearTimeout(timeout);
|
|
271
|
+
resolve();
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
echoWs.onerror = (error) => {
|
|
275
|
+
clearTimeout(timeout);
|
|
276
|
+
reject(error);
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
281
|
+
echoWs.send('echo message');
|
|
282
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
283
|
+
|
|
284
|
+
expect(ChatGateway.messages).toContain('chat message');
|
|
285
|
+
expect(EchoGateway.echoes).toContain('echo message');
|
|
286
|
+
|
|
287
|
+
chatWs.close();
|
|
288
|
+
echoWs.close();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('should return 404 for unregistered WebSocket path', async () => {
|
|
292
|
+
app.registerWebSocketGateway(ChatGateway);
|
|
293
|
+
await app.listen();
|
|
294
|
+
|
|
295
|
+
const response = await fetch(`http://localhost:${port}/ws/unknown`, {
|
|
296
|
+
headers: {
|
|
297
|
+
Upgrade: 'websocket',
|
|
298
|
+
Connection: 'Upgrade',
|
|
299
|
+
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
|
300
|
+
'Sec-WebSocket-Version': '13',
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
expect(response.status).toBe(404);
|
|
305
|
+
const text = await response.text();
|
|
306
|
+
expect(text).toContain('WebSocket gateway not found');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('should handle WebSocket message with binary data', async () => {
|
|
310
|
+
app.registerWebSocketGateway(EchoGateway);
|
|
311
|
+
await app.listen();
|
|
312
|
+
|
|
313
|
+
const ws = new WebSocket(`ws://localhost:${port}/ws/echo`);
|
|
314
|
+
|
|
315
|
+
await new Promise<void>((resolve, reject) => {
|
|
316
|
+
const timeout = setTimeout(() => {
|
|
317
|
+
reject(new Error('WebSocket connection timeout'));
|
|
318
|
+
}, 2000);
|
|
319
|
+
|
|
320
|
+
ws.onopen = () => {
|
|
321
|
+
clearTimeout(timeout);
|
|
322
|
+
resolve();
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
ws.onerror = (error) => {
|
|
326
|
+
clearTimeout(timeout);
|
|
327
|
+
reject(error);
|
|
328
|
+
};
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
332
|
+
|
|
333
|
+
const binaryData = new Uint8Array([1, 2, 3, 4, 5]);
|
|
334
|
+
ws.send(binaryData);
|
|
335
|
+
|
|
336
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
337
|
+
|
|
338
|
+
// 二进制数据会被转换为字符串或 ArrayBuffer
|
|
339
|
+
// 这里主要验证不会抛出错误
|
|
340
|
+
expect(EchoGateway.echoes.length).toBeGreaterThan(0);
|
|
341
|
+
|
|
342
|
+
ws.close();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('should support method-level event handlers', async () => {
|
|
346
|
+
@WebSocketGateway('/ws/method-test')
|
|
347
|
+
class MethodLevelGateway {
|
|
348
|
+
public static events: string[] = [];
|
|
349
|
+
|
|
350
|
+
@OnOpen
|
|
351
|
+
public onConnection(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
352
|
+
MethodLevelGateway.events.push('connection-opened');
|
|
353
|
+
ws.send('Connected via method handler');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@OnMessage
|
|
357
|
+
public onMessageReceived(
|
|
358
|
+
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
359
|
+
message: string,
|
|
360
|
+
): void {
|
|
361
|
+
MethodLevelGateway.events.push(`message-received:${message}`);
|
|
362
|
+
ws.send(`Method handler received: ${message}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@OnClose
|
|
366
|
+
public onConnectionClosed(
|
|
367
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
368
|
+
code: number,
|
|
369
|
+
reason: string,
|
|
370
|
+
): void {
|
|
371
|
+
MethodLevelGateway.events.push(`connection-closed:${code}:${reason}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
app.registerWebSocketGateway(MethodLevelGateway);
|
|
376
|
+
await app.listen();
|
|
377
|
+
|
|
378
|
+
const ws = new WebSocket(`ws://localhost:${port}/ws/method-test`);
|
|
379
|
+
|
|
380
|
+
await new Promise<void>((resolve, reject) => {
|
|
381
|
+
const timeout = setTimeout(() => {
|
|
382
|
+
reject(new Error('WebSocket connection timeout'));
|
|
383
|
+
}, 2000);
|
|
384
|
+
|
|
385
|
+
ws.onopen = () => {
|
|
386
|
+
clearTimeout(timeout);
|
|
387
|
+
resolve();
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
ws.onerror = (error) => {
|
|
391
|
+
clearTimeout(timeout);
|
|
392
|
+
reject(error);
|
|
393
|
+
};
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
397
|
+
expect(MethodLevelGateway.events).toContain('connection-opened');
|
|
398
|
+
|
|
399
|
+
ws.send('test-message');
|
|
400
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
401
|
+
expect(MethodLevelGateway.events).toContain('message-received:test-message');
|
|
402
|
+
|
|
403
|
+
ws.close(1000, 'test-close');
|
|
404
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
405
|
+
expect(MethodLevelGateway.events.some((e) => e.includes('connection-closed'))).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test('should extract and provide path information from WebSocket URL', async () => {
|
|
409
|
+
// 注意:当前实现使用精确路径匹配,不支持动态路径参数(如 :roomId)
|
|
410
|
+
// 这个测试验证路径信息是否被正确保存到 ws.data.path
|
|
411
|
+
@WebSocketGateway('/ws/rooms')
|
|
412
|
+
class RoomGateway {
|
|
413
|
+
public static paths: string[] = [];
|
|
414
|
+
|
|
415
|
+
@OnOpen
|
|
416
|
+
public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
417
|
+
const path = ws.data?.path || '';
|
|
418
|
+
RoomGateway.paths.push(path);
|
|
419
|
+
ws.send(`Joined room from path: ${path}`);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
@OnMessage
|
|
423
|
+
public handleMessage(
|
|
424
|
+
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
425
|
+
message: string,
|
|
426
|
+
): void {
|
|
427
|
+
const path = ws.data?.path || '';
|
|
428
|
+
ws.send(`Room ${path}: ${message}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
app.registerWebSocketGateway(RoomGateway);
|
|
433
|
+
await app.listen();
|
|
434
|
+
|
|
435
|
+
const ws = new WebSocket(`ws://localhost:${port}/ws/rooms`);
|
|
436
|
+
|
|
437
|
+
await new Promise<void>((resolve, reject) => {
|
|
438
|
+
const timeout = setTimeout(() => {
|
|
439
|
+
reject(new Error('WebSocket connection timeout'));
|
|
440
|
+
}, 2000);
|
|
441
|
+
|
|
442
|
+
ws.onopen = () => {
|
|
443
|
+
clearTimeout(timeout);
|
|
444
|
+
resolve();
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
ws.onerror = (error) => {
|
|
448
|
+
clearTimeout(timeout);
|
|
449
|
+
reject(error);
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
454
|
+
|
|
455
|
+
// 验证路径被正确保存
|
|
456
|
+
expect(RoomGateway.paths.length).toBeGreaterThan(0);
|
|
457
|
+
expect(RoomGateway.paths[0]).toBe('/ws/rooms');
|
|
458
|
+
ws.close();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test('should extract and provide query parameters from WebSocket URL', async () => {
|
|
462
|
+
@WebSocketGateway('/ws/query-test')
|
|
463
|
+
class QueryParamGateway {
|
|
464
|
+
public static queryParams: Record<string, string>[] = [];
|
|
465
|
+
public static paths: string[] = [];
|
|
466
|
+
|
|
467
|
+
@OnOpen
|
|
468
|
+
public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
469
|
+
const path = ws.data?.path || '';
|
|
470
|
+
QueryParamGateway.paths.push(path);
|
|
471
|
+
|
|
472
|
+
// 尝试从 URL 中提取查询参数
|
|
473
|
+
// 注意:当前实现可能不支持,这个测试可以验证功能是否存在
|
|
474
|
+
const urlMatch = path.match(/\?(.+)$/);
|
|
475
|
+
if (urlMatch) {
|
|
476
|
+
const params: Record<string, string> = {};
|
|
477
|
+
urlMatch[1].split('&').forEach((pair) => {
|
|
478
|
+
const [key, value] = pair.split('=');
|
|
479
|
+
if (key && value) {
|
|
480
|
+
params[decodeURIComponent(key)] = decodeURIComponent(value);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
QueryParamGateway.queryParams.push(params);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
ws.send(`Query params extracted: ${JSON.stringify(QueryParamGateway.queryParams)}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
@OnMessage
|
|
490
|
+
public handleMessage(
|
|
491
|
+
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
492
|
+
message: string,
|
|
493
|
+
): void {
|
|
494
|
+
ws.send(`Received with query: ${message}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
app.registerWebSocketGateway(QueryParamGateway);
|
|
499
|
+
await app.listen();
|
|
500
|
+
|
|
501
|
+
const userId = 'user-456';
|
|
502
|
+
const token = 'token-789';
|
|
503
|
+
const ws = new WebSocket(
|
|
504
|
+
`ws://localhost:${port}/ws/query-test?userId=${userId}&token=${token}`,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
await new Promise<void>((resolve, reject) => {
|
|
508
|
+
const timeout = setTimeout(() => {
|
|
509
|
+
reject(new Error('WebSocket connection timeout'));
|
|
510
|
+
}, 2000);
|
|
511
|
+
|
|
512
|
+
ws.onopen = () => {
|
|
513
|
+
clearTimeout(timeout);
|
|
514
|
+
resolve();
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
ws.onerror = (error) => {
|
|
518
|
+
clearTimeout(timeout);
|
|
519
|
+
reject(error);
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
524
|
+
|
|
525
|
+
// 验证路径被保存(可能包含或不包含查询参数,取决于实现)
|
|
526
|
+
expect(QueryParamGateway.paths.length).toBeGreaterThan(0);
|
|
527
|
+
// 如果支持查询参数提取,应该能提取到参数
|
|
528
|
+
// 注意:当前实现可能不支持,这个测试可以验证功能是否存在
|
|
529
|
+
ws.close();
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test('should support path parameter extraction (manual parsing)', async () => {
|
|
533
|
+
// 注意:当前实现不支持动态路径参数和 @Param 装饰器
|
|
534
|
+
// 这个测试演示如何手动从路径中提取参数,作为未来功能的参考
|
|
535
|
+
@WebSocketGateway('/ws/param-test')
|
|
536
|
+
class ParamGateway {
|
|
537
|
+
public static receivedParams: string[] = [];
|
|
538
|
+
public static extractedIds: string[] = [];
|
|
539
|
+
|
|
540
|
+
@OnOpen
|
|
541
|
+
public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
542
|
+
const path = ws.data?.path || '';
|
|
543
|
+
// 手动从路径中提取参数(如果路径包含参数)
|
|
544
|
+
// 例如:如果路径是 /ws/param-test/123,提取 123
|
|
545
|
+
const match = path.match(/\/ws\/param-test\/([^/?]+)/);
|
|
546
|
+
if (match) {
|
|
547
|
+
ParamGateway.extractedIds.push(match[1]);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
@OnMessage
|
|
552
|
+
public handleMessage(
|
|
553
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
554
|
+
message: string,
|
|
555
|
+
): void {
|
|
556
|
+
ParamGateway.receivedParams.push(message);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
app.registerWebSocketGateway(ParamGateway);
|
|
561
|
+
await app.listen();
|
|
562
|
+
|
|
563
|
+
// 使用精确路径(当前实现要求)
|
|
564
|
+
const ws = new WebSocket(`ws://localhost:${port}/ws/param-test`);
|
|
565
|
+
|
|
566
|
+
await new Promise<void>((resolve, reject) => {
|
|
567
|
+
const timeout = setTimeout(() => {
|
|
568
|
+
reject(new Error('WebSocket connection timeout'));
|
|
569
|
+
}, 2000);
|
|
570
|
+
|
|
571
|
+
ws.onopen = () => {
|
|
572
|
+
clearTimeout(timeout);
|
|
573
|
+
resolve();
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
ws.onerror = (error) => {
|
|
577
|
+
clearTimeout(timeout);
|
|
578
|
+
reject(error);
|
|
579
|
+
};
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
583
|
+
ws.send('test-message');
|
|
584
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
585
|
+
|
|
586
|
+
// 验证消息被接收
|
|
587
|
+
expect(ParamGateway.receivedParams).toContain('test-message');
|
|
588
|
+
ws.close();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test('should support @Query decorator for query parameters', async () => {
|
|
592
|
+
// 注意:这个测试假设 WebSocket 支持 @Query 装饰器
|
|
593
|
+
// 如果功能不存在,测试会失败,这样可以明确需要实现该功能
|
|
594
|
+
@WebSocketGateway('/ws/query-decorator-test')
|
|
595
|
+
class QueryDecoratorGateway {
|
|
596
|
+
public static receivedQueries: Record<string, string>[] = [];
|
|
597
|
+
|
|
598
|
+
@OnMessage
|
|
599
|
+
public handleMessage(
|
|
600
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
601
|
+
message: string,
|
|
602
|
+
// 假设支持 @Query 装饰器
|
|
603
|
+
// @Query('userId') userId: string,
|
|
604
|
+
// @Query('token') token: string,
|
|
605
|
+
): void {
|
|
606
|
+
// 如果支持 @Query,这里应该能获取到查询参数
|
|
607
|
+
// 当前实现可能不支持,这个测试可以验证功能是否存在
|
|
608
|
+
QueryDecoratorGateway.receivedQueries.push({ message });
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
app.registerWebSocketGateway(QueryDecoratorGateway);
|
|
613
|
+
await app.listen();
|
|
614
|
+
|
|
615
|
+
const userId = 'user-999';
|
|
616
|
+
const token = 'token-888';
|
|
617
|
+
const ws = new WebSocket(
|
|
618
|
+
`ws://localhost:${port}/ws/query-decorator-test?userId=${userId}&token=${token}`,
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
await new Promise<void>((resolve, reject) => {
|
|
622
|
+
const timeout = setTimeout(() => {
|
|
623
|
+
reject(new Error('WebSocket connection timeout'));
|
|
624
|
+
}, 2000);
|
|
625
|
+
|
|
626
|
+
ws.onopen = () => {
|
|
627
|
+
clearTimeout(timeout);
|
|
628
|
+
resolve();
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
ws.onerror = (error) => {
|
|
632
|
+
clearTimeout(timeout);
|
|
633
|
+
reject(error);
|
|
634
|
+
};
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
638
|
+
ws.send('test-query');
|
|
639
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
640
|
+
|
|
641
|
+
// 验证消息被接收(查询参数提取功能可能需要额外实现)
|
|
642
|
+
expect(QueryDecoratorGateway.receivedQueries.length).toBeGreaterThan(0);
|
|
643
|
+
ws.close();
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test('should support @Context decorator for accessing request context', async () => {
|
|
647
|
+
// 注意:这个测试假设 WebSocket 支持 @Context 装饰器
|
|
648
|
+
// 如果功能不存在,测试会失败,这样可以明确需要实现该功能
|
|
649
|
+
@WebSocketGateway('/ws/context-test')
|
|
650
|
+
class ContextGateway {
|
|
651
|
+
public static contexts: unknown[] = [];
|
|
652
|
+
|
|
653
|
+
@OnOpen
|
|
654
|
+
public handleOpen(
|
|
655
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
656
|
+
// 假设支持 @Context 装饰器
|
|
657
|
+
// @Context() context: Context,
|
|
658
|
+
): void {
|
|
659
|
+
// 如果支持 @Context,这里应该能获取到 Context 对象
|
|
660
|
+
// 当前实现可能不支持,这个测试可以验证功能是否存在
|
|
661
|
+
ContextGateway.contexts.push('open');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
@OnMessage
|
|
665
|
+
public handleMessage(
|
|
666
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
667
|
+
message: string,
|
|
668
|
+
// 假设支持 @Context 装饰器
|
|
669
|
+
// @Context() context: Context,
|
|
670
|
+
): void {
|
|
671
|
+
// 如果支持 @Context,这里应该能访问 context.query, context.headers 等
|
|
672
|
+
// 当前实现可能不支持,这个测试可以验证功能是否存在
|
|
673
|
+
ContextGateway.contexts.push(`message:${message}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
app.registerWebSocketGateway(ContextGateway);
|
|
678
|
+
await app.listen();
|
|
679
|
+
|
|
680
|
+
const ws = new WebSocket(`ws://localhost:${port}/ws/context-test?key=value`);
|
|
681
|
+
|
|
682
|
+
await new Promise<void>((resolve, reject) => {
|
|
683
|
+
const timeout = setTimeout(() => {
|
|
684
|
+
reject(new Error('WebSocket connection timeout'));
|
|
685
|
+
}, 2000);
|
|
686
|
+
|
|
687
|
+
ws.onopen = () => {
|
|
688
|
+
clearTimeout(timeout);
|
|
689
|
+
resolve();
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
ws.onerror = (error) => {
|
|
693
|
+
clearTimeout(timeout);
|
|
694
|
+
reject(error);
|
|
695
|
+
};
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
699
|
+
ws.send('test-context');
|
|
700
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
701
|
+
|
|
702
|
+
// 验证事件被处理(Context 功能可能需要额外实现)
|
|
703
|
+
expect(ContextGateway.contexts.length).toBeGreaterThan(0);
|
|
704
|
+
expect(ContextGateway.contexts).toContain('open');
|
|
705
|
+
expect(ContextGateway.contexts.some((c) => String(c).includes('test-context'))).toBe(true);
|
|
706
|
+
ws.close();
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test('should handle query parameters with path information together', async () => {
|
|
710
|
+
// 注意:当前实现不支持动态路径参数,但支持查询参数
|
|
711
|
+
@WebSocketGateway('/ws/combined')
|
|
712
|
+
class CombinedGateway {
|
|
713
|
+
public static data: {
|
|
714
|
+
path?: string;
|
|
715
|
+
query?: Record<string, string>;
|
|
716
|
+
}[] = [];
|
|
717
|
+
|
|
718
|
+
@OnOpen
|
|
719
|
+
public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
720
|
+
const path = ws.data?.path || '';
|
|
721
|
+
const data: {
|
|
722
|
+
path?: string;
|
|
723
|
+
query?: Record<string, string>;
|
|
724
|
+
} = { path };
|
|
725
|
+
|
|
726
|
+
// 提取查询参数(从完整 URL 中)
|
|
727
|
+
// 注意:ws.data.path 可能包含或不包含查询参数,取决于实现
|
|
728
|
+
const queryMatch = path.match(/\?(.+)$/);
|
|
729
|
+
if (queryMatch) {
|
|
730
|
+
const params: Record<string, string> = {};
|
|
731
|
+
queryMatch[1].split('&').forEach((pair) => {
|
|
732
|
+
const [key, value] = pair.split('=');
|
|
733
|
+
if (key && value) {
|
|
734
|
+
params[decodeURIComponent(key)] = decodeURIComponent(value);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
data.query = params;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
CombinedGateway.data.push(data);
|
|
741
|
+
ws.send(`Combined: ${JSON.stringify(data)}`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
@OnMessage
|
|
745
|
+
public handleMessage(
|
|
746
|
+
ws: ServerWebSocket<WebSocketConnectionData>,
|
|
747
|
+
message: string,
|
|
748
|
+
): void {
|
|
749
|
+
ws.send(`Message: ${message}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
app.registerWebSocketGateway(CombinedGateway);
|
|
754
|
+
await app.listen();
|
|
755
|
+
|
|
756
|
+
const userId = 'user-xyz';
|
|
757
|
+
const ws = new WebSocket(
|
|
758
|
+
`ws://localhost:${port}/ws/combined?userId=${userId}`,
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
await new Promise<void>((resolve, reject) => {
|
|
762
|
+
const timeout = setTimeout(() => {
|
|
763
|
+
reject(new Error('WebSocket connection timeout'));
|
|
764
|
+
}, 2000);
|
|
765
|
+
|
|
766
|
+
ws.onopen = () => {
|
|
767
|
+
clearTimeout(timeout);
|
|
768
|
+
resolve();
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
ws.onerror = (error) => {
|
|
772
|
+
clearTimeout(timeout);
|
|
773
|
+
reject(error);
|
|
774
|
+
};
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
778
|
+
|
|
779
|
+
// 验证数据被收集
|
|
780
|
+
expect(CombinedGateway.data.length).toBeGreaterThan(0);
|
|
781
|
+
expect(CombinedGateway.data[0].path).toBeDefined();
|
|
782
|
+
ws.close();
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test('should create new instance for each WebSocket connection (dynamic instance creation)', async () => {
|
|
786
|
+
@WebSocketGateway('/ws/dynamic-instance')
|
|
787
|
+
class DynamicInstanceGateway {
|
|
788
|
+
public static instanceIds: number[] = [];
|
|
789
|
+
public static callCounts: Map<number, number> = new Map();
|
|
790
|
+
private readonly instanceId: number;
|
|
791
|
+
|
|
792
|
+
public constructor() {
|
|
793
|
+
this.instanceId = Math.random();
|
|
794
|
+
DynamicInstanceGateway.instanceIds.push(this.instanceId);
|
|
795
|
+
DynamicInstanceGateway.callCounts.set(this.instanceId, 0);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
@OnOpen
|
|
799
|
+
public handleOpen(ws: ServerWebSocket<WebSocketConnectionData>): void {
|
|
800
|
+
const count = DynamicInstanceGateway.callCounts.get(this.instanceId) || 0;
|
|
801
|
+
DynamicInstanceGateway.callCounts.set(this.instanceId, count + 1);
|
|
802
|
+
ws.send(`Instance ID: ${this.instanceId}`);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
@OnMessage
|
|
806
|
+
public handleMessage(
|
|
807
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
808
|
+
message: string,
|
|
809
|
+
): void {
|
|
810
|
+
const count = DynamicInstanceGateway.callCounts.get(this.instanceId) || 0;
|
|
811
|
+
DynamicInstanceGateway.callCounts.set(this.instanceId, count + 1);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
app.registerWebSocketGateway(DynamicInstanceGateway);
|
|
816
|
+
await app.listen();
|
|
817
|
+
|
|
818
|
+
// 创建第一个连接
|
|
819
|
+
const ws1 = new WebSocket(`ws://localhost:${port}/ws/dynamic-instance`);
|
|
820
|
+
await new Promise<void>((resolve, reject) => {
|
|
821
|
+
const timeout = setTimeout(() => {
|
|
822
|
+
reject(new Error('WebSocket connection timeout'));
|
|
823
|
+
}, 2000);
|
|
824
|
+
|
|
825
|
+
ws1.onopen = () => {
|
|
826
|
+
clearTimeout(timeout);
|
|
827
|
+
resolve();
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
ws1.onerror = (error) => {
|
|
831
|
+
clearTimeout(timeout);
|
|
832
|
+
reject(error);
|
|
833
|
+
};
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
837
|
+
const firstInstanceCount = DynamicInstanceGateway.instanceIds.length;
|
|
838
|
+
|
|
839
|
+
// 创建第二个连接
|
|
840
|
+
const ws2 = new WebSocket(`ws://localhost:${port}/ws/dynamic-instance`);
|
|
841
|
+
await new Promise<void>((resolve, reject) => {
|
|
842
|
+
const timeout = setTimeout(() => {
|
|
843
|
+
reject(new Error('WebSocket connection timeout'));
|
|
844
|
+
}, 2000);
|
|
845
|
+
|
|
846
|
+
ws2.onopen = () => {
|
|
847
|
+
clearTimeout(timeout);
|
|
848
|
+
resolve();
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
ws2.onerror = (error) => {
|
|
852
|
+
clearTimeout(timeout);
|
|
853
|
+
reject(error);
|
|
854
|
+
};
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
858
|
+
|
|
859
|
+
// 验证创建了实例(至少一个)
|
|
860
|
+
expect(DynamicInstanceGateway.instanceIds.length).toBeGreaterThan(0);
|
|
861
|
+
// 验证每个连接都调用了处理器(说明实例被使用)
|
|
862
|
+
const totalCalls = Array.from(DynamicInstanceGateway.callCounts.values()).reduce(
|
|
863
|
+
(sum, count) => sum + count,
|
|
864
|
+
0,
|
|
865
|
+
);
|
|
866
|
+
expect(totalCalls).toBeGreaterThan(0);
|
|
867
|
+
|
|
868
|
+
ws1.close();
|
|
869
|
+
ws2.close();
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
test('should support @Query decorator in WebSocket handlers', async () => {
|
|
873
|
+
@WebSocketGateway('/ws/query-decorator')
|
|
874
|
+
class QueryDecoratorGateway {
|
|
875
|
+
public static receivedQueries: Record<string, string>[] = [];
|
|
876
|
+
|
|
877
|
+
@OnOpen
|
|
878
|
+
public handleOpen(
|
|
879
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
880
|
+
@Query('userId') userId: string | null,
|
|
881
|
+
@Query('token') token: string | null,
|
|
882
|
+
): void {
|
|
883
|
+
QueryDecoratorGateway.receivedQueries.push({
|
|
884
|
+
userId: userId || '',
|
|
885
|
+
token: token || '',
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
@OnMessage
|
|
890
|
+
public handleMessage(
|
|
891
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
892
|
+
message: string,
|
|
893
|
+
@Query('userId') userId: string | null,
|
|
894
|
+
): void {
|
|
895
|
+
QueryDecoratorGateway.receivedQueries.push({
|
|
896
|
+
message,
|
|
897
|
+
userId: userId || '',
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
app.registerWebSocketGateway(QueryDecoratorGateway);
|
|
903
|
+
await app.listen();
|
|
904
|
+
|
|
905
|
+
const userId = 'user-123';
|
|
906
|
+
const token = 'token-456';
|
|
907
|
+
const ws = new WebSocket(
|
|
908
|
+
`ws://localhost:${port}/ws/query-decorator?userId=${userId}&token=${token}`,
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
await new Promise<void>((resolve, reject) => {
|
|
912
|
+
const timeout = setTimeout(() => {
|
|
913
|
+
reject(new Error('WebSocket connection timeout'));
|
|
914
|
+
}, 2000);
|
|
915
|
+
|
|
916
|
+
ws.onopen = () => {
|
|
917
|
+
clearTimeout(timeout);
|
|
918
|
+
resolve();
|
|
919
|
+
};
|
|
920
|
+
|
|
921
|
+
ws.onerror = (error) => {
|
|
922
|
+
clearTimeout(timeout);
|
|
923
|
+
reject(error);
|
|
924
|
+
};
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
928
|
+
|
|
929
|
+
// 验证 @Query 装饰器正确提取了查询参数
|
|
930
|
+
expect(QueryDecoratorGateway.receivedQueries.length).toBeGreaterThan(0);
|
|
931
|
+
const openQuery = QueryDecoratorGateway.receivedQueries.find(
|
|
932
|
+
(q) => q.userId === userId && q.token === token,
|
|
933
|
+
);
|
|
934
|
+
expect(openQuery).toBeDefined();
|
|
935
|
+
|
|
936
|
+
ws.send('test-message');
|
|
937
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
938
|
+
|
|
939
|
+
// 验证消息处理中的 @Query 也工作
|
|
940
|
+
const messageQuery = QueryDecoratorGateway.receivedQueries.find(
|
|
941
|
+
(q) => q.message === 'test-message' && q.userId === userId,
|
|
942
|
+
);
|
|
943
|
+
expect(messageQuery).toBeDefined();
|
|
944
|
+
|
|
945
|
+
ws.close();
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
test('should support @Context decorator in WebSocket handlers', async () => {
|
|
949
|
+
@WebSocketGateway('/ws/context-decorator')
|
|
950
|
+
class ContextDecoratorGateway {
|
|
951
|
+
public static contexts: {
|
|
952
|
+
path?: string;
|
|
953
|
+
query?: string;
|
|
954
|
+
hasContext?: boolean;
|
|
955
|
+
}[] = [];
|
|
956
|
+
|
|
957
|
+
@OnOpen
|
|
958
|
+
public handleOpen(
|
|
959
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
960
|
+
@Context() context: RequestContext,
|
|
961
|
+
): void {
|
|
962
|
+
ContextDecoratorGateway.contexts.push({
|
|
963
|
+
path: context.path,
|
|
964
|
+
query: context.getQuery('userId') || undefined,
|
|
965
|
+
hasContext: context !== undefined,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
@OnMessage
|
|
970
|
+
public handleMessage(
|
|
971
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
972
|
+
message: string,
|
|
973
|
+
@Context() context: RequestContext,
|
|
974
|
+
): void {
|
|
975
|
+
ContextDecoratorGateway.contexts.push({
|
|
976
|
+
path: context.path,
|
|
977
|
+
query: context.getQuery('userId') || undefined,
|
|
978
|
+
hasContext: context !== undefined,
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
app.registerWebSocketGateway(ContextDecoratorGateway);
|
|
984
|
+
await app.listen();
|
|
985
|
+
|
|
986
|
+
const userId = 'user-789';
|
|
987
|
+
const ws = new WebSocket(
|
|
988
|
+
`ws://localhost:${port}/ws/context-decorator?userId=${userId}`,
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
await new Promise<void>((resolve, reject) => {
|
|
992
|
+
const timeout = setTimeout(() => {
|
|
993
|
+
reject(new Error('WebSocket connection timeout'));
|
|
994
|
+
}, 2000);
|
|
995
|
+
|
|
996
|
+
ws.onopen = () => {
|
|
997
|
+
clearTimeout(timeout);
|
|
998
|
+
resolve();
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
ws.onerror = (error) => {
|
|
1002
|
+
clearTimeout(timeout);
|
|
1003
|
+
reject(error);
|
|
1004
|
+
};
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1008
|
+
|
|
1009
|
+
// 验证 @Context 装饰器正确注入了 Context 对象
|
|
1010
|
+
expect(ContextDecoratorGateway.contexts.length).toBeGreaterThan(0);
|
|
1011
|
+
const openContext = ContextDecoratorGateway.contexts[0];
|
|
1012
|
+
expect(openContext.hasContext).toBe(true);
|
|
1013
|
+
expect(openContext.path).toBe('/ws/context-decorator');
|
|
1014
|
+
expect(openContext.query).toBe(userId);
|
|
1015
|
+
|
|
1016
|
+
ws.send('test-context');
|
|
1017
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1018
|
+
|
|
1019
|
+
// 验证消息处理中的 @Context 也工作
|
|
1020
|
+
const messageContext = ContextDecoratorGateway.contexts.find(
|
|
1021
|
+
(c) => c.path === '/ws/context-decorator' && c.query === userId,
|
|
1022
|
+
);
|
|
1023
|
+
expect(messageContext).toBeDefined();
|
|
1024
|
+
expect(messageContext?.hasContext).toBe(true);
|
|
1025
|
+
|
|
1026
|
+
ws.close();
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
test('should support @Query and @Context decorators together', async () => {
|
|
1030
|
+
@WebSocketGateway('/ws/combined-decorators')
|
|
1031
|
+
class CombinedDecoratorsGateway {
|
|
1032
|
+
public static results: {
|
|
1033
|
+
queryValue?: string | null;
|
|
1034
|
+
contextPath?: string;
|
|
1035
|
+
hasContext?: boolean;
|
|
1036
|
+
}[] = [];
|
|
1037
|
+
|
|
1038
|
+
@OnMessage
|
|
1039
|
+
public handleMessage(
|
|
1040
|
+
_ws: ServerWebSocket<WebSocketConnectionData>,
|
|
1041
|
+
message: string,
|
|
1042
|
+
@Query('roomId') roomId: string | null,
|
|
1043
|
+
@Context() context: RequestContext,
|
|
1044
|
+
): void {
|
|
1045
|
+
CombinedDecoratorsGateway.results.push({
|
|
1046
|
+
queryValue: roomId,
|
|
1047
|
+
contextPath: context.path,
|
|
1048
|
+
hasContext: context !== undefined,
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
app.registerWebSocketGateway(CombinedDecoratorsGateway);
|
|
1054
|
+
await app.listen();
|
|
1055
|
+
|
|
1056
|
+
const roomId = 'room-abc';
|
|
1057
|
+
const ws = new WebSocket(
|
|
1058
|
+
`ws://localhost:${port}/ws/combined-decorators?roomId=${roomId}`,
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
await new Promise<void>((resolve, reject) => {
|
|
1062
|
+
const timeout = setTimeout(() => {
|
|
1063
|
+
reject(new Error('WebSocket connection timeout'));
|
|
1064
|
+
}, 2000);
|
|
1065
|
+
|
|
1066
|
+
ws.onopen = () => {
|
|
1067
|
+
clearTimeout(timeout);
|
|
1068
|
+
resolve();
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
ws.onerror = (error) => {
|
|
1072
|
+
clearTimeout(timeout);
|
|
1073
|
+
reject(error);
|
|
1074
|
+
};
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1078
|
+
ws.send('test-combined');
|
|
1079
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
1080
|
+
|
|
1081
|
+
// 验证 @Query 和 @Context 同时工作
|
|
1082
|
+
expect(CombinedDecoratorsGateway.results.length).toBeGreaterThan(0);
|
|
1083
|
+
const result = CombinedDecoratorsGateway.results[0];
|
|
1084
|
+
expect(result.queryValue).toBe(roomId);
|
|
1085
|
+
expect(result.contextPath).toBe('/ws/combined-decorators');
|
|
1086
|
+
expect(result.hasContext).toBe(true);
|
|
1087
|
+
|
|
1088
|
+
ws.close();
|
|
1089
|
+
});
|
|
1090
|
+
});
|
|
1091
|
+
|
|
68
1092
|
|