@dangao/bun-server 1.7.1 → 1.8.0

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.
Files changed (116) hide show
  1. package/README.md +129 -21
  2. package/dist/di/decorators.d.ts +37 -0
  3. package/dist/di/decorators.d.ts.map +1 -1
  4. package/dist/di/index.d.ts +1 -1
  5. package/dist/di/index.d.ts.map +1 -1
  6. package/dist/di/module-registry.d.ts +17 -0
  7. package/dist/di/module-registry.d.ts.map +1 -1
  8. package/dist/events/decorators.d.ts +52 -0
  9. package/dist/events/decorators.d.ts.map +1 -0
  10. package/dist/events/event-module.d.ts +97 -0
  11. package/dist/events/event-module.d.ts.map +1 -0
  12. package/dist/events/index.d.ts +5 -0
  13. package/dist/events/index.d.ts.map +1 -0
  14. package/dist/events/service.d.ts +76 -0
  15. package/dist/events/service.d.ts.map +1 -0
  16. package/dist/events/types.d.ts +184 -0
  17. package/dist/events/types.d.ts.map +1 -0
  18. package/dist/index.d.ts +5 -3
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1511 -11
  21. package/dist/security/filter.d.ts +23 -0
  22. package/dist/security/filter.d.ts.map +1 -1
  23. package/dist/security/guards/builtin/auth-guard.d.ts +44 -0
  24. package/dist/security/guards/builtin/auth-guard.d.ts.map +1 -0
  25. package/dist/security/guards/builtin/index.d.ts +3 -0
  26. package/dist/security/guards/builtin/index.d.ts.map +1 -0
  27. package/dist/security/guards/builtin/roles-guard.d.ts +66 -0
  28. package/dist/security/guards/builtin/roles-guard.d.ts.map +1 -0
  29. package/dist/security/guards/decorators.d.ts +50 -0
  30. package/dist/security/guards/decorators.d.ts.map +1 -0
  31. package/dist/security/guards/execution-context.d.ts +56 -0
  32. package/dist/security/guards/execution-context.d.ts.map +1 -0
  33. package/dist/security/guards/guard-registry.d.ts +67 -0
  34. package/dist/security/guards/guard-registry.d.ts.map +1 -0
  35. package/dist/security/guards/index.d.ts +7 -0
  36. package/dist/security/guards/index.d.ts.map +1 -0
  37. package/dist/security/guards/reflector.d.ts +57 -0
  38. package/dist/security/guards/reflector.d.ts.map +1 -0
  39. package/dist/security/guards/types.d.ts +126 -0
  40. package/dist/security/guards/types.d.ts.map +1 -0
  41. package/dist/security/index.d.ts +1 -0
  42. package/dist/security/index.d.ts.map +1 -1
  43. package/dist/security/security-module.d.ts +20 -0
  44. package/dist/security/security-module.d.ts.map +1 -1
  45. package/dist/validation/class-validator.d.ts +108 -0
  46. package/dist/validation/class-validator.d.ts.map +1 -0
  47. package/dist/validation/custom-validator.d.ts +130 -0
  48. package/dist/validation/custom-validator.d.ts.map +1 -0
  49. package/dist/validation/errors.d.ts +22 -2
  50. package/dist/validation/errors.d.ts.map +1 -1
  51. package/dist/validation/index.d.ts +7 -1
  52. package/dist/validation/index.d.ts.map +1 -1
  53. package/dist/validation/rules/array.d.ts +33 -0
  54. package/dist/validation/rules/array.d.ts.map +1 -0
  55. package/dist/validation/rules/common.d.ts +90 -0
  56. package/dist/validation/rules/common.d.ts.map +1 -0
  57. package/dist/validation/rules/conditional.d.ts +30 -0
  58. package/dist/validation/rules/conditional.d.ts.map +1 -0
  59. package/dist/validation/rules/index.d.ts +5 -0
  60. package/dist/validation/rules/index.d.ts.map +1 -0
  61. package/dist/validation/rules/object.d.ts +30 -0
  62. package/dist/validation/rules/object.d.ts.map +1 -0
  63. package/dist/validation/types.d.ts +52 -1
  64. package/dist/validation/types.d.ts.map +1 -1
  65. package/docs/events.md +494 -0
  66. package/docs/guards.md +376 -0
  67. package/docs/guide.md +309 -1
  68. package/docs/request-lifecycle.md +444 -0
  69. package/docs/validation.md +407 -0
  70. package/docs/zh/events.md +494 -0
  71. package/docs/zh/guards.md +376 -0
  72. package/docs/zh/guide.md +309 -1
  73. package/docs/zh/request-lifecycle.md +444 -0
  74. package/docs/zh/validation.md +407 -0
  75. package/package.json +1 -1
  76. package/src/di/decorators.ts +46 -0
  77. package/src/di/index.ts +10 -1
  78. package/src/di/module-registry.ts +39 -0
  79. package/src/events/decorators.ts +103 -0
  80. package/src/events/event-module.ts +272 -0
  81. package/src/events/index.ts +32 -0
  82. package/src/events/service.ts +352 -0
  83. package/src/events/types.ts +223 -0
  84. package/src/index.ts +133 -1
  85. package/src/security/filter.ts +88 -8
  86. package/src/security/guards/builtin/auth-guard.ts +68 -0
  87. package/src/security/guards/builtin/index.ts +3 -0
  88. package/src/security/guards/builtin/roles-guard.ts +165 -0
  89. package/src/security/guards/decorators.ts +124 -0
  90. package/src/security/guards/execution-context.ts +152 -0
  91. package/src/security/guards/guard-registry.ts +164 -0
  92. package/src/security/guards/index.ts +7 -0
  93. package/src/security/guards/reflector.ts +99 -0
  94. package/src/security/guards/types.ts +144 -0
  95. package/src/security/index.ts +1 -0
  96. package/src/security/security-module.ts +72 -2
  97. package/src/validation/class-validator.ts +322 -0
  98. package/src/validation/custom-validator.ts +289 -0
  99. package/src/validation/errors.ts +50 -2
  100. package/src/validation/index.ts +103 -1
  101. package/src/validation/rules/array.ts +118 -0
  102. package/src/validation/rules/common.ts +286 -0
  103. package/src/validation/rules/conditional.ts +52 -0
  104. package/src/validation/rules/index.ts +51 -0
  105. package/src/validation/rules/object.ts +86 -0
  106. package/src/validation/types.ts +61 -1
  107. package/tests/di/global-module.test.ts +487 -0
  108. package/tests/events/event-decorators.test.ts +173 -0
  109. package/tests/events/event-emitter.test.ts +373 -0
  110. package/tests/events/event-module.test.ts +373 -0
  111. package/tests/security/guards/guards-integration.test.ts +371 -0
  112. package/tests/security/guards/guards.test.ts +775 -0
  113. package/tests/security/security-module.test.ts +2 -2
  114. package/tests/validation/class-validator.test.ts +349 -0
  115. package/tests/validation/custom-validator.test.ts +335 -0
  116. package/tests/validation/rules.test.ts +543 -0
