@onebun/core 0.1.10 → 0.1.12
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/package.json +7 -6
- package/src/application/application.test.ts +92 -0
- package/src/application/application.ts +50 -13
- package/src/docs-examples.test.ts +9 -2
- package/src/index.ts +11 -1
- package/src/module/config.interface.ts +60 -0
- package/src/module/config.service.test.ts +56 -32
- package/src/module/config.service.ts +26 -9
- package/src/module/controller.test.ts +27 -29
- package/src/module/controller.ts +3 -2
- package/src/module/index.ts +1 -0
- package/src/module/module.test.ts +31 -32
- package/src/module/module.ts +9 -5
- package/src/module/service.test.ts +22 -20
- package/src/module/service.ts +5 -3
- package/src/queue/adapters/memory.adapter.test.ts +19 -3
- package/src/queue/adapters/redis.adapter.test.ts +289 -0
- package/src/queue/queue.service.test.ts +240 -0
- package/src/queue/scheduler.test.ts +22 -9
- package/src/redis/shared-redis.test.ts +255 -0
- package/src/testing/test-utils.ts +56 -0
- package/src/websocket/ws-client.test.ts +96 -48
- package/src/websocket/ws-integration.test.ts +21 -19
- package/src/websocket/ws-storage-redis.test.ts +517 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Redis Provider Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests using testcontainers for real Redis integration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
afterAll,
|
|
9
|
+
afterEach,
|
|
10
|
+
beforeAll,
|
|
11
|
+
describe,
|
|
12
|
+
expect,
|
|
13
|
+
it,
|
|
14
|
+
} from 'bun:test';
|
|
15
|
+
import { Effect, pipe } from 'effect';
|
|
16
|
+
import {
|
|
17
|
+
GenericContainer,
|
|
18
|
+
type StartedTestContainer,
|
|
19
|
+
Wait,
|
|
20
|
+
} from 'testcontainers';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
SharedRedisProvider,
|
|
24
|
+
SharedRedisService,
|
|
25
|
+
makeSharedRedisLayer,
|
|
26
|
+
getSharedRedis,
|
|
27
|
+
} from './shared-redis';
|
|
28
|
+
|
|
29
|
+
describe('SharedRedisProvider', () => {
|
|
30
|
+
let redisContainer: StartedTestContainer;
|
|
31
|
+
let redisUrl: string;
|
|
32
|
+
|
|
33
|
+
beforeAll(async () => {
|
|
34
|
+
// Start Redis container
|
|
35
|
+
redisContainer = await new GenericContainer('redis:7-alpine')
|
|
36
|
+
.withExposedPorts(6379)
|
|
37
|
+
.withWaitStrategy(Wait.forLogMessage(/.*Ready to accept connections.*/))
|
|
38
|
+
.withStartupTimeout(30000)
|
|
39
|
+
.withLogConsumer(() => {
|
|
40
|
+
// Suppress container logs
|
|
41
|
+
})
|
|
42
|
+
.start();
|
|
43
|
+
|
|
44
|
+
const host = redisContainer.getHost();
|
|
45
|
+
const port = redisContainer.getMappedPort(6379);
|
|
46
|
+
redisUrl = `redis://${host}:${port}`;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterAll(async () => {
|
|
50
|
+
await SharedRedisProvider.reset();
|
|
51
|
+
if (redisContainer) {
|
|
52
|
+
await redisContainer.stop();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
afterEach(async () => {
|
|
57
|
+
await SharedRedisProvider.reset();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('configuration', () => {
|
|
61
|
+
it('should configure the provider', () => {
|
|
62
|
+
expect(SharedRedisProvider.isConfigured()).toBe(false);
|
|
63
|
+
|
|
64
|
+
SharedRedisProvider.configure({ url: redisUrl });
|
|
65
|
+
|
|
66
|
+
expect(SharedRedisProvider.isConfigured()).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should throw when getClient called without configuration', async () => {
|
|
70
|
+
await expect(SharedRedisProvider.getClient()).rejects.toThrow(
|
|
71
|
+
'SharedRedisProvider not configured',
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('connection', () => {
|
|
77
|
+
it('should connect and return a client', async () => {
|
|
78
|
+
SharedRedisProvider.configure({
|
|
79
|
+
url: redisUrl,
|
|
80
|
+
keyPrefix: 'test:',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const client = await SharedRedisProvider.getClient();
|
|
84
|
+
|
|
85
|
+
expect(client).toBeDefined();
|
|
86
|
+
expect(client.isConnected()).toBe(true);
|
|
87
|
+
expect(SharedRedisProvider.isConnected()).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return the same instance on multiple calls', async () => {
|
|
91
|
+
SharedRedisProvider.configure({ url: redisUrl });
|
|
92
|
+
|
|
93
|
+
const client1 = await SharedRedisProvider.getClient();
|
|
94
|
+
const client2 = await SharedRedisProvider.getClient();
|
|
95
|
+
|
|
96
|
+
expect(client1).toBe(client2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle concurrent getClient calls', async () => {
|
|
100
|
+
SharedRedisProvider.configure({ url: redisUrl });
|
|
101
|
+
|
|
102
|
+
// Call getClient multiple times concurrently
|
|
103
|
+
const [client1, client2, client3] = await Promise.all([
|
|
104
|
+
SharedRedisProvider.getClient(),
|
|
105
|
+
SharedRedisProvider.getClient(),
|
|
106
|
+
SharedRedisProvider.getClient(),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
// All should be the same instance
|
|
110
|
+
expect(client1).toBe(client2);
|
|
111
|
+
expect(client2).toBe(client3);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should disconnect properly', async () => {
|
|
115
|
+
SharedRedisProvider.configure({ url: redisUrl });
|
|
116
|
+
|
|
117
|
+
await SharedRedisProvider.getClient();
|
|
118
|
+
expect(SharedRedisProvider.isConnected()).toBe(true);
|
|
119
|
+
|
|
120
|
+
await SharedRedisProvider.disconnect();
|
|
121
|
+
|
|
122
|
+
expect(SharedRedisProvider.isConnected()).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should reconnect after disconnect', async () => {
|
|
126
|
+
SharedRedisProvider.configure({ url: redisUrl });
|
|
127
|
+
|
|
128
|
+
const client1 = await SharedRedisProvider.getClient();
|
|
129
|
+
await SharedRedisProvider.disconnect();
|
|
130
|
+
|
|
131
|
+
const client2 = await SharedRedisProvider.getClient();
|
|
132
|
+
|
|
133
|
+
expect(client2).toBeDefined();
|
|
134
|
+
expect(client2.isConnected()).toBe(true);
|
|
135
|
+
// After disconnect and reconnect, we get a new instance
|
|
136
|
+
expect(client1).not.toBe(client2);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('client operations', () => {
|
|
141
|
+
it('should perform basic Redis operations', async () => {
|
|
142
|
+
SharedRedisProvider.configure({
|
|
143
|
+
url: redisUrl,
|
|
144
|
+
keyPrefix: 'test:ops:',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const client = await SharedRedisProvider.getClient();
|
|
148
|
+
|
|
149
|
+
// Set and get
|
|
150
|
+
await client.set('testkey', 'testvalue');
|
|
151
|
+
const value = await client.get('testkey');
|
|
152
|
+
expect(value).toBe('testvalue');
|
|
153
|
+
|
|
154
|
+
// Delete
|
|
155
|
+
await client.del('testkey');
|
|
156
|
+
const deleted = await client.get('testkey');
|
|
157
|
+
expect(deleted).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('createClient', () => {
|
|
162
|
+
it('should create standalone client with custom URL', async () => {
|
|
163
|
+
SharedRedisProvider.configure({ url: redisUrl });
|
|
164
|
+
|
|
165
|
+
const standalone = SharedRedisProvider.createClient({
|
|
166
|
+
url: redisUrl,
|
|
167
|
+
keyPrefix: 'standalone:',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await standalone.connect();
|
|
171
|
+
expect(standalone.isConnected()).toBe(true);
|
|
172
|
+
|
|
173
|
+
// Should be different from shared client
|
|
174
|
+
const shared = await SharedRedisProvider.getClient();
|
|
175
|
+
expect(standalone).not.toBe(shared);
|
|
176
|
+
|
|
177
|
+
await standalone.disconnect();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should use base options when not specified', async () => {
|
|
181
|
+
SharedRedisProvider.configure({
|
|
182
|
+
url: redisUrl,
|
|
183
|
+
keyPrefix: 'base:',
|
|
184
|
+
reconnect: true,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const standalone = SharedRedisProvider.createClient({});
|
|
188
|
+
|
|
189
|
+
await standalone.connect();
|
|
190
|
+
expect(standalone.isConnected()).toBe(true);
|
|
191
|
+
await standalone.disconnect();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should throw when URL is not available', () => {
|
|
195
|
+
// Don't configure provider
|
|
196
|
+
expect(() => SharedRedisProvider.createClient({})).toThrow('Redis URL is required');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('reset', () => {
|
|
201
|
+
it('should reset provider state completely', async () => {
|
|
202
|
+
SharedRedisProvider.configure({ url: redisUrl });
|
|
203
|
+
await SharedRedisProvider.getClient();
|
|
204
|
+
|
|
205
|
+
expect(SharedRedisProvider.isConnected()).toBe(true);
|
|
206
|
+
expect(SharedRedisProvider.isConfigured()).toBe(true);
|
|
207
|
+
|
|
208
|
+
await SharedRedisProvider.reset();
|
|
209
|
+
|
|
210
|
+
expect(SharedRedisProvider.isConnected()).toBe(false);
|
|
211
|
+
expect(SharedRedisProvider.isConfigured()).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('Effect.js integration', () => {
|
|
216
|
+
it('should work with makeSharedRedisLayer', async () => {
|
|
217
|
+
const layer = makeSharedRedisLayer({
|
|
218
|
+
url: redisUrl,
|
|
219
|
+
keyPrefix: 'effect:',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const program = pipe(
|
|
223
|
+
SharedRedisService,
|
|
224
|
+
Effect.flatMap((redis) =>
|
|
225
|
+
Effect.promise(async () => {
|
|
226
|
+
await redis.set('effect-test', 'value');
|
|
227
|
+
|
|
228
|
+
return await redis.get('effect-test');
|
|
229
|
+
}),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const result = await Effect.runPromise(Effect.provide(program, layer));
|
|
234
|
+
|
|
235
|
+
expect(result).toBe('value');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should fail getSharedRedis when not configured', async () => {
|
|
239
|
+
// Reset to ensure not configured
|
|
240
|
+
await SharedRedisProvider.reset();
|
|
241
|
+
|
|
242
|
+
const result = await Effect.runPromiseExit(getSharedRedis);
|
|
243
|
+
|
|
244
|
+
expect(result._tag).toBe('Failure');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should succeed getSharedRedis when configured', async () => {
|
|
248
|
+
SharedRedisProvider.configure({ url: redisUrl });
|
|
249
|
+
|
|
250
|
+
const result = await Effect.runPromiseExit(getSharedRedis);
|
|
251
|
+
|
|
252
|
+
expect(result._tag).toBe('Success');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
import { Effect, Layer } from 'effect';
|
|
7
7
|
|
|
8
|
+
import type { IConfig, OneBunAppConfig } from '../module/config.interface';
|
|
9
|
+
|
|
8
10
|
import type { Logger, SyncLogger } from '@onebun/logger';
|
|
9
11
|
import { LoggerService } from '@onebun/logger';
|
|
10
12
|
|
|
@@ -337,3 +339,57 @@ export function createMockSyncLogger(): SyncLogger {
|
|
|
337
339
|
export function makeMockLoggerLayer(): Layer.Layer<Logger, never, never> {
|
|
338
340
|
return Layer.succeed(LoggerService, createMockLogger());
|
|
339
341
|
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Create a mock config for testing.
|
|
345
|
+
* Returns an IConfig-compatible object with customizable values.
|
|
346
|
+
*
|
|
347
|
+
* @param values - Optional object with config values that get() will return
|
|
348
|
+
* @param options - Optional configuration options
|
|
349
|
+
* @returns An IConfig-compatible mock object
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```typescript
|
|
353
|
+
* const mockConfig = createMockConfig({
|
|
354
|
+
* 'server.port': 3000,
|
|
355
|
+
* 'server.host': '0.0.0.0'
|
|
356
|
+
* });
|
|
357
|
+
* controller.initializeController(mockLogger, mockConfig);
|
|
358
|
+
* ```
|
|
359
|
+
*/
|
|
360
|
+
export function createMockConfig(
|
|
361
|
+
values: Record<string, unknown> = {},
|
|
362
|
+
options?: { isInitialized?: boolean },
|
|
363
|
+
): IConfig<OneBunAppConfig> {
|
|
364
|
+
const isInitialized = options?.isInitialized ?? true;
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
get(path: string): unknown {
|
|
368
|
+
if (!isInitialized) {
|
|
369
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return values[path];
|
|
373
|
+
},
|
|
374
|
+
get values(): OneBunAppConfig {
|
|
375
|
+
if (!isInitialized) {
|
|
376
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return values as unknown as OneBunAppConfig;
|
|
380
|
+
},
|
|
381
|
+
getSafeConfig(): OneBunAppConfig {
|
|
382
|
+
if (!isInitialized) {
|
|
383
|
+
throw new Error('Configuration not initialized. Provide envSchema in ApplicationOptions.');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return values as unknown as OneBunAppConfig;
|
|
387
|
+
},
|
|
388
|
+
get isInitialized(): boolean {
|
|
389
|
+
return isInitialized;
|
|
390
|
+
},
|
|
391
|
+
async initialize(): Promise<void> {
|
|
392
|
+
// No-op for mock
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
}
|
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
|
|
14
14
|
import type { WsServiceDefinition } from './ws-service-definition';
|
|
15
15
|
|
|
16
|
+
import { useFakeTimers } from '../testing/test-utils';
|
|
17
|
+
|
|
16
18
|
import { createWsClient } from './ws-client';
|
|
17
19
|
import { WsConnectionState } from './ws-client.types';
|
|
18
20
|
import { WsHandlerType } from './ws.types';
|
|
@@ -359,50 +361,84 @@ describe('WsClient', () => {
|
|
|
359
361
|
});
|
|
360
362
|
|
|
361
363
|
it('should emit message and wait for acknowledgement', async () => {
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
event: '
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
364
|
+
const { advanceTime, restore } = useFakeTimers();
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const definition = createMockDefinition();
|
|
368
|
+
const client = createWsClient(definition, {
|
|
369
|
+
url: 'ws://localhost:3000',
|
|
370
|
+
timeout: 1000,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Advance time to trigger MockWebSocket's async open
|
|
374
|
+
const connectPromise = client.connect();
|
|
375
|
+
advanceTime(1);
|
|
376
|
+
await connectPromise;
|
|
377
|
+
|
|
378
|
+
const ws = MockWebSocket.getLastInstance();
|
|
379
|
+
|
|
380
|
+
// Start emit (returns promise)
|
|
381
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
382
|
+
const emitPromise = (client as any).TestGateway.emit('test:event', { foo: 'bar' });
|
|
383
|
+
|
|
384
|
+
// Advance time to process sent message
|
|
385
|
+
advanceTime(10);
|
|
386
|
+
const sent = JSON.parse(ws!.sentMessages[ws!.sentMessages.length - 1]);
|
|
387
|
+
ws?.receiveMessage(JSON.stringify({
|
|
388
|
+
event: 'ack',
|
|
389
|
+
data: { result: 'ok' },
|
|
390
|
+
ack: sent.ack,
|
|
391
|
+
}));
|
|
392
|
+
|
|
393
|
+
const result = await emitPromise;
|
|
394
|
+
expect(result).toEqual({ result: 'ok' });
|
|
395
|
+
} finally {
|
|
396
|
+
restore();
|
|
397
|
+
}
|
|
387
398
|
});
|
|
388
399
|
|
|
389
400
|
it('should timeout emit if no acknowledgement', async () => {
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
401
|
+
const { advanceTime, restore } = useFakeTimers();
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const definition = createMockDefinition();
|
|
405
|
+
const client = createWsClient(definition, {
|
|
406
|
+
url: 'ws://localhost:3000',
|
|
407
|
+
timeout: 50, // Short timeout for test
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Advance time to trigger MockWebSocket's async open
|
|
411
|
+
const connectPromise = client.connect();
|
|
412
|
+
advanceTime(1);
|
|
413
|
+
await connectPromise;
|
|
414
|
+
|
|
415
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
416
|
+
const emitPromise = (client as any).TestGateway.emit('test:event', { foo: 'bar' });
|
|
417
|
+
|
|
418
|
+
// Advance time past the timeout
|
|
419
|
+
advanceTime(100);
|
|
420
|
+
|
|
421
|
+
await expect(emitPromise).rejects.toThrow('Request timeout');
|
|
422
|
+
} finally {
|
|
423
|
+
restore();
|
|
424
|
+
}
|
|
402
425
|
});
|
|
403
426
|
});
|
|
404
427
|
|
|
405
428
|
describe('reconnection', () => {
|
|
429
|
+
let advanceTime: (ms: number) => void;
|
|
430
|
+
let restore: () => void;
|
|
431
|
+
|
|
432
|
+
beforeEach(() => {
|
|
433
|
+
const fakeTimers = useFakeTimers();
|
|
434
|
+
advanceTime = fakeTimers.advanceTime;
|
|
435
|
+
restore = fakeTimers.restore;
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
afterEach(() => {
|
|
439
|
+
restore();
|
|
440
|
+
});
|
|
441
|
+
|
|
406
442
|
it('should attempt reconnection on disconnect', async () => {
|
|
407
443
|
const definition = createMockDefinition();
|
|
408
444
|
const client = createWsClient(definition, {
|
|
@@ -413,15 +449,19 @@ describe('WsClient', () => {
|
|
|
413
449
|
});
|
|
414
450
|
const reconnectAttemptHandler = mock(() => undefined);
|
|
415
451
|
|
|
416
|
-
|
|
452
|
+
// Advance time to trigger MockWebSocket's async open
|
|
453
|
+
const connectPromise = client.connect();
|
|
454
|
+
advanceTime(1);
|
|
455
|
+
await connectPromise;
|
|
456
|
+
|
|
417
457
|
client.on('reconnect_attempt', reconnectAttemptHandler);
|
|
418
458
|
|
|
419
459
|
// Simulate server disconnect
|
|
420
460
|
const ws = MockWebSocket.getLastInstance();
|
|
421
461
|
ws?.close(1006, 'Connection lost');
|
|
422
462
|
|
|
423
|
-
//
|
|
424
|
-
|
|
463
|
+
// Advance time for reconnect attempt
|
|
464
|
+
advanceTime(50);
|
|
425
465
|
|
|
426
466
|
expect(reconnectAttemptHandler).toHaveBeenCalled();
|
|
427
467
|
});
|
|
@@ -436,7 +476,11 @@ describe('WsClient', () => {
|
|
|
436
476
|
});
|
|
437
477
|
let reconnectAttemptCount = 0;
|
|
438
478
|
|
|
439
|
-
|
|
479
|
+
// Advance time to trigger MockWebSocket's async open
|
|
480
|
+
const connectPromise = client.connect();
|
|
481
|
+
advanceTime(1);
|
|
482
|
+
await connectPromise;
|
|
483
|
+
|
|
440
484
|
client.on('reconnect_attempt', (attempt) => {
|
|
441
485
|
reconnectAttemptCount = attempt;
|
|
442
486
|
});
|
|
@@ -445,8 +489,8 @@ describe('WsClient', () => {
|
|
|
445
489
|
const ws = MockWebSocket.getLastInstance();
|
|
446
490
|
ws?.close(1006, 'Connection lost');
|
|
447
491
|
|
|
448
|
-
//
|
|
449
|
-
|
|
492
|
+
// Advance time for first reconnect attempt
|
|
493
|
+
advanceTime(50);
|
|
450
494
|
|
|
451
495
|
expect(reconnectAttemptCount).toBeGreaterThanOrEqual(1);
|
|
452
496
|
});
|
|
@@ -459,19 +503,23 @@ describe('WsClient', () => {
|
|
|
459
503
|
});
|
|
460
504
|
const reconnectAttemptHandler = mock(() => undefined);
|
|
461
505
|
|
|
462
|
-
|
|
463
|
-
client.
|
|
506
|
+
// Advance time to trigger MockWebSocket's async open
|
|
507
|
+
const connectPromise = client.connect();
|
|
508
|
+
advanceTime(1);
|
|
509
|
+
await connectPromise;
|
|
464
510
|
|
|
465
|
-
|
|
511
|
+
client.on('reconnect_attempt', reconnectAttemptHandler);
|
|
466
512
|
|
|
467
513
|
// Simulate disconnect
|
|
468
514
|
const ws = MockWebSocket.getLastInstance();
|
|
469
515
|
ws?.close(1006, 'Connection lost');
|
|
470
516
|
|
|
471
|
-
|
|
517
|
+
advanceTime(50);
|
|
472
518
|
|
|
519
|
+
// Main assertion: reconnect handler should not be called when reconnect is disabled
|
|
473
520
|
expect(reconnectAttemptHandler).not.toHaveBeenCalled();
|
|
474
|
-
|
|
521
|
+
// Note: We don't check MockWebSocket.instances.length here because it's a static array
|
|
522
|
+
// that can be affected by parallel test runs. The handler check is sufficient.
|
|
475
523
|
});
|
|
476
524
|
});
|
|
477
525
|
|