@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.
@@ -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
- describe('BaseService', () => {
23
- let mockLogger: any;
24
- let mockConfig: IConfig<OneBunAppConfig>;
25
-
26
- beforeEach(() => {
27
- mockLogger = {
28
- child: mock((context: any) => mockLogger),
29
- debug: mock(),
30
- info: mock(),
31
- warn: mock(),
32
- error: mock(),
33
- };
34
-
35
- mockConfig = createMockConfig({
36
- /* eslint-disable @typescript-eslint/naming-convention */
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 = new TestService();
48
- expect(service).toBeInstanceOf(BaseService);
49
- expect(service.isInitialized).toBe(false);
48
+ const { instance: service, config } = createTestService(TestService, {
49
+ config: {
50
+ 'database.host': 'localhost',
51
+ 'app.name': 'test-app',
52
+ },
53
+ });
50
54
 
51
- // Initialize service
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(mockConfig);
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 = new TestService();
91
- service.initializeService(mockLogger, mockConfig);
92
-
93
- const otherLogger = { ...mockLogger, child: mock(() => ({ ...mockLogger })) };
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 logger (no reinit)
98
- expect((service as any).config).toBe(mockConfig);
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
- service = new TestService();
117
- service.initializeService(mockLogger, mockConfig);
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 = { ...mockLogger, child: mock(() => ({ ...mockLogger })) };
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 = new TestService();
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 = new TestService();
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 redisContainer: StartedTestContainer;
29
- let redisUrl: string;
25
+ let redis: TestContainer;
30
26
  let adapter: RedisQueueAdapter;
31
27
 
32
28
  beforeAll(async () => {
33
- // Start Redis container
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: redisUrl });
32
+ SharedRedisProvider.configure({ url: redis.url });
49
33
  });
50
34
 
51
35
  afterAll(async () => {
52
36
  await SharedRedisProvider.reset();
53
- if (redisContainer) {
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: redisUrl,
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; stream?: 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
- stream: 'EVENTS',
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
- import {
17
- GenericContainer,
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 redisContainer: StartedTestContainer;
31
- let redisUrl: string;
27
+ let redis: TestContainer;
32
28
 
33
29
  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}`;
30
+ redis = await createRedisContainer();
47
31
  });
48
32
 
49
33
  afterAll(async () => {
50
34
  await SharedRedisProvider.reset();
51
- if (redisContainer) {
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: redisUrl });
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: redisUrl,
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: redisUrl });
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: redisUrl });
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: redisUrl });
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: redisUrl });
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: redisUrl,
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: redisUrl });
145
+ SharedRedisProvider.configure({ url: redis.url });
164
146
 
165
147
  const standalone = SharedRedisProvider.createClient({
166
- url: redisUrl,
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: redisUrl,
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: redisUrl });
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: redisUrl,
200
+ url: redis.url,
219
201
  keyPrefix: 'effect:',
220
202
  });
221
203
 
222
204
  const program = pipe(
223
205
  SharedRedisService,
224
- Effect.flatMap((redis) =>
206
+ Effect.flatMap((redisClient) =>
225
207
  Effect.promise(async () => {
226
- await redis.set('effect-test', 'value');
208
+ await redisClient.set('effect-test', 'value');
227
209
 
228
- return await redis.get('effect-test');
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: redisUrl });
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
+ }