@@ -0,0 +1,487 @@
1
+ import { beforeEach, afterEach, describe, expect, test } from 'bun:test';
2
+
3
+ import { Application } from '../../src/core/application';
4
+ import { Controller, ControllerRegistry } from '../../src/controller/controller';
5
+ import { GET } from '../../src/router/decorators';
6
+ import { Module } from '../../src/di/module';
7
+ import { ModuleRegistry } from '../../src/di/module-registry';
8
+ import { RouteRegistry } from '../../src/router/registry';
9
+ import { Injectable, Inject, Global, isGlobalModule, GLOBAL_MODULE_METADATA_KEY } from '../../src/di/decorators';
10
+ import { Context } from '../../src/core/context';
11
+
12
+ describe('Global Module', () => {
13
+ beforeEach(() => {
14
+ // 清理全局注册表,避免测试间污染
15
+ RouteRegistry.getInstance().clear();
16
+ ControllerRegistry.getInstance().clear();
17
+ ModuleRegistry.getInstance().clear();
18
+ });
19
+
20
+ afterEach(() => {
21
+ // 确保测试后清理
22
+ RouteRegistry.getInstance().clear();
23
+ ControllerRegistry.getInstance().clear();
24
+ ModuleRegistry.getInstance().clear();
25
+ });
26
+
27
+ describe('@Global() decorator', () => {
28
+ test('should mark module as global', () => {
29
+ @Global()
30
+ @Module({
31
+ providers: [],
32
+ })
33
+ class GlobalModule {}
34
+
35
+ expect(isGlobalModule(GlobalModule)).toBe(true);
36
+ });
37
+
38
+ test('should return false for non-global modules', () => {
39
+ @Module({
40
+ providers: [],
41
+ })
42
+ class RegularModule {}
43
+
44
+ expect(isGlobalModule(RegularModule)).toBe(false);
45
+ });
46
+
47
+ test('should set global metadata key', () => {
48
+ @Global()
49
+ @Module({
50
+ providers: [],
51
+ })
52
+ class GlobalModule {}
53
+
54
+ const metadata = Reflect.getMetadata(GLOBAL_MODULE_METADATA_KEY, GlobalModule);
55
+ expect(metadata).toBe(true);
56
+ });
57
+ });
58
+
59
+ describe('Global module exports', () => {
60
+ test('should allow accessing global module exports without explicit import', () => {
61
+ // 创建全局配置服务
62
+ const CONFIG_TOKEN = Symbol('config');
63
+
64
+ @Injectable()
65
+ class GlobalConfigService {
66
+ public readonly appName = 'TestApp';
67
+
68
+ public get(key: string): string {
69
+ return `config:${key}`;
70
+ }
71
+ }
72
+
73
+ @Global()
74
+ @Module({
75
+ providers: [
76
+ {
77
+ provide: CONFIG_TOKEN,
78
+ useClass: GlobalConfigService,
79
+ },
80
+ ],
81
+ exports: [CONFIG_TOKEN],
82
+ })
83
+ class GlobalConfigModule {}
84
+
85
+ // 创建一个使用全局服务的用户服务
86
+ @Injectable()
87
+ class UserService {
88
+ public constructor(@Inject(CONFIG_TOKEN) private readonly config: GlobalConfigService) {}
89
+
90
+ public getConfigValue(): string {
91
+ return this.config.get('user.prefix');
92
+ }
93
+ }
94
+
95
+ @Controller('/users')
96
+ class UserController {
97
+ public constructor(@Inject(UserService) private readonly userService: UserService) {}
98
+
99
+ @GET('/config')
100
+ public getConfig(): string {
101
+ return this.userService.getConfigValue();
102
+ }
103
+ }
104
+
105
+ // 用户模块不需要导入 GlobalConfigModule
106
+ @Module({
107
+ controllers: [UserController],
108
+ providers: [UserService],
109
+ })
110
+ class UserModule {}
111
+
112
+ // 注册模块
113
+ const app = new Application();
114
+ app.registerModule(GlobalConfigModule);
115
+ app.registerModule(UserModule);
116
+
117
+ // 验证全局模块被正确识别
118
+ const registry = ModuleRegistry.getInstance();
119
+ const globalModules = registry.getGlobalModules();
120
+ expect(globalModules).toContain(GlobalConfigModule);
121
+
122
+ // 验证用户模块可以解析全局提供者
123
+ const userModuleRef = registry.getModuleRef(UserModule);
124
+ expect(userModuleRef).toBeDefined();
125
+
126
+ // 通过父容器(根容器)解析全局服务
127
+ const userService = userModuleRef!.container.resolve(UserService);
128
+ expect(userService.getConfigValue()).toBe('config:user.prefix');
129
+ });
130
+
131
+ test('should share singleton instance across modules for global exports', () => {
132
+ const SHARED_TOKEN = Symbol('shared');
133
+
134
+ @Injectable()
135
+ class SharedService {
136
+ public readonly instanceId = Math.random();
137
+ }
138
+
139
+ @Global()
140
+ @Module({
141
+ providers: [
142
+ {
143
+ provide: SHARED_TOKEN,
144
+ useClass: SharedService,
145
+ },
146
+ ],
147
+ exports: [SHARED_TOKEN],
148
+ })
149
+ class SharedGlobalModule {}
150
+
151
+ @Injectable()
152
+ class ServiceA {
153
+ public constructor(@Inject(SHARED_TOKEN) public readonly shared: SharedService) {}
154
+ }
155
+
156
+ @Injectable()
157
+ class ServiceB {
158
+ public constructor(@Inject(SHARED_TOKEN) public readonly shared: SharedService) {}
159
+ }
160
+
161
+ @Module({
162
+ providers: [ServiceA],
163
+ })
164
+ class ModuleA {}
165
+
166
+ @Module({
167
+ providers: [ServiceB],
168
+ })
169
+ class ModuleB {}
170
+
171
+ const app = new Application();
172
+ app.registerModule(SharedGlobalModule);
173
+ app.registerModule(ModuleA);
174
+ app.registerModule(ModuleB);
175
+
176
+ const registry = ModuleRegistry.getInstance();
177
+ const moduleARef = registry.getModuleRef(ModuleA);
178
+ const moduleBRef = registry.getModuleRef(ModuleB);
179
+
180
+ const serviceA = moduleARef!.container.resolve(ServiceA);
181
+ const serviceB = moduleBRef!.container.resolve(ServiceB);
182
+
183
+ // 验证两个服务使用的是同一个共享实例
184
+ expect(serviceA.shared.instanceId).toBe(serviceB.shared.instanceId);
185
+ });
186
+
187
+ test('should work with class-based exports in global modules', () => {
188
+ @Injectable()
189
+ class LoggerService {
190
+ public log(message: string): string {
191
+ return `[LOG] ${message}`;
192
+ }
193
+ }
194
+
195
+ @Global()
196
+ @Module({
197
+ providers: [LoggerService],
198
+ exports: [LoggerService],
199
+ })
200
+ class LoggerGlobalModule {}
201
+
202
+ @Injectable()
203
+ class UserService {
204
+ public constructor(@Inject(LoggerService) private readonly logger: LoggerService) {}
205
+
206
+ public createUser(name: string): string {
207
+ return this.logger.log(`User created: ${name}`);
208
+ }
209
+ }
210
+
211
+ @Module({
212
+ providers: [UserService],
213
+ })
214
+ class UserModule {}
215
+
216
+ const app = new Application();
217
+ app.registerModule(LoggerGlobalModule);
218
+ app.registerModule(UserModule);
219
+
220
+ const registry = ModuleRegistry.getInstance();
221
+ const userModuleRef = registry.getModuleRef(UserModule);
222
+ const userService = userModuleRef!.container.resolve(UserService);
223
+
224
+ expect(userService.createUser('Alice')).toBe('[LOG] User created: Alice');
225
+ });
226
+
227
+ test('should not affect non-global module behavior', () => {
228
+ @Injectable()
229
+ class LocalService {
230
+ public getValue(): string {
231
+ return 'local';
232
+ }
233
+ }
234
+
235
+ // 非全局模块
236
+ @Module({
237
+ providers: [LocalService],
238
+ exports: [LocalService],
239
+ })
240
+ class LocalModule {}
241
+
242
+ @Injectable()
243
+ class ConsumerService {
244
+ public constructor(@Inject(LocalService) private readonly local: LocalService) {}
245
+
246
+ public consume(): string {
247
+ return this.local.getValue();
248
+ }
249
+ }
250
+
251
+ // 必须显式导入 LocalModule 才能使用 LocalService
252
+ @Module({
253
+ imports: [LocalModule],
254
+ providers: [ConsumerService],
255
+ })
256
+ class ConsumerModule {}
257
+
258
+ const app = new Application();
259
+ app.registerModule(ConsumerModule);
260
+
261
+ const registry = ModuleRegistry.getInstance();
262
+ const consumerRef = registry.getModuleRef(ConsumerModule);
263
+ const consumerService = consumerRef!.container.resolve(ConsumerService);
264
+
265
+ expect(consumerService.consume()).toBe('local');
266
+ });
267
+
268
+ test('should require explicit import for non-global module exports when modules are isolated', () => {
269
+ // 这个测试验证非全局模块的 exports 不会自动暴露给其他模块
270
+ // 除非显式导入
271
+
272
+ @Injectable()
273
+ class IsolatedService {
274
+ public getValue(): string {
275
+ return 'isolated';
276
+ }
277
+ }
278
+
279
+ // 非全局模块
280
+ @Module({
281
+ providers: [IsolatedService],
282
+ exports: [IsolatedService],
283
+ })
284
+ class IsolatedModule {}
285
+
286
+ @Injectable()
287
+ class ConsumerServiceWithImport {
288
+ public constructor(@Inject(IsolatedService) private readonly isolated: IsolatedService) {}
289
+
290
+ public consume(): string {
291
+ return this.isolated.getValue();
292
+ }
293
+ }
294
+
295
+ // 导入 IsolatedModule 才能使用 IsolatedService
296
+ @Module({
297
+ imports: [IsolatedModule],
298
+ providers: [ConsumerServiceWithImport],
299
+ })
300
+ class ConsumerModuleWithImport {}
301
+
302
+ const app = new Application();
303
+ app.registerModule(ConsumerModuleWithImport);
304
+
305
+ const registry = ModuleRegistry.getInstance();
306
+
307
+ // 验证非全局模块不在全局模块列表中
308
+ const globalModules = registry.getGlobalModules();
309
+ expect(globalModules).not.toContain(IsolatedModule);
310
+
311
+ // 但通过显式导入后可以正常使用
312
+ const consumerRef = registry.getModuleRef(ConsumerModuleWithImport);
313
+ const consumerService = consumerRef!.container.resolve(ConsumerServiceWithImport);
314
+ expect(consumerService.consume()).toBe('isolated');
315
+ });
316
+ });
317
+
318
+ describe('Multiple global modules', () => {
319
+ test('should support multiple global modules', () => {
320
+ const CONFIG_TOKEN = Symbol('config');
321
+ const LOGGER_TOKEN = Symbol('logger');
322
+
323
+ @Injectable()
324
+ class ConfigService {
325
+ public get(key: string): string {
326
+ return `config:${key}`;
327
+ }
328
+ }
329
+
330
+ @Injectable()
331
+ class LoggerService {
332
+ public log(msg: string): string {
333
+ return `[LOG] ${msg}`;
334
+ }
335
+ }
336
+
337
+ @Global()
338
+ @Module({
339
+ providers: [{ provide: CONFIG_TOKEN, useClass: ConfigService }],
340
+ exports: [CONFIG_TOKEN],
341
+ })
342
+ class ConfigGlobalModule {}
343
+
344
+ @Global()
345
+ @Module({
346
+ providers: [{ provide: LOGGER_TOKEN, useClass: LoggerService }],
347
+ exports: [LOGGER_TOKEN],
348
+ })
349
+ class LoggerGlobalModule {}
350
+
351
+ @Injectable()
352
+ class AppService {
353
+ public constructor(
354
+ @Inject(CONFIG_TOKEN) private readonly config: ConfigService,
355
+ @Inject(LOGGER_TOKEN) private readonly logger: LoggerService,
356
+ ) {}
357
+
358
+ public run(): string {
359
+ const configValue = this.config.get('app.name');
360
+ return this.logger.log(configValue);
361
+ }
362
+ }
363
+
364
+ @Module({
365
+ providers: [AppService],
366
+ })
367
+ class AppModule {}
368
+
369
+ const app = new Application();
370
+ app.registerModule(ConfigGlobalModule);
371
+ app.registerModule(LoggerGlobalModule);
372
+ app.registerModule(AppModule);
373
+
374
+ const registry = ModuleRegistry.getInstance();
375
+ const globalModules = registry.getGlobalModules();
376
+ expect(globalModules).toHaveLength(2);
377
+ expect(globalModules).toContain(ConfigGlobalModule);
378
+ expect(globalModules).toContain(LoggerGlobalModule);
379
+
380
+ const appRef = registry.getModuleRef(AppModule);
381
+ const appService = appRef!.container.resolve(AppService);
382
+ expect(appService.run()).toBe('[LOG] config:app.name');
383
+ });
384
+ });
385
+
386
+ describe('Global module with controllers', () => {
387
+ test('should register controllers from global modules', async () => {
388
+ @Injectable()
389
+ class StatusService {
390
+ public getStatus(): string {
391
+ return 'OK';
392
+ }
393
+ }
394
+
395
+ @Controller('/status')
396
+ class StatusController {
397
+ public constructor(@Inject(StatusService) private readonly statusService: StatusService) {}
398
+
399
+ @GET('/')
400
+ public getStatus(): string {
401
+ return this.statusService.getStatus();
402
+ }
403
+ }
404
+
405
+ @Global()
406
+ @Module({
407
+ controllers: [StatusController],
408
+ providers: [StatusService],
409
+ exports: [StatusService],
410
+ })
411
+ class StatusGlobalModule {}
412
+
413
+ const app = new Application();
414
+ app.registerModule(StatusGlobalModule);
415
+
416
+ // 验证控制器被正确注册
417
+ const router = RouteRegistry.getInstance().getRouter();
418
+ const context = new Context(new Request('http://localhost/status'));
419
+ const response = await router.handle(context);
420
+ expect(await response?.text()).toBe('OK');
421
+ });
422
+ });
423
+
424
+ describe('Global module integration with forRoot pattern', () => {
425
+ test('should work with forRoot pattern in global modules', () => {
426
+ const DB_TOKEN = Symbol('database');
427
+
428
+ interface DatabaseConfig {
429
+ host: string;
430
+ port: number;
431
+ }
432
+
433
+ @Injectable()
434
+ class DatabaseService {
435
+ public constructor(public readonly config: DatabaseConfig) {}
436
+
437
+ public getConnectionString(): string {
438
+ return `${this.config.host}:${this.config.port}`;
439
+ }
440
+ }
441
+
442
+ @Global()
443
+ @Module({})
444
+ class DatabaseGlobalModule {
445
+ public static forRoot(config: DatabaseConfig): typeof DatabaseGlobalModule {
446
+ Module({
447
+ providers: [
448
+ {
449
+ provide: DB_TOKEN,
450
+ useFactory: () => new DatabaseService(config),
451
+ },
452
+ ],
453
+ exports: [DB_TOKEN],
454
+ })(DatabaseGlobalModule);
455
+ return DatabaseGlobalModule;
456
+ }
457
+ }
458
+
459
+ @Injectable()
460
+ class UserRepository {
461
+ public constructor(@Inject(DB_TOKEN) private readonly db: DatabaseService) {}
462
+
463
+ public getDbInfo(): string {
464
+ return this.db.getConnectionString();
465
+ }
466
+ }
467
+
468
+ @Module({
469
+ providers: [UserRepository],
470
+ })
471
+ class UserModule {}
472
+
473
+ // 使用 forRoot 配置全局数据库模块
474
+ DatabaseGlobalModule.forRoot({ host: 'localhost', port: 5432 });
475
+
476
+ const app = new Application();
477
+ app.registerModule(DatabaseGlobalModule);
478
+ app.registerModule(UserModule);
479
+
480
+ const registry = ModuleRegistry.getInstance();
481
+ const userRef = registry.getModuleRef(UserModule);
482
+ const userRepo = userRef!.container.resolve(UserRepository);
483
+
484
+ expect(userRepo.getDbInfo()).toBe('localhost:5432');
485
+ });
486
+ });
487
+ });
@@ -0,0 +1,173 @@
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import 'reflect-metadata';
3
+ import {
4
+ OnEvent,
5
+ getOnEventMetadata,
6
+ isEventListenerClass,
7
+ } from '../../src/events/decorators';
8
+ import { Injectable } from '../../src/di/decorators';
9
+ import {
10
+ ON_EVENT_METADATA_KEY,
11
+ EVENT_LISTENER_CLASS_METADATA_KEY,
12
+ } from '../../src/events/types';
13
+
14
+ describe('OnEvent decorator', () => {
15
+ beforeEach(() => {
16
+ // 清理可能的元数据污染
17
+ });
18
+
19
+ test('should mark class as event listener class', () => {
20
+ @Injectable()
21
+ class TestService {
22
+ @OnEvent('test.event')
23
+ public handleEvent(payload: unknown): void {
24
+ // handler
25
+ }
26
+ }
27
+
28
+ expect(isEventListenerClass(TestService)).toBe(true);
29
+ });
30
+
31
+ test('should store event metadata on class', () => {
32
+ @Injectable()
33
+ class TestService {
34
+ @OnEvent('user.created')
35
+ public handleUserCreated(payload: unknown): void {
36
+ // handler
37
+ }
38
+ }
39
+
40
+ const metadata = getOnEventMetadata(TestService);
41
+ expect(metadata).toBeDefined();
42
+ expect(metadata?.length).toBe(1);
43
+ expect(metadata?.[0]?.event).toBe('user.created');
44
+ expect(metadata?.[0]?.methodName).toBe('handleUserCreated');
45
+ });
46
+
47
+ test('should support Symbol as event name', () => {
48
+ const USER_DELETED = Symbol('user.deleted');
49
+
50
+ @Injectable()
51
+ class TestService {
52
+ @OnEvent(USER_DELETED)
53
+ public handleUserDeleted(payload: unknown): void {
54
+ // handler
55
+ }
56
+ }
57
+
58
+ const metadata = getOnEventMetadata(TestService);
59
+ expect(metadata?.[0]?.event).toBe(USER_DELETED);
60
+ });
61
+
62
+ test('should support multiple event listeners in same class', () => {
63
+ @Injectable()
64
+ class TestService {
65
+ @OnEvent('event1')
66
+ public handleEvent1(payload: unknown): void {}
67
+
68
+ @OnEvent('event2')
69
+ public handleEvent2(payload: unknown): void {}
70
+
71
+ @OnEvent('event3')
72
+ public handleEvent3(payload: unknown): void {}
73
+ }
74
+
75
+ const metadata = getOnEventMetadata(TestService);
76
+ expect(metadata?.length).toBe(3);
77
+
78
+ const events = metadata?.map((m) => m.event);
79
+ expect(events).toContain('event1');
80
+ expect(events).toContain('event2');
81
+ expect(events).toContain('event3');
82
+ });
83
+
84
+ test('should use default options when not specified', () => {
85
+ @Injectable()
86
+ class TestService {
87
+ @OnEvent('test.event')
88
+ public handleEvent(payload: unknown): void {}
89
+ }
90
+
91
+ const metadata = getOnEventMetadata(TestService);
92
+ expect(metadata?.[0]?.async).toBe(false);
93
+ expect(metadata?.[0]?.priority).toBe(0);
94
+ });
95
+
96
+ test('should support async option', () => {
97
+ @Injectable()
98
+ class TestService {
99
+ @OnEvent('test.event', { async: true })
100
+ public handleEvent(payload: unknown): void {}
101
+ }
102
+
103
+ const metadata = getOnEventMetadata(TestService);
104
+ expect(metadata?.[0]?.async).toBe(true);
105
+ });
106
+
107
+ test('should support priority option', () => {
108
+ @Injectable()
109
+ class TestService {
110
+ @OnEvent('test.event', { priority: 10 })
111
+ public handleEvent(payload: unknown): void {}
112
+ }
113
+
114
+ const metadata = getOnEventMetadata(TestService);
115
+ expect(metadata?.[0]?.priority).toBe(10);
116
+ });
117
+
118
+ test('should support both async and priority options', () => {
119
+ @Injectable()
120
+ class TestService {
121
+ @OnEvent('test.event', { async: true, priority: 5 })
122
+ public handleEvent(payload: unknown): void {}
123
+ }
124
+
125
+ const metadata = getOnEventMetadata(TestService);
126
+ expect(metadata?.[0]?.async).toBe(true);
127
+ expect(metadata?.[0]?.priority).toBe(5);
128
+ });
129
+
130
+ test('should not mark class without @OnEvent as listener class', () => {
131
+ @Injectable()
132
+ class RegularService {
133
+ public doSomething(): void {}
134
+ }
135
+
136
+ expect(isEventListenerClass(RegularService)).toBe(false);
137
+ });
138
+
139
+ test('should return undefined for class without @OnEvent', () => {
140
+ @Injectable()
141
+ class RegularService {
142
+ public doSomething(): void {}
143
+ }
144
+
145
+ const metadata = getOnEventMetadata(RegularService);
146
+ expect(metadata).toBeUndefined();
147
+ });
148
+ });
149
+
150
+ describe('Event metadata isolation', () => {
151
+ test('should isolate metadata between different classes', () => {
152
+ @Injectable()
153
+ class ServiceA {
154
+ @OnEvent('event.a')
155
+ public handleA(payload: unknown): void {}
156
+ }
157
+
158
+ @Injectable()
159
+ class ServiceB {
160
+ @OnEvent('event.b')
161
+ public handleB(payload: unknown): void {}
162
+ }
163
+
164
+ const metadataA = getOnEventMetadata(ServiceA);
165
+ const metadataB = getOnEventMetadata(ServiceB);
166
+
167
+ expect(metadataA?.length).toBe(1);
168
+ expect(metadataA?.[0]?.event).toBe('event.a');
169
+
170
+ expect(metadataB?.length).toBe(1);
171
+ expect(metadataB?.[0]?.event).toBe('event.b');
172
+ });
173
+ });