@onebun/core 0.2.10 → 0.2.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 +6 -2
- package/src/application/application.test.ts +554 -2
- package/src/application/application.ts +23 -6
- package/src/decorators/decorators.test.ts +1 -1
- package/src/decorators/decorators.ts +9 -2
- package/src/decorators/metadata.ts +25 -0
- package/src/docs-examples.test.ts +2 -1
- package/src/index.ts +2 -2
- package/src/queue/docs-examples.test.ts +90 -0
- package/src/queue/scheduler.ts +13 -2
- package/src/testing/docs-examples.test.ts +67 -1
- package/src/testing/test-utils.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebun/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Core package for OneBun framework - decorators, DI, modules, controllers",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -32,6 +32,10 @@
|
|
|
32
32
|
"src",
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
|
+
"exports": {
|
|
36
|
+
".": "./src/index.ts",
|
|
37
|
+
"./testing": "./src/testing/index.ts"
|
|
38
|
+
},
|
|
35
39
|
"main": "src/index.ts",
|
|
36
40
|
"module": "src/index.ts",
|
|
37
41
|
"types": "src/index.ts",
|
|
@@ -43,7 +47,7 @@
|
|
|
43
47
|
"arktype": "^2.0.0",
|
|
44
48
|
"@onebun/logger": "^0.2.1",
|
|
45
49
|
"@onebun/envs": "^0.2.1",
|
|
46
|
-
"@onebun/metrics": "^0.2.
|
|
50
|
+
"@onebun/metrics": "^0.2.2",
|
|
47
51
|
"@onebun/requests": "^0.2.1",
|
|
48
52
|
"@onebun/trace": "^0.2.1"
|
|
49
53
|
},
|
|
@@ -39,9 +39,16 @@ import {
|
|
|
39
39
|
import { Controller as BaseController } from '../module/controller';
|
|
40
40
|
import { BaseMiddleware } from '../module/middleware';
|
|
41
41
|
import { clearGlobalServicesRegistry } from '../module/module';
|
|
42
|
-
import { Service } from '../module/service';
|
|
42
|
+
import { BaseService, Service } from '../module/service';
|
|
43
43
|
import { QueueService, QUEUE_NOT_ENABLED_ERROR_MESSAGE } from '../queue';
|
|
44
|
-
import {
|
|
44
|
+
import { CronExpression } from '../queue/cron-expression';
|
|
45
|
+
import {
|
|
46
|
+
Cron,
|
|
47
|
+
Interval,
|
|
48
|
+
OnQueueReady,
|
|
49
|
+
Subscribe,
|
|
50
|
+
Timeout,
|
|
51
|
+
} from '../queue/decorators';
|
|
45
52
|
import { makeMockLoggerLayer } from '../testing/test-utils';
|
|
46
53
|
|
|
47
54
|
|
|
@@ -2699,6 +2706,43 @@ describe('OneBunApplication', () => {
|
|
|
2699
2706
|
expect(bodyListWithSlash.result).toEqual({ tasks: ['t1', 't2'], status: 'all' });
|
|
2700
2707
|
});
|
|
2701
2708
|
|
|
2709
|
+
test('should not produce double slash when @Controller() has no path prefix', async () => {
|
|
2710
|
+
@Controller()
|
|
2711
|
+
class NoPrefixController extends BaseController {
|
|
2712
|
+
@Get('/users')
|
|
2713
|
+
async listUsers(): Promise<OneBunResponse> {
|
|
2714
|
+
return this.success({ users: ['u1'] });
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2717
|
+
@Get('/')
|
|
2718
|
+
async root(): Promise<OneBunResponse> {
|
|
2719
|
+
return this.success({ root: true });
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
@Module({ controllers: [NoPrefixController] })
|
|
2724
|
+
class TestModule {}
|
|
2725
|
+
|
|
2726
|
+
const app = createTestApp(TestModule);
|
|
2727
|
+
await app.start();
|
|
2728
|
+
|
|
2729
|
+
// Route should be /users, not //users
|
|
2730
|
+
const responseUsers = await (mockServer as /* eslint-disable-line @typescript-eslint/no-explicit-any */ any).fetchHandler(
|
|
2731
|
+
new Request('http://localhost:3000/users', { method: 'GET' }),
|
|
2732
|
+
);
|
|
2733
|
+
expect(responseUsers.status).toBe(200);
|
|
2734
|
+
const bodyUsers = await responseUsers.json();
|
|
2735
|
+
expect(bodyUsers.result).toEqual({ users: ['u1'] });
|
|
2736
|
+
|
|
2737
|
+
// Route should be /, not empty
|
|
2738
|
+
const responseRoot = await (mockServer as /* eslint-disable-line @typescript-eslint/no-explicit-any */ any).fetchHandler(
|
|
2739
|
+
new Request('http://localhost:3000/', { method: 'GET' }),
|
|
2740
|
+
);
|
|
2741
|
+
expect(responseRoot.status).toBe(200);
|
|
2742
|
+
const bodyRoot = await responseRoot.json();
|
|
2743
|
+
expect(bodyRoot.result).toEqual({ root: true });
|
|
2744
|
+
});
|
|
2745
|
+
|
|
2702
2746
|
test('should normalize metrics route labels - trailing slash requests use same label as non-trailing', async () => {
|
|
2703
2747
|
@Controller('/api')
|
|
2704
2748
|
class ApiController extends BaseController {
|
|
@@ -4083,4 +4127,512 @@ describe('OneBunApplication', () => {
|
|
|
4083
4127
|
await app.stop();
|
|
4084
4128
|
});
|
|
4085
4129
|
});
|
|
4130
|
+
|
|
4131
|
+
describe('queue auto-detection', () => {
|
|
4132
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
4133
|
+
const noop = () => {};
|
|
4134
|
+
|
|
4135
|
+
afterEach(async () => {
|
|
4136
|
+
clearGlobalServicesRegistry();
|
|
4137
|
+
register.clear();
|
|
4138
|
+
});
|
|
4139
|
+
|
|
4140
|
+
test('auto-enables queue when controller has @Subscribe', async () => {
|
|
4141
|
+
@Controller('/auto-sub')
|
|
4142
|
+
class AutoSubController extends BaseController {
|
|
4143
|
+
@Subscribe('auto.test')
|
|
4144
|
+
async handle(): Promise<void> {}
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
@Module({ controllers: [AutoSubController] })
|
|
4148
|
+
class AutoSubModule {}
|
|
4149
|
+
|
|
4150
|
+
const app = createTestApp(AutoSubModule, { port: 0 });
|
|
4151
|
+
await app.start();
|
|
4152
|
+
|
|
4153
|
+
expect(app.getQueueService()).not.toBeNull();
|
|
4154
|
+
|
|
4155
|
+
await app.stop();
|
|
4156
|
+
});
|
|
4157
|
+
|
|
4158
|
+
test('auto-enables queue when controller has @Cron', async () => {
|
|
4159
|
+
@Controller('/auto-cron')
|
|
4160
|
+
class AutoCronController extends BaseController {
|
|
4161
|
+
@Cron(CronExpression.EVERY_MINUTE, { pattern: 'cron.test' })
|
|
4162
|
+
getData() {
|
|
4163
|
+
return { ts: Date.now() };
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
@Module({ controllers: [AutoCronController] })
|
|
4168
|
+
class AutoCronModule {}
|
|
4169
|
+
|
|
4170
|
+
const app = createTestApp(AutoCronModule, { port: 0 });
|
|
4171
|
+
await app.start();
|
|
4172
|
+
|
|
4173
|
+
expect(app.getQueueService()).not.toBeNull();
|
|
4174
|
+
|
|
4175
|
+
await app.stop();
|
|
4176
|
+
});
|
|
4177
|
+
|
|
4178
|
+
test('auto-enables queue when controller has @Interval', async () => {
|
|
4179
|
+
|
|
4180
|
+
const intervalMs = 5000;
|
|
4181
|
+
|
|
4182
|
+
@Controller('/auto-interval')
|
|
4183
|
+
class AutoIntervalController extends BaseController {
|
|
4184
|
+
@Interval(intervalMs, { pattern: 'interval.test' })
|
|
4185
|
+
getData() {
|
|
4186
|
+
return { ts: Date.now() };
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
|
|
4190
|
+
@Module({ controllers: [AutoIntervalController] })
|
|
4191
|
+
class AutoIntervalModule {}
|
|
4192
|
+
|
|
4193
|
+
const app = createTestApp(AutoIntervalModule, { port: 0 });
|
|
4194
|
+
await app.start();
|
|
4195
|
+
|
|
4196
|
+
expect(app.getQueueService()).not.toBeNull();
|
|
4197
|
+
|
|
4198
|
+
await app.stop();
|
|
4199
|
+
});
|
|
4200
|
+
|
|
4201
|
+
test('auto-enables queue when controller has @Timeout', async () => {
|
|
4202
|
+
|
|
4203
|
+
const timeoutMs = 1000;
|
|
4204
|
+
|
|
4205
|
+
@Controller('/auto-timeout')
|
|
4206
|
+
class AutoTimeoutController extends BaseController {
|
|
4207
|
+
@Timeout(timeoutMs, { pattern: 'timeout.test' })
|
|
4208
|
+
getData() {
|
|
4209
|
+
return { ts: Date.now() };
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
|
|
4213
|
+
@Module({ controllers: [AutoTimeoutController] })
|
|
4214
|
+
class AutoTimeoutModule {}
|
|
4215
|
+
|
|
4216
|
+
const app = createTestApp(AutoTimeoutModule, { port: 0 });
|
|
4217
|
+
await app.start();
|
|
4218
|
+
|
|
4219
|
+
expect(app.getQueueService()).not.toBeNull();
|
|
4220
|
+
|
|
4221
|
+
await app.stop();
|
|
4222
|
+
});
|
|
4223
|
+
|
|
4224
|
+
test('does not enable queue when no queue decorators present', async () => {
|
|
4225
|
+
@Controller('/plain')
|
|
4226
|
+
class PlainController extends BaseController {
|
|
4227
|
+
@Get('/')
|
|
4228
|
+
async index(): Promise<OneBunResponse> {
|
|
4229
|
+
return this.success({ ok: true });
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
@Module({ controllers: [PlainController] })
|
|
4234
|
+
class PlainModule {}
|
|
4235
|
+
|
|
4236
|
+
const app = createTestApp(PlainModule, { port: 0 });
|
|
4237
|
+
await app.start();
|
|
4238
|
+
|
|
4239
|
+
expect(app.getQueueService()).toBeNull();
|
|
4240
|
+
|
|
4241
|
+
await app.stop();
|
|
4242
|
+
});
|
|
4243
|
+
|
|
4244
|
+
test('does not enable queue when only lifecycle decorators present', async () => {
|
|
4245
|
+
@Controller('/lifecycle-only')
|
|
4246
|
+
class LifecycleOnlyController extends BaseController {
|
|
4247
|
+
@OnQueueReady()
|
|
4248
|
+
onReady() {
|
|
4249
|
+
noop();
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
@Module({ controllers: [LifecycleOnlyController] })
|
|
4254
|
+
class LifecycleOnlyModule {}
|
|
4255
|
+
|
|
4256
|
+
const app = createTestApp(LifecycleOnlyModule, { port: 0 });
|
|
4257
|
+
await app.start();
|
|
4258
|
+
|
|
4259
|
+
expect(app.getQueueService()).toBeNull();
|
|
4260
|
+
|
|
4261
|
+
await app.stop();
|
|
4262
|
+
});
|
|
4263
|
+
|
|
4264
|
+
test('enabled: false overrides auto-detection', async () => {
|
|
4265
|
+
@Controller('/force-off')
|
|
4266
|
+
class ForceOffController extends BaseController {
|
|
4267
|
+
@Subscribe('force.off')
|
|
4268
|
+
async handle(): Promise<void> {}
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
@Module({ controllers: [ForceOffController] })
|
|
4272
|
+
class ForceOffModule {}
|
|
4273
|
+
|
|
4274
|
+
const app = createTestApp(ForceOffModule, {
|
|
4275
|
+
port: 0,
|
|
4276
|
+
queue: { enabled: false },
|
|
4277
|
+
});
|
|
4278
|
+
await app.start();
|
|
4279
|
+
|
|
4280
|
+
expect(app.getQueueService()).toBeNull();
|
|
4281
|
+
|
|
4282
|
+
await app.stop();
|
|
4283
|
+
});
|
|
4284
|
+
|
|
4285
|
+
test('auto-detects from nested child module', async () => {
|
|
4286
|
+
@Controller('/child-auto')
|
|
4287
|
+
class ChildAutoController extends BaseController {
|
|
4288
|
+
@Subscribe('child.auto')
|
|
4289
|
+
async handle(): Promise<void> {}
|
|
4290
|
+
}
|
|
4291
|
+
|
|
4292
|
+
@Module({ controllers: [ChildAutoController] })
|
|
4293
|
+
class ChildAutoModule {}
|
|
4294
|
+
|
|
4295
|
+
@Module({ imports: [ChildAutoModule] })
|
|
4296
|
+
class ParentAutoModule {}
|
|
4297
|
+
|
|
4298
|
+
const app = createTestApp(ParentAutoModule, { port: 0 });
|
|
4299
|
+
await app.start();
|
|
4300
|
+
|
|
4301
|
+
expect(app.getQueueService()).not.toBeNull();
|
|
4302
|
+
|
|
4303
|
+
await app.stop();
|
|
4304
|
+
});
|
|
4305
|
+
|
|
4306
|
+
test('auto-detects from deeply nested module (grandchild)', async () => {
|
|
4307
|
+
@Controller('/grandchild')
|
|
4308
|
+
class GrandchildController extends BaseController {
|
|
4309
|
+
@Subscribe('grandchild.event')
|
|
4310
|
+
async handle(): Promise<void> {}
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
@Module({ controllers: [GrandchildController] })
|
|
4314
|
+
class GrandchildModule {}
|
|
4315
|
+
|
|
4316
|
+
@Module({ imports: [GrandchildModule] })
|
|
4317
|
+
class MiddleModule {}
|
|
4318
|
+
|
|
4319
|
+
@Module({ imports: [MiddleModule] })
|
|
4320
|
+
class TopModule {}
|
|
4321
|
+
|
|
4322
|
+
const app = createTestApp(TopModule, { port: 0 });
|
|
4323
|
+
await app.start();
|
|
4324
|
+
|
|
4325
|
+
expect(app.getQueueService()).not.toBeNull();
|
|
4326
|
+
|
|
4327
|
+
await app.stop();
|
|
4328
|
+
});
|
|
4329
|
+
|
|
4330
|
+
test('getQueueService() returns null when queue not enabled', async () => {
|
|
4331
|
+
@Controller('/no-queue')
|
|
4332
|
+
class NoQueueController extends BaseController {
|
|
4333
|
+
@Get('/')
|
|
4334
|
+
async index(): Promise<OneBunResponse> {
|
|
4335
|
+
return this.success({});
|
|
4336
|
+
}
|
|
4337
|
+
}
|
|
4338
|
+
|
|
4339
|
+
@Module({ controllers: [NoQueueController] })
|
|
4340
|
+
class NoQueueModule {}
|
|
4341
|
+
|
|
4342
|
+
const app = createTestApp(NoQueueModule, { port: 0 });
|
|
4343
|
+
await app.start();
|
|
4344
|
+
|
|
4345
|
+
const queueService = app.getQueueService();
|
|
4346
|
+
expect(queueService).toBeNull();
|
|
4347
|
+
|
|
4348
|
+
await app.stop();
|
|
4349
|
+
});
|
|
4350
|
+
});
|
|
4351
|
+
|
|
4352
|
+
describe('queue handler execution', () => {
|
|
4353
|
+
afterEach(async () => {
|
|
4354
|
+
clearGlobalServicesRegistry();
|
|
4355
|
+
register.clear();
|
|
4356
|
+
});
|
|
4357
|
+
|
|
4358
|
+
test('@Interval handler fires immediately on start (auto-detected, no explicit queue config)', async () => {
|
|
4359
|
+
let callCount = 0;
|
|
4360
|
+
const intervalMs = 60000;
|
|
4361
|
+
|
|
4362
|
+
@Controller('/interval-fire')
|
|
4363
|
+
class IntervalFireController extends BaseController {
|
|
4364
|
+
@Interval(intervalMs, { pattern: 'test.interval' })
|
|
4365
|
+
getData() {
|
|
4366
|
+
callCount++;
|
|
4367
|
+
|
|
4368
|
+
return { ts: Date.now() };
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
@Module({ controllers: [IntervalFireController] })
|
|
4373
|
+
class IntervalFireModule {}
|
|
4374
|
+
|
|
4375
|
+
const app = createTestApp(IntervalFireModule, { port: 0 });
|
|
4376
|
+
await app.start();
|
|
4377
|
+
|
|
4378
|
+
// scheduler.startIntervalJob calls executeJob immediately (fire-and-forget async)
|
|
4379
|
+
await new Promise(r => setTimeout(r, 50));
|
|
4380
|
+
|
|
4381
|
+
expect(app.getQueueService()).not.toBeNull();
|
|
4382
|
+
expect(callCount).toBe(1);
|
|
4383
|
+
|
|
4384
|
+
await app.stop();
|
|
4385
|
+
});
|
|
4386
|
+
|
|
4387
|
+
test('@Interval handler fires with explicit queue: { enabled: true }', async () => {
|
|
4388
|
+
let callCount = 0;
|
|
4389
|
+
const intervalMs = 60000;
|
|
4390
|
+
|
|
4391
|
+
@Controller('/interval-explicit')
|
|
4392
|
+
class IntervalExplicitController extends BaseController {
|
|
4393
|
+
@Interval(intervalMs, { pattern: 'test.explicit' })
|
|
4394
|
+
getData() {
|
|
4395
|
+
callCount++;
|
|
4396
|
+
|
|
4397
|
+
return { ts: Date.now() };
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
@Module({ controllers: [IntervalExplicitController] })
|
|
4402
|
+
class IntervalExplicitModule {}
|
|
4403
|
+
|
|
4404
|
+
const app = createTestApp(IntervalExplicitModule, {
|
|
4405
|
+
port: 0,
|
|
4406
|
+
queue: { enabled: true },
|
|
4407
|
+
});
|
|
4408
|
+
await app.start();
|
|
4409
|
+
|
|
4410
|
+
await new Promise(r => setTimeout(r, 50));
|
|
4411
|
+
|
|
4412
|
+
expect(callCount).toBe(1);
|
|
4413
|
+
|
|
4414
|
+
await app.stop();
|
|
4415
|
+
});
|
|
4416
|
+
|
|
4417
|
+
test('@Interval handler fires with injected service dependency', async () => {
|
|
4418
|
+
let callCount = 0;
|
|
4419
|
+
const intervalMs = 60000;
|
|
4420
|
+
|
|
4421
|
+
@Service()
|
|
4422
|
+
class WorkerService extends BaseService {
|
|
4423
|
+
async doWork(): Promise<string> {
|
|
4424
|
+
return 'done';
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
@Controller('/interval-di')
|
|
4429
|
+
class IntervalDIController extends BaseController {
|
|
4430
|
+
constructor(private readonly worker: WorkerService) {
|
|
4431
|
+
super();
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
@Interval(intervalMs, { pattern: 'test.di' })
|
|
4435
|
+
async getData(): Promise<Record<string, unknown>> {
|
|
4436
|
+
callCount++;
|
|
4437
|
+
|
|
4438
|
+
return { result: await this.worker.doWork() };
|
|
4439
|
+
}
|
|
4440
|
+
}
|
|
4441
|
+
|
|
4442
|
+
@Module({ controllers: [IntervalDIController], providers: [WorkerService] })
|
|
4443
|
+
class IntervalDIModule {}
|
|
4444
|
+
|
|
4445
|
+
const app = createTestApp(IntervalDIModule, { port: 0 });
|
|
4446
|
+
await app.start();
|
|
4447
|
+
|
|
4448
|
+
await new Promise(r => setTimeout(r, 50));
|
|
4449
|
+
|
|
4450
|
+
expect(callCount).toBe(1);
|
|
4451
|
+
|
|
4452
|
+
await app.stop();
|
|
4453
|
+
});
|
|
4454
|
+
|
|
4455
|
+
test('@Cron handler registers job in scheduler (auto-detected)', async () => {
|
|
4456
|
+
@Controller('/cron-register')
|
|
4457
|
+
class CronRegisterController extends BaseController {
|
|
4458
|
+
@Cron(CronExpression.EVERY_HOUR, { pattern: 'cron.hourly' })
|
|
4459
|
+
getData() {
|
|
4460
|
+
return { ts: Date.now() };
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
@Module({ controllers: [CronRegisterController] })
|
|
4465
|
+
class CronRegisterModule {}
|
|
4466
|
+
|
|
4467
|
+
const app = createTestApp(CronRegisterModule, { port: 0 });
|
|
4468
|
+
await app.start();
|
|
4469
|
+
|
|
4470
|
+
const scheduler = app.getQueueService()!.getScheduler();
|
|
4471
|
+
const jobs = scheduler.getJobs();
|
|
4472
|
+
expect(jobs.length).toBe(1);
|
|
4473
|
+
expect(jobs[0].pattern).toBe('cron.hourly');
|
|
4474
|
+
|
|
4475
|
+
await app.stop();
|
|
4476
|
+
});
|
|
4477
|
+
|
|
4478
|
+
test('@Timeout handler registers and fires once (auto-detected)', async () => {
|
|
4479
|
+
let callCount = 0;
|
|
4480
|
+
const timeoutMs = 10;
|
|
4481
|
+
|
|
4482
|
+
@Controller('/timeout-fire')
|
|
4483
|
+
class TimeoutFireController extends BaseController {
|
|
4484
|
+
@Timeout(timeoutMs, { pattern: 'test.timeout' })
|
|
4485
|
+
getData() {
|
|
4486
|
+
callCount++;
|
|
4487
|
+
|
|
4488
|
+
return { ts: Date.now() };
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
|
|
4492
|
+
@Module({ controllers: [TimeoutFireController] })
|
|
4493
|
+
class TimeoutFireModule {}
|
|
4494
|
+
|
|
4495
|
+
const app = createTestApp(TimeoutFireModule, { port: 0 });
|
|
4496
|
+
await app.start();
|
|
4497
|
+
|
|
4498
|
+
// Wait for timeout to fire
|
|
4499
|
+
await new Promise(r => setTimeout(r, 100));
|
|
4500
|
+
|
|
4501
|
+
expect(callCount).toBe(1);
|
|
4502
|
+
|
|
4503
|
+
await app.stop();
|
|
4504
|
+
});
|
|
4505
|
+
|
|
4506
|
+
test('scheduled decorators in nested child module auto-init queue and fire', async () => {
|
|
4507
|
+
let callCount = 0;
|
|
4508
|
+
const intervalMs = 60000;
|
|
4509
|
+
|
|
4510
|
+
@Controller('/nested-interval')
|
|
4511
|
+
class NestedIntervalController extends BaseController {
|
|
4512
|
+
@Interval(intervalMs, { pattern: 'nested.interval' })
|
|
4513
|
+
getData() {
|
|
4514
|
+
callCount++;
|
|
4515
|
+
|
|
4516
|
+
return { ts: Date.now() };
|
|
4517
|
+
}
|
|
4518
|
+
}
|
|
4519
|
+
|
|
4520
|
+
@Module({ controllers: [NestedIntervalController] })
|
|
4521
|
+
class ChildModule {}
|
|
4522
|
+
|
|
4523
|
+
@Module({ imports: [ChildModule] })
|
|
4524
|
+
class ParentModule {}
|
|
4525
|
+
|
|
4526
|
+
const app = createTestApp(ParentModule, { port: 0 });
|
|
4527
|
+
await app.start();
|
|
4528
|
+
|
|
4529
|
+
await new Promise(r => setTimeout(r, 50));
|
|
4530
|
+
|
|
4531
|
+
expect(app.getQueueService()).not.toBeNull();
|
|
4532
|
+
expect(callCount).toBe(1);
|
|
4533
|
+
|
|
4534
|
+
await app.stop();
|
|
4535
|
+
});
|
|
4536
|
+
|
|
4537
|
+
test('scheduled decorators in deeply nested module auto-init queue and fire', async () => {
|
|
4538
|
+
let callCount = 0;
|
|
4539
|
+
const intervalMs = 60000;
|
|
4540
|
+
|
|
4541
|
+
@Controller('/deep-interval')
|
|
4542
|
+
class DeepIntervalController extends BaseController {
|
|
4543
|
+
@Interval(intervalMs, { pattern: 'deep.interval' })
|
|
4544
|
+
getData() {
|
|
4545
|
+
callCount++;
|
|
4546
|
+
|
|
4547
|
+
return { ts: Date.now() };
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4551
|
+
@Module({ controllers: [DeepIntervalController] })
|
|
4552
|
+
class GrandchildModule {}
|
|
4553
|
+
|
|
4554
|
+
@Module({ imports: [GrandchildModule] })
|
|
4555
|
+
class MiddleModule {}
|
|
4556
|
+
|
|
4557
|
+
@Module({ imports: [MiddleModule] })
|
|
4558
|
+
class TopModule {}
|
|
4559
|
+
|
|
4560
|
+
const app = createTestApp(TopModule, { port: 0 });
|
|
4561
|
+
await app.start();
|
|
4562
|
+
|
|
4563
|
+
await new Promise(r => setTimeout(r, 50));
|
|
4564
|
+
|
|
4565
|
+
expect(app.getQueueService()).not.toBeNull();
|
|
4566
|
+
expect(callCount).toBe(1);
|
|
4567
|
+
|
|
4568
|
+
await app.stop();
|
|
4569
|
+
});
|
|
4570
|
+
|
|
4571
|
+
test('handler error does not break scheduler', async () => {
|
|
4572
|
+
let errorCallCount = 0;
|
|
4573
|
+
let successCallCount = 0;
|
|
4574
|
+
const intervalMs = 60000;
|
|
4575
|
+
|
|
4576
|
+
@Controller('/error-handler')
|
|
4577
|
+
class ErrorHandlerController extends BaseController {
|
|
4578
|
+
@Interval(intervalMs, { pattern: 'error.handler' })
|
|
4579
|
+
getErrorData() {
|
|
4580
|
+
errorCallCount++;
|
|
4581
|
+
throw new Error('Handler failed');
|
|
4582
|
+
}
|
|
4583
|
+
|
|
4584
|
+
@Interval(intervalMs, { pattern: 'success.handler' })
|
|
4585
|
+
getSuccessData() {
|
|
4586
|
+
successCallCount++;
|
|
4587
|
+
|
|
4588
|
+
return { ok: true };
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
@Module({ controllers: [ErrorHandlerController] })
|
|
4593
|
+
class ErrorHandlerModule {}
|
|
4594
|
+
|
|
4595
|
+
const app = createTestApp(ErrorHandlerModule, { port: 0 });
|
|
4596
|
+
await app.start();
|
|
4597
|
+
|
|
4598
|
+
await new Promise(r => setTimeout(r, 50));
|
|
4599
|
+
|
|
4600
|
+
// Both handlers should have been called despite one throwing
|
|
4601
|
+
expect(errorCallCount).toBe(1);
|
|
4602
|
+
expect(successCallCount).toBe(1);
|
|
4603
|
+
|
|
4604
|
+
await app.stop();
|
|
4605
|
+
});
|
|
4606
|
+
|
|
4607
|
+
test('only scheduled decorators (no @Subscribe) implicitly use in-memory adapter', async () => {
|
|
4608
|
+
@Controller('/scheduled-only')
|
|
4609
|
+
class ScheduledOnlyController extends BaseController {
|
|
4610
|
+
@Interval(60000, { pattern: 'scheduled.only' })
|
|
4611
|
+
getData() {
|
|
4612
|
+
return { ts: Date.now() };
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
@Cron(CronExpression.EVERY_HOUR, { pattern: 'scheduled.cron' })
|
|
4616
|
+
getCronData() {
|
|
4617
|
+
return { ts: Date.now() };
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
|
|
4621
|
+
@Module({ controllers: [ScheduledOnlyController] })
|
|
4622
|
+
class ScheduledOnlyModule {}
|
|
4623
|
+
|
|
4624
|
+
// No queue config at all — should auto-detect and use in-memory
|
|
4625
|
+
const app = createTestApp(ScheduledOnlyModule, { port: 0 });
|
|
4626
|
+
await app.start();
|
|
4627
|
+
|
|
4628
|
+
const qs = app.getQueueService();
|
|
4629
|
+
expect(qs).not.toBeNull();
|
|
4630
|
+
|
|
4631
|
+
const scheduler = qs!.getScheduler();
|
|
4632
|
+
const jobs = scheduler.getJobs();
|
|
4633
|
+
expect(jobs.length).toBe(2);
|
|
4634
|
+
|
|
4635
|
+
await app.stop();
|
|
4636
|
+
});
|
|
4637
|
+
});
|
|
4086
4638
|
});
|
|
@@ -1777,7 +1777,7 @@ export class OneBunApplication {
|
|
|
1777
1777
|
return false;
|
|
1778
1778
|
}
|
|
1779
1779
|
|
|
1780
|
-
return hasQueueDecorators(instance.constructor);
|
|
1780
|
+
return hasQueueDecorators(controller) || hasQueueDecorators(instance.constructor);
|
|
1781
1781
|
});
|
|
1782
1782
|
|
|
1783
1783
|
// Determine if queue should be enabled
|
|
@@ -1840,24 +1840,41 @@ export class OneBunApplication {
|
|
|
1840
1840
|
: queueOptions?.redis,
|
|
1841
1841
|
};
|
|
1842
1842
|
this.queueService = new QueueService(queueServiceConfig);
|
|
1843
|
-
|
|
1843
|
+
|
|
1844
1844
|
// Initialize with the adapter
|
|
1845
1845
|
await this.queueService.initialize(this.queueAdapter);
|
|
1846
1846
|
|
|
1847
|
+
// Wire scheduler error handler so failed jobs are logged
|
|
1848
|
+
this.queueService.getScheduler().setErrorHandler((jobName, error) => {
|
|
1849
|
+
this.logger.warn(`Scheduled job "${jobName}" failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1847
1852
|
// Register handlers from controllers using registerService
|
|
1848
1853
|
for (const controllerClass of controllers) {
|
|
1849
1854
|
const instance = this.ensureModule().getControllerInstance?.(controllerClass);
|
|
1850
1855
|
if (!instance) {
|
|
1856
|
+
this.logger.debug(`Queue: skipping controller ${controllerClass.name} (no instance found)`);
|
|
1851
1857
|
continue;
|
|
1852
1858
|
}
|
|
1853
|
-
|
|
1854
|
-
//
|
|
1855
|
-
if
|
|
1859
|
+
|
|
1860
|
+
// Check both controllerClass and instance.constructor for queue decorators
|
|
1861
|
+
// These should be identical, but if @Controller wrapping produces a different reference,
|
|
1862
|
+
// we check both to be safe
|
|
1863
|
+
const hasDecorators =
|
|
1864
|
+
hasQueueDecorators(controllerClass) || hasQueueDecorators(instance.constructor);
|
|
1865
|
+
|
|
1866
|
+
if (hasDecorators) {
|
|
1867
|
+
// Use whichever reference has the metadata for registration
|
|
1868
|
+
const registrationClass = hasQueueDecorators(controllerClass)
|
|
1869
|
+
? controllerClass
|
|
1870
|
+
: instance.constructor;
|
|
1856
1871
|
await this.queueService.registerService(
|
|
1857
1872
|
instance,
|
|
1858
|
-
|
|
1873
|
+
registrationClass as new (...args: unknown[]) => unknown,
|
|
1859
1874
|
);
|
|
1860
1875
|
this.logger.debug(`Registered queue handlers for controller: ${controllerClass.name}`);
|
|
1876
|
+
} else {
|
|
1877
|
+
this.logger.debug(`Queue: controller ${controllerClass.name} has no queue decorators`);
|
|
1861
1878
|
}
|
|
1862
1879
|
}
|
|
1863
1880
|
|
|
@@ -104,7 +104,7 @@ describe('decorators', () => {
|
|
|
104
104
|
|
|
105
105
|
const metadata = getControllerMetadata(TestController);
|
|
106
106
|
expect(metadata).toBeDefined();
|
|
107
|
-
expect(metadata?.path).toBe('
|
|
107
|
+
expect(metadata?.path).toBe('');
|
|
108
108
|
expect(metadata?.routes).toEqual([]);
|
|
109
109
|
});
|
|
110
110
|
|
|
@@ -15,7 +15,11 @@ import {
|
|
|
15
15
|
type RouteOptions,
|
|
16
16
|
} from '../types';
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
copyAllMetadata,
|
|
20
|
+
getConstructorParamTypes as getDesignParamTypes,
|
|
21
|
+
Reflect,
|
|
22
|
+
} from './metadata';
|
|
19
23
|
|
|
20
24
|
/**
|
|
21
25
|
* Metadata storage for controllers
|
|
@@ -152,7 +156,7 @@ export function controllerDecorator(basePath: string = '') {
|
|
|
152
156
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
157
|
return <T extends new (...args: any[]) => any>(target: T): T => {
|
|
154
158
|
const metadata: ControllerMetadata = {
|
|
155
|
-
path: basePath.startsWith('/') ? basePath : `/${basePath}
|
|
159
|
+
path: basePath === '' ? '' : (basePath.startsWith('/') ? basePath : `/${basePath}`),
|
|
156
160
|
routes: [],
|
|
157
161
|
};
|
|
158
162
|
|
|
@@ -174,6 +178,9 @@ export function controllerDecorator(basePath: string = '') {
|
|
|
174
178
|
}
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
// Copy all method-decorator metadata (queue decorators, etc.) from original to wrapped class
|
|
182
|
+
copyAllMetadata(target, WrappedController);
|
|
183
|
+
|
|
177
184
|
// Copy metadata and static properties
|
|
178
185
|
META_CONTROLLERS.set(WrappedController, metadata);
|
|
179
186
|
META_CONTROLLERS.set(target, metadata); // Keep original for compatibility
|
|
@@ -43,6 +43,31 @@ export function defineMetadata(
|
|
|
43
43
|
keyMetadata.set(propertyKey || '', metadataValue);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Copy all metadata from one target to another.
|
|
48
|
+
* Used by @Controller to preserve method-decorator metadata (e.g. queue decorators)
|
|
49
|
+
* when wrapping the original class.
|
|
50
|
+
*/
|
|
51
|
+
export function copyAllMetadata(source: object, destination: object): void {
|
|
52
|
+
const sourceMetadata = metadataStorage.get(source);
|
|
53
|
+
if (!sourceMetadata) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let destMetadata = metadataStorage.get(destination);
|
|
58
|
+
if (!destMetadata) {
|
|
59
|
+
destMetadata = new Map<string, Map<string | symbol, any>>();
|
|
60
|
+
metadataStorage.set(destination, destMetadata);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [metadataKey, keyMap] of sourceMetadata) {
|
|
64
|
+
// Only copy if destination doesn't already have this key
|
|
65
|
+
if (!destMetadata.has(metadataKey)) {
|
|
66
|
+
destMetadata.set(metadataKey, new Map(keyMap));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
46
71
|
/**
|
|
47
72
|
* Get metadata from a target object
|
|
48
73
|
* @param metadataKey - The key for the metadata
|
|
@@ -48,6 +48,8 @@ import type {
|
|
|
48
48
|
import type { HttpExecutionContext } from './types';
|
|
49
49
|
import type { ServerWebSocket } from 'bun';
|
|
50
50
|
|
|
51
|
+
import { makeMockLoggerLayer } from './testing';
|
|
52
|
+
|
|
51
53
|
import {
|
|
52
54
|
Controller,
|
|
53
55
|
Get,
|
|
@@ -109,7 +111,6 @@ import {
|
|
|
109
111
|
createWsClient,
|
|
110
112
|
createNativeWsClient,
|
|
111
113
|
matchPattern,
|
|
112
|
-
makeMockLoggerLayer,
|
|
113
114
|
hasOnModuleInit,
|
|
114
115
|
hasOnApplicationInit,
|
|
115
116
|
hasOnModuleDestroy,
|
package/src/index.ts
CHANGED
|
@@ -130,8 +130,8 @@ export * from './queue';
|
|
|
130
130
|
// Validation
|
|
131
131
|
export * from './validation';
|
|
132
132
|
|
|
133
|
-
// Testing Utilities
|
|
134
|
-
|
|
133
|
+
// Testing Utilities are available via '@onebun/core/testing' subpath import
|
|
134
|
+
// to avoid requiring testcontainers as a mandatory dependency
|
|
135
135
|
|
|
136
136
|
// HTTP Guards
|
|
137
137
|
export * from './http-guards';
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
getIntervalMetadata,
|
|
50
50
|
getTimeoutMetadata,
|
|
51
51
|
hasQueueDecorators,
|
|
52
|
+
QueueScheduler,
|
|
52
53
|
} from './index';
|
|
53
54
|
|
|
54
55
|
/**
|
|
@@ -593,3 +594,92 @@ describe('Feature Support Matrix (docs/api/queue.md)', () => {
|
|
|
593
594
|
expect(adapter.supports('retry')).toBe(false);
|
|
594
595
|
});
|
|
595
596
|
});
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* @source docs/api/queue.md#setup (scheduled-only tip)
|
|
600
|
+
*/
|
|
601
|
+
describe('Scheduled-only Controllers (docs/api/queue.md)', () => {
|
|
602
|
+
it('should auto-detect queue decorators on controller with only @Interval', () => {
|
|
603
|
+
// From docs/api/queue.md: Scheduled-only Controllers tip
|
|
604
|
+
class ScheduledOnlyController {
|
|
605
|
+
@Interval(60000, { pattern: 'metrics.collect' })
|
|
606
|
+
getMetrics() {
|
|
607
|
+
return { cpu: process.cpuUsage() };
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// No @Subscribe — only scheduling decorators
|
|
612
|
+
expect(hasQueueDecorators(ScheduledOnlyController)).toBe(true);
|
|
613
|
+
expect(getSubscribeMetadata(ScheduledOnlyController).length).toBe(0);
|
|
614
|
+
expect(getIntervalMetadata(ScheduledOnlyController).length).toBe(1);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it('should auto-detect queue decorators on controller with only @Cron', () => {
|
|
618
|
+
class CronOnlyController {
|
|
619
|
+
@Cron(CronExpression.EVERY_HOUR, { pattern: 'cleanup.expired' })
|
|
620
|
+
getCleanupData() {
|
|
621
|
+
return { timestamp: Date.now() };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
expect(hasQueueDecorators(CronOnlyController)).toBe(true);
|
|
626
|
+
expect(getSubscribeMetadata(CronOnlyController).length).toBe(0);
|
|
627
|
+
expect(getCronMetadata(CronOnlyController).length).toBe(1);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it('should auto-detect queue decorators on controller with only @Timeout', () => {
|
|
631
|
+
class TimeoutOnlyController {
|
|
632
|
+
@Timeout(5000, { pattern: 'startup.warmup' })
|
|
633
|
+
getWarmupData() {
|
|
634
|
+
return { type: 'warmup' };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
expect(hasQueueDecorators(TimeoutOnlyController)).toBe(true);
|
|
639
|
+
expect(getSubscribeMetadata(TimeoutOnlyController).length).toBe(0);
|
|
640
|
+
expect(getTimeoutMetadata(TimeoutOnlyController).length).toBe(1);
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* @source docs/api/queue.md#setup (error handling info)
|
|
646
|
+
*/
|
|
647
|
+
describe('Scheduled Job Error Handling (docs/api/queue.md)', () => {
|
|
648
|
+
let adapter: InMemoryQueueAdapter;
|
|
649
|
+
|
|
650
|
+
beforeEach(async () => {
|
|
651
|
+
adapter = new InMemoryQueueAdapter();
|
|
652
|
+
await adapter.connect();
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
afterEach(async () => {
|
|
656
|
+
await adapter.disconnect();
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it('should continue scheduler after handler error via setErrorHandler', async () => {
|
|
660
|
+
// From docs/api/queue.md: Scheduled Job Error Handling info
|
|
661
|
+
const scheduler = new QueueScheduler(adapter);
|
|
662
|
+
|
|
663
|
+
const errors: Array<{ name: string; error: unknown }> = [];
|
|
664
|
+
scheduler.setErrorHandler((name: string, error: unknown) => {
|
|
665
|
+
errors.push({ name, error });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Add a job that will fail
|
|
669
|
+
scheduler.addIntervalJob('failing-job', 60000, 'test.fail', () => {
|
|
670
|
+
throw new Error('Job failed');
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
scheduler.start();
|
|
674
|
+
|
|
675
|
+
// executeJob is async fire-and-forget, wait for it
|
|
676
|
+
await new Promise(r => setTimeout(r, 50));
|
|
677
|
+
|
|
678
|
+
// Error handler should have been called (immediate execution)
|
|
679
|
+
expect(errors.length).toBe(1);
|
|
680
|
+
expect(errors[0].name).toBe('failing-job');
|
|
681
|
+
expect((errors[0].error as Error).message).toBe('Job failed');
|
|
682
|
+
|
|
683
|
+
scheduler.stop();
|
|
684
|
+
});
|
|
685
|
+
});
|
package/src/queue/scheduler.ts
CHANGED
|
@@ -64,9 +64,17 @@ export class QueueScheduler {
|
|
|
64
64
|
private running = false;
|
|
65
65
|
private cronCheckInterval?: ReturnType<typeof setInterval>;
|
|
66
66
|
private readonly cronCheckIntervalMs = 1000; // Check cron jobs every second
|
|
67
|
+
private onJobError?: (jobName: string, error: unknown) => void;
|
|
67
68
|
|
|
68
69
|
constructor(private readonly adapter: QueueAdapter) {}
|
|
69
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Set error handler for scheduled job failures
|
|
73
|
+
*/
|
|
74
|
+
setErrorHandler(handler: (jobName: string, error: unknown) => void): void {
|
|
75
|
+
this.onJobError = handler;
|
|
76
|
+
}
|
|
77
|
+
|
|
70
78
|
/**
|
|
71
79
|
* Start the scheduler
|
|
72
80
|
*/
|
|
@@ -363,8 +371,11 @@ export class QueueScheduler {
|
|
|
363
371
|
await this.adapter.publish(job.pattern, data, {
|
|
364
372
|
metadata: job.metadata,
|
|
365
373
|
});
|
|
366
|
-
} catch {
|
|
367
|
-
//
|
|
374
|
+
} catch (error) {
|
|
375
|
+
// Report error via handler if set, otherwise silently continue
|
|
376
|
+
if (this.onJobError) {
|
|
377
|
+
this.onJobError(job.name, error);
|
|
378
|
+
}
|
|
368
379
|
} finally {
|
|
369
380
|
job.isRunning = false;
|
|
370
381
|
}
|
|
@@ -22,7 +22,11 @@ import { Controller as BaseController } from '../module/controller';
|
|
|
22
22
|
import { BaseService, Service } from '../module/service';
|
|
23
23
|
|
|
24
24
|
import { createTestController, createTestService } from './service-helpers';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
createMockConfig,
|
|
27
|
+
createMockLogger,
|
|
28
|
+
useFakeTimers,
|
|
29
|
+
} from './test-utils';
|
|
26
30
|
import { TestingModule } from './testing-module';
|
|
27
31
|
|
|
28
32
|
// ============================================================================
|
|
@@ -161,6 +165,50 @@ describe('docs/testing.md — TestingModule', () => {
|
|
|
161
165
|
await module?.close();
|
|
162
166
|
}
|
|
163
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
|
+
});
|
|
164
212
|
});
|
|
165
213
|
|
|
166
214
|
// ============================================================================
|
|
@@ -191,6 +239,24 @@ describe('docs/testing.md — useFakeTimers', () => {
|
|
|
191
239
|
});
|
|
192
240
|
});
|
|
193
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
|
+
|
|
194
260
|
// ============================================================================
|
|
195
261
|
// createMockConfig — docs/testing.md
|
|
196
262
|
// ============================================================================
|
|
@@ -232,7 +232,7 @@ export const fakeTimers = new FakeTimers();
|
|
|
232
232
|
*
|
|
233
233
|
* @example
|
|
234
234
|
* ```typescript
|
|
235
|
-
* import { useFakeTimers } from '@onebun/core';
|
|
235
|
+
* import { useFakeTimers } from '@onebun/core/testing';
|
|
236
236
|
*
|
|
237
237
|
* describe('My tests', () => {
|
|
238
238
|
* const { advanceTime, restore } = useFakeTimers();
|