@onebun/core 0.2.8 → 0.2.10
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 +9 -1
- package/src/application/application.test.ts +46 -0
- package/src/application/application.ts +12 -12
- package/src/module/controller.test.ts +110 -126
- package/src/module/module.ts +10 -0
- package/src/module/service.test.ts +78 -51
- package/src/queue/adapters/redis.adapter.test.ts +7 -25
- package/src/queue/docs-examples.test.ts +2 -2
- package/src/redis/shared-redis.test.ts +21 -39
- package/src/testing/containers.test.ts +35 -0
- package/src/testing/containers.ts +89 -0
- package/src/testing/docs-examples.test.ts +214 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/service-helpers.test.ts +166 -0
- package/src/testing/service-helpers.ts +69 -0
- package/src/testing/testing-module.test.ts +80 -0
- package/src/testing/testing-module.ts +47 -2
- package/src/websocket/ws-storage-redis.test.ts +6 -24
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* eslint-disable
|
|
2
2
|
@typescript-eslint/no-unused-vars,
|
|
3
|
-
@typescript-eslint/no-explicit-any
|
|
3
|
+
@typescript-eslint/no-explicit-any,
|
|
4
|
+
@typescript-eslint/naming-convention,
|
|
5
|
+
jest/unbound-method */
|
|
4
6
|
import {
|
|
5
7
|
describe,
|
|
6
8
|
test,
|
|
@@ -10,6 +12,9 @@ import {
|
|
|
10
12
|
} from 'bun:test';
|
|
11
13
|
import { Effect } from 'effect';
|
|
12
14
|
|
|
15
|
+
import type { SyncLogger } from '@onebun/logger';
|
|
16
|
+
|
|
17
|
+
import { createTestService } from '../testing/service-helpers';
|
|
13
18
|
import { createMockConfig } from '../testing/test-utils';
|
|
14
19
|
|
|
15
20
|
import {
|
|
@@ -19,50 +24,48 @@ import {
|
|
|
19
24
|
} from './config.interface';
|
|
20
25
|
import { BaseService } from './service';
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'database.host': 'localhost',
|
|
38
|
-
'app.name': 'test-app',
|
|
39
|
-
/* eslint-enable @typescript-eslint/naming-convention */
|
|
40
|
-
});
|
|
41
|
-
});
|
|
27
|
+
function createMockLogger(): SyncLogger {
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
29
|
+
const noOp = () => {};
|
|
30
|
+
const logger: SyncLogger = {
|
|
31
|
+
trace: mock(noOp),
|
|
32
|
+
debug: mock(noOp),
|
|
33
|
+
info: mock(noOp),
|
|
34
|
+
warn: mock(noOp),
|
|
35
|
+
error: mock(noOp),
|
|
36
|
+
fatal: mock(noOp),
|
|
37
|
+
child: mock(() => logger),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return logger;
|
|
41
|
+
}
|
|
42
42
|
|
|
43
|
+
describe('BaseService', () => {
|
|
43
44
|
describe('Initialization via initializeService', () => {
|
|
44
45
|
test('should initialize service with logger and config via initializeService', () => {
|
|
45
46
|
class TestService extends BaseService {}
|
|
46
47
|
|
|
47
|
-
const service =
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const { instance: service, config } = createTestService(TestService, {
|
|
49
|
+
config: {
|
|
50
|
+
'database.host': 'localhost',
|
|
51
|
+
'app.name': 'test-app',
|
|
52
|
+
},
|
|
53
|
+
});
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
service.initializeService(mockLogger, mockConfig);
|
|
53
|
-
|
|
55
|
+
expect(service).toBeInstanceOf(BaseService);
|
|
54
56
|
expect(service.isInitialized).toBe(true);
|
|
55
57
|
expect((service as any).logger).toBeDefined();
|
|
56
|
-
expect((service as any).config).toBe(
|
|
58
|
+
expect((service as any).config).toBe(config);
|
|
57
59
|
});
|
|
58
60
|
|
|
59
61
|
test('should initialize service with logger and NotInitializedConfig', () => {
|
|
60
62
|
class TestService extends BaseService {}
|
|
61
63
|
|
|
62
64
|
const service = new TestService();
|
|
65
|
+
const mockLogger = createMockLogger();
|
|
63
66
|
const notInitConfig = new NotInitializedConfig();
|
|
64
67
|
service.initializeService(mockLogger, notInitConfig);
|
|
65
|
-
|
|
68
|
+
|
|
66
69
|
expect(service.isInitialized).toBe(true);
|
|
67
70
|
expect((service as any).logger).toBeDefined();
|
|
68
71
|
expect((service as any).config).toBeInstanceOf(NotInitializedConfig);
|
|
@@ -72,6 +75,7 @@ describe('BaseService', () => {
|
|
|
72
75
|
class TestService extends BaseService {}
|
|
73
76
|
|
|
74
77
|
const service = new TestService();
|
|
78
|
+
const mockConfig = createMockConfig({});
|
|
75
79
|
expect(() => service.initializeService(undefined as any, mockConfig))
|
|
76
80
|
.toThrow('Logger is required for service TestService');
|
|
77
81
|
});
|
|
@@ -80,6 +84,7 @@ describe('BaseService', () => {
|
|
|
80
84
|
class TestService extends BaseService {}
|
|
81
85
|
|
|
82
86
|
const service = new TestService();
|
|
87
|
+
const mockConfig = createMockConfig({});
|
|
83
88
|
expect(() => service.initializeService(null as any, mockConfig))
|
|
84
89
|
.toThrow('Logger is required for service TestService');
|
|
85
90
|
});
|
|
@@ -87,15 +92,19 @@ describe('BaseService', () => {
|
|
|
87
92
|
test('should not reinitialize if already initialized', () => {
|
|
88
93
|
class TestService extends BaseService {}
|
|
89
94
|
|
|
90
|
-
const service =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
const { instance: service, config } = createTestService(TestService, {
|
|
96
|
+
config: {
|
|
97
|
+
'database.host': 'localhost',
|
|
98
|
+
'app.name': 'test-app',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const otherLogger = createMockLogger();
|
|
94
103
|
const otherConfig = createMockConfig({ 'other': 'config' });
|
|
95
104
|
service.initializeService(otherLogger, otherConfig);
|
|
96
|
-
|
|
97
|
-
// Should still have original
|
|
98
|
-
expect((service as any).config).toBe(
|
|
105
|
+
|
|
106
|
+
// Should still have original config (no reinit)
|
|
107
|
+
expect((service as any).config).toBe(config);
|
|
99
108
|
});
|
|
100
109
|
});
|
|
101
110
|
|
|
@@ -113,8 +122,8 @@ describe('BaseService', () => {
|
|
|
113
122
|
let service: TestService;
|
|
114
123
|
|
|
115
124
|
beforeEach(() => {
|
|
116
|
-
|
|
117
|
-
service.
|
|
125
|
+
const result = createTestService(TestService);
|
|
126
|
+
service = result.instance;
|
|
118
127
|
});
|
|
119
128
|
|
|
120
129
|
test('should run effect successfully', async () => {
|
|
@@ -125,14 +134,14 @@ describe('BaseService', () => {
|
|
|
125
134
|
|
|
126
135
|
test('should handle effect failure', async () => {
|
|
127
136
|
const effect = Effect.fail(new Error('Test error'));
|
|
128
|
-
|
|
137
|
+
|
|
129
138
|
await expect(service.testRunEffect(effect)).rejects.toThrow('Test error');
|
|
130
139
|
});
|
|
131
140
|
|
|
132
141
|
test('should format Error instances', () => {
|
|
133
142
|
const error = new Error('Test error');
|
|
134
143
|
const formattedError = service.testFormatError(error);
|
|
135
|
-
|
|
144
|
+
|
|
136
145
|
expect(formattedError).toBe(error);
|
|
137
146
|
expect(formattedError.message).toBe('Test error');
|
|
138
147
|
});
|
|
@@ -140,7 +149,7 @@ describe('BaseService', () => {
|
|
|
140
149
|
test('should format string errors', () => {
|
|
141
150
|
const error = 'String error message';
|
|
142
151
|
const formattedError = service.testFormatError(error);
|
|
143
|
-
|
|
152
|
+
|
|
144
153
|
expect(formattedError).toBeInstanceOf(Error);
|
|
145
154
|
expect(formattedError.message).toBe('String error message');
|
|
146
155
|
});
|
|
@@ -148,7 +157,7 @@ describe('BaseService', () => {
|
|
|
148
157
|
test('should format unknown errors', () => {
|
|
149
158
|
const error = { unknown: 'object' };
|
|
150
159
|
const formattedError = service.testFormatError(error);
|
|
151
|
-
|
|
160
|
+
|
|
152
161
|
expect(formattedError).toBeInstanceOf(Error);
|
|
153
162
|
expect(formattedError.message).toBe('[object Object]'); // String(object) result
|
|
154
163
|
});
|
|
@@ -156,10 +165,10 @@ describe('BaseService', () => {
|
|
|
156
165
|
test('should format null/undefined errors', () => {
|
|
157
166
|
const nullError = service.testFormatError(null);
|
|
158
167
|
const undefinedError = service.testFormatError(undefined);
|
|
159
|
-
|
|
168
|
+
|
|
160
169
|
expect(nullError).toBeInstanceOf(Error);
|
|
161
170
|
expect(nullError.message).toBe('null'); // String(null) result
|
|
162
|
-
|
|
171
|
+
|
|
163
172
|
expect(undefinedError).toBeInstanceOf(Error);
|
|
164
173
|
expect(undefinedError.message).toBe('undefined'); // String(undefined) result
|
|
165
174
|
});
|
|
@@ -178,6 +187,12 @@ describe('BaseService', () => {
|
|
|
178
187
|
}
|
|
179
188
|
}
|
|
180
189
|
|
|
190
|
+
const mockLogger = createMockLogger();
|
|
191
|
+
const mockConfig = createMockConfig({
|
|
192
|
+
'database.host': 'localhost',
|
|
193
|
+
'app.name': 'test-app',
|
|
194
|
+
});
|
|
195
|
+
|
|
181
196
|
// Set init context before construction (as the framework does)
|
|
182
197
|
BaseService.setInitContext(mockLogger, mockConfig);
|
|
183
198
|
let service: TestService;
|
|
@@ -203,6 +218,11 @@ describe('BaseService', () => {
|
|
|
203
218
|
}
|
|
204
219
|
}
|
|
205
220
|
|
|
221
|
+
const mockLogger = createMockLogger();
|
|
222
|
+
const mockConfig = createMockConfig({
|
|
223
|
+
'database.host': 'localhost',
|
|
224
|
+
});
|
|
225
|
+
|
|
206
226
|
BaseService.setInitContext(mockLogger, mockConfig);
|
|
207
227
|
let service: TestService;
|
|
208
228
|
try {
|
|
@@ -217,6 +237,9 @@ describe('BaseService', () => {
|
|
|
217
237
|
test('should create child logger with correct className in constructor', () => {
|
|
218
238
|
class MyCustomService extends BaseService {}
|
|
219
239
|
|
|
240
|
+
const mockLogger = createMockLogger();
|
|
241
|
+
const mockConfig = createMockConfig({});
|
|
242
|
+
|
|
220
243
|
BaseService.setInitContext(mockLogger, mockConfig);
|
|
221
244
|
try {
|
|
222
245
|
new MyCustomService();
|
|
@@ -240,6 +263,9 @@ describe('BaseService', () => {
|
|
|
240
263
|
test('initializeService should be a no-op if already initialized via init context', () => {
|
|
241
264
|
class TestService extends BaseService {}
|
|
242
265
|
|
|
266
|
+
const mockLogger = createMockLogger();
|
|
267
|
+
const mockConfig = createMockConfig({});
|
|
268
|
+
|
|
243
269
|
BaseService.setInitContext(mockLogger, mockConfig);
|
|
244
270
|
let service: TestService;
|
|
245
271
|
try {
|
|
@@ -251,7 +277,7 @@ describe('BaseService', () => {
|
|
|
251
277
|
expect(service.isInitialized).toBe(true);
|
|
252
278
|
|
|
253
279
|
// Call initializeService again — should be a no-op
|
|
254
|
-
const otherLogger =
|
|
280
|
+
const otherLogger = createMockLogger();
|
|
255
281
|
const otherConfig = createMockConfig({ other: 'config' });
|
|
256
282
|
service.initializeService(otherLogger, otherConfig);
|
|
257
283
|
|
|
@@ -260,6 +286,9 @@ describe('BaseService', () => {
|
|
|
260
286
|
});
|
|
261
287
|
|
|
262
288
|
test('clearInitContext should prevent subsequent constructors from picking up context', () => {
|
|
289
|
+
const mockLogger = createMockLogger();
|
|
290
|
+
const mockConfig = createMockConfig({});
|
|
291
|
+
|
|
263
292
|
BaseService.setInitContext(mockLogger, mockConfig);
|
|
264
293
|
BaseService.clearInitContext();
|
|
265
294
|
|
|
@@ -275,13 +304,12 @@ describe('BaseService', () => {
|
|
|
275
304
|
class TestService extends BaseService {
|
|
276
305
|
async testComplexEffect() {
|
|
277
306
|
const effect = Effect.succeed(84); // Simplify to avoid generator complexity
|
|
278
|
-
|
|
307
|
+
|
|
279
308
|
return await this.runEffect(effect as any);
|
|
280
309
|
}
|
|
281
310
|
}
|
|
282
311
|
|
|
283
|
-
const service =
|
|
284
|
-
service.initializeService(mockLogger, mockConfig);
|
|
312
|
+
const { instance: service } = createTestService(TestService);
|
|
285
313
|
const result = await service.testComplexEffect();
|
|
286
314
|
expect(result).toBe(84);
|
|
287
315
|
});
|
|
@@ -291,13 +319,12 @@ describe('BaseService', () => {
|
|
|
291
319
|
testStackTrace() {
|
|
292
320
|
const originalError = new Error('Original error');
|
|
293
321
|
const formattedError = this.formatError(originalError);
|
|
294
|
-
|
|
322
|
+
|
|
295
323
|
expect(formattedError).toBe(originalError); // Same Error instance returned
|
|
296
324
|
}
|
|
297
325
|
}
|
|
298
326
|
|
|
299
|
-
const service =
|
|
300
|
-
service.initializeService(mockLogger, mockConfig);
|
|
327
|
+
const { instance: service } = createTestService(TestService);
|
|
301
328
|
service.testStackTrace();
|
|
302
329
|
});
|
|
303
330
|
});
|
|
@@ -14,45 +14,27 @@ import {
|
|
|
14
14
|
it,
|
|
15
15
|
mock,
|
|
16
16
|
} from 'bun:test';
|
|
17
|
-
import {
|
|
18
|
-
GenericContainer,
|
|
19
|
-
type StartedTestContainer,
|
|
20
|
-
Wait,
|
|
21
|
-
} from 'testcontainers';
|
|
22
17
|
|
|
23
18
|
import { SharedRedisProvider } from '../../redis/shared-redis';
|
|
19
|
+
import { createRedisContainer, type TestContainer } from '../../testing/containers';
|
|
20
|
+
|
|
24
21
|
|
|
25
22
|
import { RedisQueueAdapter, createRedisQueueAdapter } from './redis.adapter';
|
|
26
23
|
|
|
27
24
|
describe('RedisQueueAdapter', () => {
|
|
28
|
-
let
|
|
29
|
-
let redisUrl: string;
|
|
25
|
+
let redis: TestContainer;
|
|
30
26
|
let adapter: RedisQueueAdapter;
|
|
31
27
|
|
|
32
28
|
beforeAll(async () => {
|
|
33
|
-
|
|
34
|
-
redisContainer = await new GenericContainer('redis:7-alpine')
|
|
35
|
-
.withExposedPorts(6379)
|
|
36
|
-
.withWaitStrategy(Wait.forLogMessage(/.*Ready to accept connections.*/))
|
|
37
|
-
.withStartupTimeout(30000)
|
|
38
|
-
.withLogConsumer(() => {
|
|
39
|
-
// Suppress container logs
|
|
40
|
-
})
|
|
41
|
-
.start();
|
|
42
|
-
|
|
43
|
-
const host = redisContainer.getHost();
|
|
44
|
-
const port = redisContainer.getMappedPort(6379);
|
|
45
|
-
redisUrl = `redis://${host}:${port}`;
|
|
29
|
+
redis = await createRedisContainer();
|
|
46
30
|
|
|
47
31
|
// Configure shared Redis
|
|
48
|
-
SharedRedisProvider.configure({ url:
|
|
32
|
+
SharedRedisProvider.configure({ url: redis.url });
|
|
49
33
|
});
|
|
50
34
|
|
|
51
35
|
afterAll(async () => {
|
|
52
36
|
await SharedRedisProvider.reset();
|
|
53
|
-
|
|
54
|
-
await redisContainer.stop();
|
|
55
|
-
}
|
|
37
|
+
await redis.stop();
|
|
56
38
|
});
|
|
57
39
|
|
|
58
40
|
beforeEach(async () => {
|
|
@@ -125,7 +107,7 @@ describe('RedisQueueAdapter', () => {
|
|
|
125
107
|
it('should create own client when useSharedClient is false', async () => {
|
|
126
108
|
const ownAdapter = new RedisQueueAdapter({
|
|
127
109
|
useSharedClient: false,
|
|
128
|
-
url:
|
|
110
|
+
url: redis.url,
|
|
129
111
|
keyPrefix: 'own:',
|
|
130
112
|
});
|
|
131
113
|
|
|
@@ -435,7 +435,7 @@ describe('Custom adapter NATS JetStream (docs/api/queue.md)', () => {
|
|
|
435
435
|
class NatsJetStreamAdapter implements QueueAdapter {
|
|
436
436
|
readonly name = 'nats-jetstream';
|
|
437
437
|
readonly type = 'jetstream';
|
|
438
|
-
constructor(private opts: { servers: string;
|
|
438
|
+
constructor(private opts: { servers: string; streams?: Array<{ name: string; subjects: string[] }> }) {}
|
|
439
439
|
async connect(): Promise<void> {}
|
|
440
440
|
async disconnect(): Promise<void> {}
|
|
441
441
|
isConnected(): boolean {
|
|
@@ -473,7 +473,7 @@ describe('Custom adapter NATS JetStream (docs/api/queue.md)', () => {
|
|
|
473
473
|
|
|
474
474
|
const adapter = new NatsJetStreamAdapter({
|
|
475
475
|
servers: 'nats://localhost:4222',
|
|
476
|
-
|
|
476
|
+
streams: [{ name: 'EVENTS', subjects: ['events.>'] }],
|
|
477
477
|
});
|
|
478
478
|
await adapter.connect();
|
|
479
479
|
expect(adapter.name).toBe('nats-jetstream');
|
|
@@ -13,11 +13,8 @@ import {
|
|
|
13
13
|
it,
|
|
14
14
|
} from 'bun:test';
|
|
15
15
|
import { Effect, pipe } from 'effect';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type StartedTestContainer,
|
|
19
|
-
Wait,
|
|
20
|
-
} from 'testcontainers';
|
|
16
|
+
|
|
17
|
+
import { createRedisContainer, type TestContainer } from '../testing/containers';
|
|
21
18
|
|
|
22
19
|
import {
|
|
23
20
|
SharedRedisProvider,
|
|
@@ -27,30 +24,15 @@ import {
|
|
|
27
24
|
} from './shared-redis';
|
|
28
25
|
|
|
29
26
|
describe('SharedRedisProvider', () => {
|
|
30
|
-
let
|
|
31
|
-
let redisUrl: string;
|
|
27
|
+
let redis: TestContainer;
|
|
32
28
|
|
|
33
29
|
beforeAll(async () => {
|
|
34
|
-
|
|
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}`;
|
|
30
|
+
redis = await createRedisContainer();
|
|
47
31
|
});
|
|
48
32
|
|
|
49
33
|
afterAll(async () => {
|
|
50
34
|
await SharedRedisProvider.reset();
|
|
51
|
-
|
|
52
|
-
await redisContainer.stop();
|
|
53
|
-
}
|
|
35
|
+
await redis.stop();
|
|
54
36
|
});
|
|
55
37
|
|
|
56
38
|
afterEach(async () => {
|
|
@@ -61,7 +43,7 @@ describe('SharedRedisProvider', () => {
|
|
|
61
43
|
it('should configure the provider', () => {
|
|
62
44
|
expect(SharedRedisProvider.isConfigured()).toBe(false);
|
|
63
45
|
|
|
64
|
-
SharedRedisProvider.configure({ url:
|
|
46
|
+
SharedRedisProvider.configure({ url: redis.url });
|
|
65
47
|
|
|
66
48
|
expect(SharedRedisProvider.isConfigured()).toBe(true);
|
|
67
49
|
});
|
|
@@ -76,7 +58,7 @@ describe('SharedRedisProvider', () => {
|
|
|
76
58
|
describe('connection', () => {
|
|
77
59
|
it('should connect and return a client', async () => {
|
|
78
60
|
SharedRedisProvider.configure({
|
|
79
|
-
url:
|
|
61
|
+
url: redis.url,
|
|
80
62
|
keyPrefix: 'test:',
|
|
81
63
|
});
|
|
82
64
|
|
|
@@ -88,7 +70,7 @@ describe('SharedRedisProvider', () => {
|
|
|
88
70
|
});
|
|
89
71
|
|
|
90
72
|
it('should return the same instance on multiple calls', async () => {
|
|
91
|
-
SharedRedisProvider.configure({ url:
|
|
73
|
+
SharedRedisProvider.configure({ url: redis.url });
|
|
92
74
|
|
|
93
75
|
const client1 = await SharedRedisProvider.getClient();
|
|
94
76
|
const client2 = await SharedRedisProvider.getClient();
|
|
@@ -97,7 +79,7 @@ describe('SharedRedisProvider', () => {
|
|
|
97
79
|
});
|
|
98
80
|
|
|
99
81
|
it('should handle concurrent getClient calls', async () => {
|
|
100
|
-
SharedRedisProvider.configure({ url:
|
|
82
|
+
SharedRedisProvider.configure({ url: redis.url });
|
|
101
83
|
|
|
102
84
|
// Call getClient multiple times concurrently
|
|
103
85
|
const [client1, client2, client3] = await Promise.all([
|
|
@@ -112,7 +94,7 @@ describe('SharedRedisProvider', () => {
|
|
|
112
94
|
});
|
|
113
95
|
|
|
114
96
|
it('should disconnect properly', async () => {
|
|
115
|
-
SharedRedisProvider.configure({ url:
|
|
97
|
+
SharedRedisProvider.configure({ url: redis.url });
|
|
116
98
|
|
|
117
99
|
await SharedRedisProvider.getClient();
|
|
118
100
|
expect(SharedRedisProvider.isConnected()).toBe(true);
|
|
@@ -123,7 +105,7 @@ describe('SharedRedisProvider', () => {
|
|
|
123
105
|
});
|
|
124
106
|
|
|
125
107
|
it('should reconnect after disconnect', async () => {
|
|
126
|
-
SharedRedisProvider.configure({ url:
|
|
108
|
+
SharedRedisProvider.configure({ url: redis.url });
|
|
127
109
|
|
|
128
110
|
const client1 = await SharedRedisProvider.getClient();
|
|
129
111
|
await SharedRedisProvider.disconnect();
|
|
@@ -140,7 +122,7 @@ describe('SharedRedisProvider', () => {
|
|
|
140
122
|
describe('client operations', () => {
|
|
141
123
|
it('should perform basic Redis operations', async () => {
|
|
142
124
|
SharedRedisProvider.configure({
|
|
143
|
-
url:
|
|
125
|
+
url: redis.url,
|
|
144
126
|
keyPrefix: 'test:ops:',
|
|
145
127
|
});
|
|
146
128
|
|
|
@@ -160,10 +142,10 @@ describe('SharedRedisProvider', () => {
|
|
|
160
142
|
|
|
161
143
|
describe('createClient', () => {
|
|
162
144
|
it('should create standalone client with custom URL', async () => {
|
|
163
|
-
SharedRedisProvider.configure({ url:
|
|
145
|
+
SharedRedisProvider.configure({ url: redis.url });
|
|
164
146
|
|
|
165
147
|
const standalone = SharedRedisProvider.createClient({
|
|
166
|
-
url:
|
|
148
|
+
url: redis.url,
|
|
167
149
|
keyPrefix: 'standalone:',
|
|
168
150
|
});
|
|
169
151
|
|
|
@@ -179,7 +161,7 @@ describe('SharedRedisProvider', () => {
|
|
|
179
161
|
|
|
180
162
|
it('should use base options when not specified', async () => {
|
|
181
163
|
SharedRedisProvider.configure({
|
|
182
|
-
url:
|
|
164
|
+
url: redis.url,
|
|
183
165
|
keyPrefix: 'base:',
|
|
184
166
|
reconnect: true,
|
|
185
167
|
});
|
|
@@ -199,7 +181,7 @@ describe('SharedRedisProvider', () => {
|
|
|
199
181
|
|
|
200
182
|
describe('reset', () => {
|
|
201
183
|
it('should reset provider state completely', async () => {
|
|
202
|
-
SharedRedisProvider.configure({ url:
|
|
184
|
+
SharedRedisProvider.configure({ url: redis.url });
|
|
203
185
|
await SharedRedisProvider.getClient();
|
|
204
186
|
|
|
205
187
|
expect(SharedRedisProvider.isConnected()).toBe(true);
|
|
@@ -215,17 +197,17 @@ describe('SharedRedisProvider', () => {
|
|
|
215
197
|
describe('Effect.js integration', () => {
|
|
216
198
|
it('should work with makeSharedRedisLayer', async () => {
|
|
217
199
|
const layer = makeSharedRedisLayer({
|
|
218
|
-
url:
|
|
200
|
+
url: redis.url,
|
|
219
201
|
keyPrefix: 'effect:',
|
|
220
202
|
});
|
|
221
203
|
|
|
222
204
|
const program = pipe(
|
|
223
205
|
SharedRedisService,
|
|
224
|
-
Effect.flatMap((
|
|
206
|
+
Effect.flatMap((redisClient) =>
|
|
225
207
|
Effect.promise(async () => {
|
|
226
|
-
await
|
|
208
|
+
await redisClient.set('effect-test', 'value');
|
|
227
209
|
|
|
228
|
-
return await
|
|
210
|
+
return await redisClient.get('effect-test');
|
|
229
211
|
}),
|
|
230
212
|
),
|
|
231
213
|
);
|
|
@@ -245,7 +227,7 @@ describe('SharedRedisProvider', () => {
|
|
|
245
227
|
});
|
|
246
228
|
|
|
247
229
|
it('should succeed getSharedRedis when configured', async () => {
|
|
248
|
-
SharedRedisProvider.configure({ url:
|
|
230
|
+
SharedRedisProvider.configure({ url: redis.url });
|
|
249
231
|
|
|
250
232
|
const result = await Effect.runPromiseExit(getSharedRedis);
|
|
251
233
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
expect,
|
|
4
|
+
test,
|
|
5
|
+
} from 'bun:test';
|
|
6
|
+
|
|
7
|
+
import { createNatsContainer, createRedisContainer } from './containers';
|
|
8
|
+
|
|
9
|
+
describe('createRedisContainer', () => {
|
|
10
|
+
test('starts redis and returns connection details', async () => {
|
|
11
|
+
const redis = await createRedisContainer();
|
|
12
|
+
try {
|
|
13
|
+
expect(redis.url).toMatch(/^redis:\/\/.+:\d+$/);
|
|
14
|
+
expect(redis.host).toBeDefined();
|
|
15
|
+
expect(redis.port).toBeGreaterThan(0);
|
|
16
|
+
expect(redis.container).toBeDefined();
|
|
17
|
+
} finally {
|
|
18
|
+
await redis.stop();
|
|
19
|
+
}
|
|
20
|
+
}, 60000);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('createNatsContainer', () => {
|
|
24
|
+
test('starts nats and returns connection details', async () => {
|
|
25
|
+
const nats = await createNatsContainer();
|
|
26
|
+
try {
|
|
27
|
+
expect(nats.url).toMatch(/^nats:\/\/.+:\d+$/);
|
|
28
|
+
expect(nats.host).toBeDefined();
|
|
29
|
+
expect(nats.port).toBeGreaterThan(0);
|
|
30
|
+
expect(nats.container).toBeDefined();
|
|
31
|
+
} finally {
|
|
32
|
+
await nats.stop();
|
|
33
|
+
}
|
|
34
|
+
}, 60000);
|
|
35
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { GenericContainer, Wait } from 'testcontainers';
|
|
2
|
+
|
|
3
|
+
import type { StartedTestContainer } from 'testcontainers';
|
|
4
|
+
|
|
5
|
+
export interface TestContainer {
|
|
6
|
+
url: string;
|
|
7
|
+
host: string;
|
|
8
|
+
port: number;
|
|
9
|
+
container: StartedTestContainer;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RedisContainerOptions {
|
|
14
|
+
image?: string;
|
|
15
|
+
startupTimeout?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NatsContainerOptions {
|
|
19
|
+
image?: string;
|
|
20
|
+
startupTimeout?: number;
|
|
21
|
+
enableJetStream?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_STARTUP_TIMEOUT = 30_000;
|
|
25
|
+
const REDIS_PORT = 6379;
|
|
26
|
+
const NATS_PORT = 4222;
|
|
27
|
+
|
|
28
|
+
export async function createRedisContainer(
|
|
29
|
+
options?: RedisContainerOptions,
|
|
30
|
+
): Promise<TestContainer> {
|
|
31
|
+
const image = options?.image ?? 'redis:7-alpine';
|
|
32
|
+
const startupTimeout = options?.startupTimeout ?? DEFAULT_STARTUP_TIMEOUT;
|
|
33
|
+
|
|
34
|
+
const container = await new GenericContainer(image)
|
|
35
|
+
.withExposedPorts(REDIS_PORT)
|
|
36
|
+
.withWaitStrategy(Wait.forLogMessage(/.*Ready to accept connections.*/))
|
|
37
|
+
.withStartupTimeout(startupTimeout)
|
|
38
|
+
.withLogConsumer(() => {
|
|
39
|
+
// Suppress container logs in tests
|
|
40
|
+
})
|
|
41
|
+
.start();
|
|
42
|
+
|
|
43
|
+
const host = container.getHost();
|
|
44
|
+
const port = container.getMappedPort(REDIS_PORT);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
url: `redis://${host}:${port}`,
|
|
48
|
+
host,
|
|
49
|
+
port,
|
|
50
|
+
container,
|
|
51
|
+
async stop() {
|
|
52
|
+
await container.stop();
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function createNatsContainer(
|
|
58
|
+
options?: NatsContainerOptions,
|
|
59
|
+
): Promise<TestContainer> {
|
|
60
|
+
const image = options?.image ?? 'nats:2.10-alpine';
|
|
61
|
+
const startupTimeout = options?.startupTimeout ?? DEFAULT_STARTUP_TIMEOUT;
|
|
62
|
+
const enableJetStream = options?.enableJetStream ?? false;
|
|
63
|
+
|
|
64
|
+
let builder = new GenericContainer(image)
|
|
65
|
+
.withExposedPorts(NATS_PORT)
|
|
66
|
+
.withWaitStrategy(Wait.forLogMessage(/.*Server is ready.*/))
|
|
67
|
+
.withStartupTimeout(startupTimeout)
|
|
68
|
+
.withLogConsumer(() => {
|
|
69
|
+
// Suppress container logs in tests
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (enableJetStream) {
|
|
73
|
+
builder = builder.withCommand(['--js']);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const container = await builder.start();
|
|
77
|
+
const host = container.getHost();
|
|
78
|
+
const port = container.getMappedPort(NATS_PORT);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
url: `nats://${host}:${port}`,
|
|
82
|
+
host,
|
|
83
|
+
port,
|
|
84
|
+
container,
|
|
85
|
+
async stop() {
|
|
86
|
+
await container.stop();
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|