@onebun/core 0.2.9 → 0.2.11
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 +229 -1
- package/src/decorators/decorators.ts +8 -1
- package/src/decorators/metadata.ts +25 -0
- package/src/module/controller.test.ts +110 -126
- package/src/module/service.test.ts +78 -51
- package/src/queue/adapters/redis.adapter.test.ts +7 -25
- 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 +280 -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
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation Examples Tests for @onebun/core testing utilities
|
|
3
|
+
*
|
|
4
|
+
* This file tests code examples from:
|
|
5
|
+
* - docs/testing.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
it,
|
|
12
|
+
} from 'bun:test';
|
|
13
|
+
|
|
14
|
+
import type { CompiledTestingModule } from './testing-module';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
Controller,
|
|
18
|
+
Get,
|
|
19
|
+
Param,
|
|
20
|
+
} from '../decorators/decorators';
|
|
21
|
+
import { Controller as BaseController } from '../module/controller';
|
|
22
|
+
import { BaseService, Service } from '../module/service';
|
|
23
|
+
|
|
24
|
+
import { createTestController, createTestService } from './service-helpers';
|
|
25
|
+
import {
|
|
26
|
+
createMockConfig,
|
|
27
|
+
createMockLogger,
|
|
28
|
+
useFakeTimers,
|
|
29
|
+
} from './test-utils';
|
|
30
|
+
import { TestingModule } from './testing-module';
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Test fixtures
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
@Service()
|
|
37
|
+
class UserService extends BaseService {
|
|
38
|
+
findById(id: string): { id: string; name: string } {
|
|
39
|
+
return { id, name: `User ${id}` };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Service()
|
|
44
|
+
class ServiceWithConfig extends BaseService {
|
|
45
|
+
getDbUrl(): unknown {
|
|
46
|
+
return this.config.get('database.url');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Service()
|
|
51
|
+
class ServiceWithDeps extends BaseService {
|
|
52
|
+
constructor(private readonly repo: { find: () => string }) {
|
|
53
|
+
super();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
getData(): string {
|
|
57
|
+
return this.repo.find();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Controller('/users')
|
|
62
|
+
class UserController extends BaseController {
|
|
63
|
+
constructor(private readonly userService: UserService) {
|
|
64
|
+
super();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Get('/:id')
|
|
68
|
+
getUser(@Param('id') id: string) {
|
|
69
|
+
return this.userService.findById(id);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// createTestService — docs/testing.md
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
describe('docs/testing.md — createTestService', () => {
|
|
78
|
+
/**
|
|
79
|
+
* @source docs/testing.md#createtestservice
|
|
80
|
+
*/
|
|
81
|
+
it('basic usage — creates service with mock logger and config', () => {
|
|
82
|
+
const { instance, logger, config } = createTestService(UserService);
|
|
83
|
+
|
|
84
|
+
const result = instance.findById('123');
|
|
85
|
+
|
|
86
|
+
expect(result).toEqual({ id: '123', name: 'User 123' });
|
|
87
|
+
expect(logger).toBeDefined();
|
|
88
|
+
expect(config).toBeDefined();
|
|
89
|
+
expect(config.isInitialized).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @source docs/testing.md#with-config-and-dependencies
|
|
94
|
+
*/
|
|
95
|
+
it('with config — config.get returns provided values', () => {
|
|
96
|
+
const { instance } = createTestService(ServiceWithConfig, {
|
|
97
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
98
|
+
config: { 'database.url': 'postgres://localhost/test' },
|
|
99
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(instance.getDbUrl()).toBe('postgres://localhost/test');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @source docs/testing.md#with-config-and-dependencies
|
|
107
|
+
*/
|
|
108
|
+
it('with deps — passes constructor dependencies', () => {
|
|
109
|
+
const mockRepo = { find: () => 'mock-data' };
|
|
110
|
+
const { instance } = createTestService(ServiceWithDeps, {
|
|
111
|
+
deps: [mockRepo],
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(instance.getData()).toBe('mock-data');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// createTestController — docs/testing.md
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
describe('docs/testing.md — createTestController', () => {
|
|
123
|
+
/**
|
|
124
|
+
* @source docs/testing.md#createtestcontroller
|
|
125
|
+
*/
|
|
126
|
+
it('basic usage — creates controller with mock logger and config', () => {
|
|
127
|
+
const mockUserService = { findById: (id: string) => ({ id, name: 'Mock' }) };
|
|
128
|
+
const { instance, logger, config } = createTestController(UserController, {
|
|
129
|
+
deps: [mockUserService],
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(instance).toBeInstanceOf(UserController);
|
|
133
|
+
expect(logger).toBeDefined();
|
|
134
|
+
expect(config).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// ============================================================================
|
|
139
|
+
// TestingModule — docs/testing.md
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
describe('docs/testing.md — TestingModule', () => {
|
|
143
|
+
/**
|
|
144
|
+
* @source docs/testing.md#basic-usage-1
|
|
145
|
+
*/
|
|
146
|
+
it('basic compile / inject / close flow', async () => {
|
|
147
|
+
let module: CompiledTestingModule | undefined;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
module = await TestingModule
|
|
151
|
+
.create({
|
|
152
|
+
controllers: [UserController],
|
|
153
|
+
providers: [UserService],
|
|
154
|
+
})
|
|
155
|
+
.compile();
|
|
156
|
+
|
|
157
|
+
const response = await module.inject('GET', '/users/42');
|
|
158
|
+
|
|
159
|
+
expect(response.status).toBe(200);
|
|
160
|
+
|
|
161
|
+
const body = await response.json() as { result: { id: string; name: string } };
|
|
162
|
+
expect(body.result.id).toBe('42');
|
|
163
|
+
expect(body.result.name).toBe('User 42');
|
|
164
|
+
} finally {
|
|
165
|
+
await module?.close();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @source docs/testing.md#overrideproviderserviceclass
|
|
171
|
+
*/
|
|
172
|
+
it('overrideProvider — replaces service with mock value', async () => {
|
|
173
|
+
const mockUser = { id: '1', name: 'MockUser' };
|
|
174
|
+
let module: CompiledTestingModule | undefined;
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
module = await TestingModule
|
|
178
|
+
.create({ controllers: [UserController], providers: [UserService] })
|
|
179
|
+
.overrideProvider(UserService).useValue({ findById: () => mockUser })
|
|
180
|
+
.compile();
|
|
181
|
+
|
|
182
|
+
const response = await module.inject('GET', '/users/1');
|
|
183
|
+
|
|
184
|
+
expect(response.status).toBe(200);
|
|
185
|
+
|
|
186
|
+
const body = await response.json() as { result: { id: string; name: string } };
|
|
187
|
+
expect(body.result.name).toBe('MockUser');
|
|
188
|
+
} finally {
|
|
189
|
+
await module?.close();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* @source docs/testing.md#setoptionsoptions
|
|
195
|
+
*/
|
|
196
|
+
it('setOptions — applies basePath to routes', async () => {
|
|
197
|
+
let module: CompiledTestingModule | undefined;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
module = await TestingModule
|
|
201
|
+
.create({ controllers: [UserController], providers: [UserService] })
|
|
202
|
+
.setOptions({ basePath: '/api' })
|
|
203
|
+
.compile();
|
|
204
|
+
|
|
205
|
+
const response = await module.inject('GET', '/api/users/1');
|
|
206
|
+
|
|
207
|
+
expect(response.status).toBe(200);
|
|
208
|
+
} finally {
|
|
209
|
+
await module?.close();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// useFakeTimers — docs/testing.md
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
describe('docs/testing.md — useFakeTimers', () => {
|
|
219
|
+
/**
|
|
220
|
+
* @source docs/testing.md#usefaketimers
|
|
221
|
+
*/
|
|
222
|
+
it('basic usage — advance time and trigger setTimeout', () => {
|
|
223
|
+
const timers = useFakeTimers();
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
let called = false;
|
|
227
|
+
setTimeout(() => {
|
|
228
|
+
called = true;
|
|
229
|
+
}, 1000);
|
|
230
|
+
|
|
231
|
+
timers.advanceTime(999);
|
|
232
|
+
expect(called).toBe(false);
|
|
233
|
+
|
|
234
|
+
timers.advanceTime(1);
|
|
235
|
+
expect(called).toBe(true);
|
|
236
|
+
} finally {
|
|
237
|
+
timers.restore();
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ============================================================================
|
|
243
|
+
// createMockLogger — docs/testing.md
|
|
244
|
+
// ============================================================================
|
|
245
|
+
|
|
246
|
+
describe('docs/testing.md — createMockLogger', () => {
|
|
247
|
+
/**
|
|
248
|
+
* @source docs/testing.md#createmocklogger
|
|
249
|
+
*/
|
|
250
|
+
it('basic usage — creates silent async logger', () => {
|
|
251
|
+
const logger = createMockLogger();
|
|
252
|
+
|
|
253
|
+
expect(logger).toBeDefined();
|
|
254
|
+
expect(typeof logger.info).toBe('function');
|
|
255
|
+
expect(typeof logger.child).toBe('function');
|
|
256
|
+
expect(logger.child({ context: 'test' })).toBe(logger);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// createMockConfig — docs/testing.md
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
describe('docs/testing.md — createMockConfig', () => {
|
|
265
|
+
/**
|
|
266
|
+
* @source docs/testing.md#createmockconfig
|
|
267
|
+
*/
|
|
268
|
+
it('basic usage — returns values from provided map', () => {
|
|
269
|
+
const config = createMockConfig({
|
|
270
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
271
|
+
'server.port': 3000,
|
|
272
|
+
'server.host': '0.0.0.0',
|
|
273
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(config.get('server.port')).toBe(3000);
|
|
277
|
+
expect(config.get('server.host')).toBe('0.0.0.0');
|
|
278
|
+
expect(config.isInitialized).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
});
|
package/src/testing/index.ts
CHANGED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
expect,
|
|
4
|
+
mock,
|
|
5
|
+
test,
|
|
6
|
+
} from 'bun:test';
|
|
7
|
+
|
|
8
|
+
import { Controller } from '../decorators/decorators';
|
|
9
|
+
import { Controller as BaseController } from '../module/controller';
|
|
10
|
+
import { BaseService, Service } from '../module/service';
|
|
11
|
+
|
|
12
|
+
import { createTestController, createTestService } from './service-helpers';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Test fixtures
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
@Service()
|
|
19
|
+
class TestService extends BaseService {
|
|
20
|
+
getValue(): string {
|
|
21
|
+
return 'service-value';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Service()
|
|
26
|
+
class ServiceWithDeps extends BaseService {
|
|
27
|
+
constructor(private readonly dep: { name: string }) {
|
|
28
|
+
super();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getDep(): string {
|
|
32
|
+
return this.dep.name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@Controller('/test')
|
|
37
|
+
class TestController extends BaseController {
|
|
38
|
+
handle(): string {
|
|
39
|
+
return 'controller-value';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Controller('/test-deps')
|
|
44
|
+
class ControllerWithDeps extends BaseController {
|
|
45
|
+
constructor(private readonly dep: { name: string }) {
|
|
46
|
+
super();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getDep(): string {
|
|
50
|
+
return this.dep.name;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// createTestService
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
describe('createTestService', () => {
|
|
59
|
+
test('creates service instance with mock logger and config', () => {
|
|
60
|
+
const { instance, logger, config } = createTestService(TestService);
|
|
61
|
+
|
|
62
|
+
expect(instance).toBeInstanceOf(TestService);
|
|
63
|
+
expect(instance.getValue()).toBe('service-value');
|
|
64
|
+
expect(logger).toBeDefined();
|
|
65
|
+
expect(config).toBeDefined();
|
|
66
|
+
expect(config.isInitialized).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('config.get returns provided config values', () => {
|
|
70
|
+
const { config } = createTestService(TestService, {
|
|
71
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
72
|
+
config: { 'app.name': 'my-app', 'server.port': 3000 },
|
|
73
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(config.get('app.name')).toBe('my-app');
|
|
77
|
+
expect(config.get('server.port')).toBe(3000);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('passes constructor dependencies', () => {
|
|
81
|
+
const { instance } = createTestService(ServiceWithDeps, {
|
|
82
|
+
deps: [{ name: 'test-dep' }],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(instance.getDep()).toBe('test-dep');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('logger methods are mock functions with .mock.calls', () => {
|
|
89
|
+
const { logger } = createTestService(TestService);
|
|
90
|
+
|
|
91
|
+
// initializeService calls logger.child() and then debug() internally,
|
|
92
|
+
// so we track calls made after initialization
|
|
93
|
+
const debugCallsBefore = (logger.debug as ReturnType<typeof mock>).mock.calls.length;
|
|
94
|
+
|
|
95
|
+
logger.info('test message');
|
|
96
|
+
logger.warn('warning');
|
|
97
|
+
logger.debug('debug');
|
|
98
|
+
|
|
99
|
+
expect((logger.info as ReturnType<typeof mock>).mock.calls).toHaveLength(1);
|
|
100
|
+
expect((logger.warn as ReturnType<typeof mock>).mock.calls).toHaveLength(1);
|
|
101
|
+
expect((logger.debug as ReturnType<typeof mock>).mock.calls.length - debugCallsBefore).toBe(1);
|
|
102
|
+
expect((logger.error as ReturnType<typeof mock>).mock.calls).toHaveLength(0);
|
|
103
|
+
expect((logger.trace as ReturnType<typeof mock>).mock.calls).toHaveLength(0);
|
|
104
|
+
expect((logger.fatal as ReturnType<typeof mock>).mock.calls).toHaveLength(0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('initializes service via initializeService', () => {
|
|
108
|
+
const { instance } = createTestService(TestService);
|
|
109
|
+
|
|
110
|
+
// After initializeService, the service should have logger and config set
|
|
111
|
+
// We verify this indirectly — the service was created and initialized without errors
|
|
112
|
+
expect(instance).toBeInstanceOf(TestService);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ============================================================================
|
|
117
|
+
// createTestController
|
|
118
|
+
// ============================================================================
|
|
119
|
+
|
|
120
|
+
describe('createTestController', () => {
|
|
121
|
+
test('creates controller instance with mock logger and config', () => {
|
|
122
|
+
const { instance, logger, config } = createTestController(TestController);
|
|
123
|
+
|
|
124
|
+
expect(instance).toBeInstanceOf(TestController);
|
|
125
|
+
expect(instance.handle()).toBe('controller-value');
|
|
126
|
+
expect(logger).toBeDefined();
|
|
127
|
+
expect(config).toBeDefined();
|
|
128
|
+
expect(config.isInitialized).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('config.get returns provided config values', () => {
|
|
132
|
+
const { config } = createTestController(TestController, {
|
|
133
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
134
|
+
config: { 'app.name': 'my-app', 'server.port': 3000 },
|
|
135
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(config.get('app.name')).toBe('my-app');
|
|
139
|
+
expect(config.get('server.port')).toBe(3000);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('passes constructor dependencies', () => {
|
|
143
|
+
const { instance } = createTestController(ControllerWithDeps, {
|
|
144
|
+
deps: [{ name: 'test-dep' }],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(instance.getDep()).toBe('test-dep');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('logger methods are mock functions with .mock.calls', () => {
|
|
151
|
+
const { logger } = createTestController(TestController);
|
|
152
|
+
|
|
153
|
+
logger.info('test message');
|
|
154
|
+
logger.error('error');
|
|
155
|
+
|
|
156
|
+
expect((logger.info as ReturnType<typeof mock>).mock.calls).toHaveLength(1);
|
|
157
|
+
expect((logger.error as ReturnType<typeof mock>).mock.calls).toHaveLength(1);
|
|
158
|
+
expect((logger.warn as ReturnType<typeof mock>).mock.calls).toHaveLength(0);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('initializes controller via initializeController', () => {
|
|
162
|
+
const { instance } = createTestController(TestController);
|
|
163
|
+
|
|
164
|
+
expect(instance).toBeInstanceOf(TestController);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mock } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import type { IConfig, OneBunAppConfig } from '../module/config.interface';
|
|
4
|
+
|
|
5
|
+
import type { SyncLogger } from '@onebun/logger';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import { createMockConfig } from './test-utils';
|
|
9
|
+
|
|
10
|
+
export interface TestInstanceResult<T> {
|
|
11
|
+
instance: T;
|
|
12
|
+
logger: SyncLogger;
|
|
13
|
+
config: IConfig<OneBunAppConfig>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface CreateTestOptions {
|
|
17
|
+
config?: Record<string, unknown>;
|
|
18
|
+
deps?: unknown[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createMockableSyncLogger(): SyncLogger {
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
23
|
+
const noOp = () => {};
|
|
24
|
+
const logger: SyncLogger = {
|
|
25
|
+
trace: mock(noOp),
|
|
26
|
+
debug: mock(noOp),
|
|
27
|
+
info: mock(noOp),
|
|
28
|
+
warn: mock(noOp),
|
|
29
|
+
error: mock(noOp),
|
|
30
|
+
fatal: mock(noOp),
|
|
31
|
+
child: mock(() => logger),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return logger;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function createTestService<T>(
|
|
38
|
+
serviceClass: new (...args: never[]) => T,
|
|
39
|
+
options?: CreateTestOptions,
|
|
40
|
+
): TestInstanceResult<T> {
|
|
41
|
+
const logger = createMockableSyncLogger();
|
|
42
|
+
const config = createMockConfig(options?.config ?? {});
|
|
43
|
+
const deps = options?.deps ?? [];
|
|
44
|
+
|
|
45
|
+
const instance = new serviceClass(...deps as never[]);
|
|
46
|
+
|
|
47
|
+
if (typeof (instance as Record<string, unknown>).initializeService === 'function') {
|
|
48
|
+
(instance as Record<string, (...args: unknown[]) => void>).initializeService(logger, config);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { instance, logger, config };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createTestController<T>(
|
|
55
|
+
controllerClass: new (...args: never[]) => T,
|
|
56
|
+
options?: CreateTestOptions,
|
|
57
|
+
): TestInstanceResult<T> {
|
|
58
|
+
const logger = createMockableSyncLogger();
|
|
59
|
+
const config = createMockConfig(options?.config ?? {});
|
|
60
|
+
const deps = options?.deps ?? [];
|
|
61
|
+
|
|
62
|
+
const instance = new controllerClass(...deps as never[]);
|
|
63
|
+
|
|
64
|
+
if (typeof (instance as Record<string, unknown>).initializeController === 'function') {
|
|
65
|
+
(instance as Record<string, (...args: unknown[]) => void>).initializeController(logger, config);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { instance, logger, config };
|
|
69
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
it,
|
|
7
7
|
} from 'bun:test';
|
|
8
8
|
|
|
9
|
+
import { OneBunApplication } from '../application/application';
|
|
9
10
|
import {
|
|
10
11
|
Controller,
|
|
11
12
|
Get,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
import { Controller as BaseController } from '../module/controller';
|
|
18
19
|
import { BaseService, Service } from '../module/service';
|
|
19
20
|
|
|
21
|
+
|
|
20
22
|
import { TestingModule, type CompiledTestingModule } from './testing-module';
|
|
21
23
|
|
|
22
24
|
// ============================================================================
|
|
@@ -146,6 +148,84 @@ describe('TestingModule', () => {
|
|
|
146
148
|
});
|
|
147
149
|
});
|
|
148
150
|
|
|
151
|
+
describe('setOptions()', () => {
|
|
152
|
+
it('passes options to the application', async () => {
|
|
153
|
+
const module = await TestingModule
|
|
154
|
+
.create({
|
|
155
|
+
controllers: [GreetController],
|
|
156
|
+
providers: [GreetingService],
|
|
157
|
+
})
|
|
158
|
+
.setOptions({ basePath: '/api' })
|
|
159
|
+
.compile();
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Without basePath prefix the route should not match
|
|
163
|
+
const notFound = await module.inject('GET', '/greet/world');
|
|
164
|
+
expect(notFound.status).toBe(404);
|
|
165
|
+
|
|
166
|
+
// With basePath prefix the route should match
|
|
167
|
+
const found = await module.inject('GET', '/api/greet/world');
|
|
168
|
+
expect(found.status).toBe(200);
|
|
169
|
+
} finally {
|
|
170
|
+
await module.close();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('getApp()', () => {
|
|
176
|
+
it('returns an OneBunApplication instance', async () => {
|
|
177
|
+
const module = await TestingModule
|
|
178
|
+
.create({
|
|
179
|
+
controllers: [GreetController],
|
|
180
|
+
providers: [GreetingService],
|
|
181
|
+
})
|
|
182
|
+
.compile();
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
expect(module.getApp()).toBeInstanceOf(OneBunApplication);
|
|
186
|
+
} finally {
|
|
187
|
+
await module.close();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe('getPort()', () => {
|
|
193
|
+
it('returns a port greater than 0', async () => {
|
|
194
|
+
const module = await TestingModule
|
|
195
|
+
.create({
|
|
196
|
+
controllers: [GreetController],
|
|
197
|
+
providers: [GreetingService],
|
|
198
|
+
})
|
|
199
|
+
.compile();
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
expect(module.getPort()).toBeGreaterThan(0);
|
|
203
|
+
} finally {
|
|
204
|
+
await module.close();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('getConfig()', () => {
|
|
210
|
+
it('returns config object when envSchema is provided', async () => {
|
|
211
|
+
const module = await TestingModule
|
|
212
|
+
.create({
|
|
213
|
+
controllers: [GreetController],
|
|
214
|
+
providers: [GreetingService],
|
|
215
|
+
})
|
|
216
|
+
.setOptions({ envSchema: {} })
|
|
217
|
+
.compile();
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const config = module.getConfig();
|
|
221
|
+
expect(config).toBeDefined();
|
|
222
|
+
expect(typeof config.get).toBe('function');
|
|
223
|
+
} finally {
|
|
224
|
+
await module.close();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
149
229
|
describe('overrideProvider()', () => {
|
|
150
230
|
it('useValue() replaces service so controller uses mock', async () => {
|
|
151
231
|
const mockService = {
|
|
@@ -17,7 +17,12 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import type { OneBunApplication } from '../application/application';
|
|
20
|
-
import type {
|
|
20
|
+
import type { IConfig, OneBunAppConfig } from '../module/config.interface';
|
|
21
|
+
import type {
|
|
22
|
+
ApplicationOptions,
|
|
23
|
+
HttpMethod,
|
|
24
|
+
OneBunResponse,
|
|
25
|
+
} from '../types';
|
|
21
26
|
import type { Context } from 'effect';
|
|
22
27
|
|
|
23
28
|
import { Module } from '../decorators/decorators';
|
|
@@ -123,6 +128,31 @@ export class CompiledTestingModule {
|
|
|
123
128
|
}) as OneBunResponse;
|
|
124
129
|
}
|
|
125
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Get the underlying OneBunApplication instance.
|
|
133
|
+
* Useful for accessing application-level APIs not exposed by the testing module.
|
|
134
|
+
*/
|
|
135
|
+
getApp(): OneBunApplication {
|
|
136
|
+
return this.app;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get the port the test server is listening on.
|
|
141
|
+
*/
|
|
142
|
+
getPort(): number {
|
|
143
|
+
return this.port;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get the application configuration.
|
|
148
|
+
* Requires `envSchema` to be set via `setOptions()`.
|
|
149
|
+
*
|
|
150
|
+
* @throws If configuration was not initialized (no envSchema provided)
|
|
151
|
+
*/
|
|
152
|
+
getConfig(): IConfig<OneBunAppConfig> {
|
|
153
|
+
return this.app.getConfig();
|
|
154
|
+
}
|
|
155
|
+
|
|
126
156
|
/**
|
|
127
157
|
* Stop the test server and release resources.
|
|
128
158
|
* Call this in `afterEach` / `afterAll` to prevent port leaks.
|
|
@@ -176,6 +206,7 @@ export class TestingModule {
|
|
|
176
206
|
tag: Context.Tag<any, any>;
|
|
177
207
|
value: unknown;
|
|
178
208
|
}> = [];
|
|
209
|
+
private appOptions: Partial<ApplicationOptions> = {};
|
|
179
210
|
|
|
180
211
|
private constructor(options: TestingModuleCreateOptions) {
|
|
181
212
|
this.options = options;
|
|
@@ -190,6 +221,19 @@ export class TestingModule {
|
|
|
190
221
|
return new TestingModule(options);
|
|
191
222
|
}
|
|
192
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Set additional application options (e.g. envSchema, cors, basePath).
|
|
226
|
+
* Options are merged into the application config. `gracefulShutdown` and
|
|
227
|
+
* `_testProviders` are always forced by the testing module.
|
|
228
|
+
*
|
|
229
|
+
* @param options - Partial application options to merge
|
|
230
|
+
*/
|
|
231
|
+
setOptions(options: Partial<ApplicationOptions>): TestingModule {
|
|
232
|
+
this.appOptions = options;
|
|
233
|
+
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
|
|
193
237
|
/**
|
|
194
238
|
* Override a provider with a mock value or class.
|
|
195
239
|
* Overrides are applied before `setup()` so controllers receive mocks at construction time.
|
|
@@ -239,8 +283,9 @@ export class TestingModule {
|
|
|
239
283
|
// - silent logger
|
|
240
284
|
// - test provider overrides injected before setup()
|
|
241
285
|
const app = new OneBunApplication(_TestingAppModule, {
|
|
242
|
-
port: 0,
|
|
243
286
|
loggerLayer: makeMockLoggerLayer() as import('effect').Layer.Layer<import('@onebun/logger').Logger>,
|
|
287
|
+
port: 0,
|
|
288
|
+
...this.appOptions,
|
|
244
289
|
gracefulShutdown: false,
|
|
245
290
|
_testProviders: this.overrides,
|
|
246
291
|
});
|