@dangao/bun-server 1.7.1 → 1.8.1

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 (159) 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/auth/auth-decorators.test.ts +241 -0
  108. package/tests/auth/oauth2-service.test.ts +318 -0
  109. package/tests/cache/cache-decorators-extended.test.ts +272 -0
  110. package/tests/cache/cache-interceptors.test.ts +534 -0
  111. package/tests/cache/cache-service-proxy.test.ts +246 -0
  112. package/tests/cache/memory-cache-store.test.ts +155 -0
  113. package/tests/cache/redis-cache-store.test.ts +199 -0
  114. package/tests/config/config-center-integration.test.ts +334 -0
  115. package/tests/config/config-module-extended.test.ts +165 -0
  116. package/tests/controller/param-binder.test.ts +333 -0
  117. package/tests/di/global-module.test.ts +487 -0
  118. package/tests/error/error-handler.test.ts +166 -57
  119. package/tests/error/i18n-extended.test.ts +105 -0
  120. package/tests/events/event-decorators.test.ts +173 -0
  121. package/tests/events/event-emitter.test.ts +373 -0
  122. package/tests/events/event-listener-scanner.test.ts +114 -0
  123. package/tests/events/event-module.test.ts +204 -0
  124. package/tests/extensions/logger-module.test.ts +158 -0
  125. package/tests/files/file-storage.test.ts +136 -0
  126. package/tests/interceptor/base-interceptor.test.ts +605 -0
  127. package/tests/interceptor/builtin/cache-interceptor.test.ts +233 -86
  128. package/tests/interceptor/builtin/log-interceptor.test.ts +469 -0
  129. package/tests/interceptor/builtin/permission-interceptor.test.ts +219 -120
  130. package/tests/interceptor/interceptor-chain.test.ts +241 -189
  131. package/tests/interceptor/interceptor-metadata.test.ts +221 -0
  132. package/tests/microservice/circuit-breaker.test.ts +221 -0
  133. package/tests/microservice/service-client-decorators.test.ts +86 -0
  134. package/tests/microservice/service-client-interceptors.test.ts +274 -0
  135. package/tests/microservice/service-registry-decorators.test.ts +147 -0
  136. package/tests/microservice/tracer.test.ts +213 -0
  137. package/tests/microservice/tracing-collectors.test.ts +168 -0
  138. package/tests/middleware/builtin/middleware-builtin-extended.test.ts +237 -0
  139. package/tests/middleware/builtin/rate-limit.test.ts +257 -0
  140. package/tests/middleware/middleware-decorators.test.ts +222 -0
  141. package/tests/middleware/middleware-pipeline.test.ts +160 -0
  142. package/tests/queue/queue-decorators.test.ts +139 -0
  143. package/tests/queue/queue-service.test.ts +191 -0
  144. package/tests/request/body-parser-extended.test.ts +291 -0
  145. package/tests/request/request-wrapper.test.ts +319 -0
  146. package/tests/router/router-decorators.test.ts +260 -0
  147. package/tests/router/router-extended.test.ts +298 -0
  148. package/tests/security/guards/guards-integration.test.ts +371 -0
  149. package/tests/security/guards/guards.test.ts +775 -0
  150. package/tests/security/guards/reflector.test.ts +188 -0
  151. package/tests/security/security-filter.test.ts +182 -0
  152. package/tests/security/security-module-extended.test.ts +133 -0
  153. package/tests/security/security-module.test.ts +2 -2
  154. package/tests/session/memory-session-store.test.ts +172 -0
  155. package/tests/session/session-decorators.test.ts +163 -0
  156. package/tests/swagger/ui.test.ts +212 -0
  157. package/tests/validation/class-validator.test.ts +349 -0
  158. package/tests/validation/custom-validator.test.ts +335 -0
  159. package/tests/validation/rules.test.ts +543 -0
@@ -1,68 +1,177 @@
1
- import { describe, expect, test } from 'bun:test';
1
+ import { describe, expect, test, beforeEach } from 'bun:test';
2
+ import 'reflect-metadata';
2
3
 
3
- import { Context } from '../../src/core/context';
4
4
  import { handleError } from '../../src/error/handler';
5
- import { HttpException, BadRequestException } from '../../src/error/http-exception';
5
+ import { HttpException, BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException } from '../../src/error';
6
6
  import { ValidationError } from '../../src/validation';
7
- import { ExceptionFilterRegistry, type ExceptionFilter } from '../../src/error/filter';
8
-
9
- function createContext(url: string = 'http://localhost/api/error'): Context {
10
- return new Context(new Request(url));
11
- }
12
-
13
- describe('Error Handler', () => {
14
- test('should handle HttpException', async () => {
15
- const ctx = createContext();
16
- const error = new BadRequestException('Invalid payload');
17
- const response = await handleError(error, ctx);
18
- expect(response.status).toBe(400);
19
- const data = await response.json();
20
- expect(data.error).toBe('Invalid payload');
21
- });
7
+ import { Context } from '../../src/core/context';
8
+ import { Container } from '../../src/di/container';
22
9
 
23
- test('should handle ValidationError', async () => {
24
- const ctx = createContext();
25
- const validationError = new ValidationError('Validation failed', [
26
- { index: 0, rule: 'isString', message: 'Must be string' },
27
- ]);
28
- const response = await handleError(validationError, ctx);
29
- expect(response.status).toBe(400);
30
- const data = await response.json();
31
- expect(data.issues.length).toBe(1);
10
+ describe('handleError', () => {
11
+ let container: Container;
12
+
13
+ beforeEach(() => {
14
+ container = new Container();
32
15
  });
33
16
 
34
- test('should allow custom exception filter', async () => {
35
- const registry = ExceptionFilterRegistry.getInstance();
36
- registry.clear();
37
-
38
- const filter: ExceptionFilter = {
39
- catch(error, context) {
40
- if (error instanceof Error && error.message === 'custom') {
41
- context.setStatus(418);
42
- return context.createResponse({ error: 'filtered' });
43
- }
44
- return undefined;
45
- },
46
- };
47
-
48
- registry.register(filter);
49
-
50
- const ctx = createContext();
51
- const response = await handleError(new Error('custom'), ctx);
52
- expect(response.status).toBe(418);
53
- const data = await response.json();
54
- expect(data.error).toBe('filtered');
55
-
56
- registry.clear();
17
+ function createContext(): Context {
18
+ const request = new Request('http://localhost/test');
19
+ return new Context(request, container);
20
+ }
21
+
22
+ describe('HttpException handling', () => {
23
+ test('should handle basic HttpException', async () => {
24
+ const context = createContext();
25
+ const error = new HttpException(400, 'Bad request');
26
+
27
+ const response = await handleError(error, context);
28
+ const body = await response.json() as { error: string };
29
+
30
+ expect(response.status).toBe(400);
31
+ expect(body.error).toBe('Bad request');
32
+ });
33
+
34
+ test('should handle BadRequestException', async () => {
35
+ const context = createContext();
36
+ const error = new BadRequestException('Invalid input');
37
+
38
+ const response = await handleError(error, context);
39
+ const body = await response.json() as { error: string };
40
+
41
+ expect(response.status).toBe(400);
42
+ expect(body.error).toBe('Invalid input');
43
+ });
44
+
45
+ test('should handle UnauthorizedException', async () => {
46
+ const context = createContext();
47
+ const error = new UnauthorizedException('Not authenticated');
48
+
49
+ const response = await handleError(error, context);
50
+ const body = await response.json() as { error: string };
51
+
52
+ expect(response.status).toBe(401);
53
+ expect(body.error).toBe('Not authenticated');
54
+ });
55
+
56
+ test('should handle ForbiddenException', async () => {
57
+ const context = createContext();
58
+ const error = new ForbiddenException('Access denied');
59
+
60
+ const response = await handleError(error, context);
61
+ const body = await response.json() as { error: string };
62
+
63
+ expect(response.status).toBe(403);
64
+ expect(body.error).toBe('Access denied');
65
+ });
66
+
67
+ test('should handle NotFoundException', async () => {
68
+ const context = createContext();
69
+ const error = new NotFoundException('Resource not found');
70
+
71
+ const response = await handleError(error, context);
72
+ const body = await response.json() as { error: string };
73
+
74
+ expect(response.status).toBe(404);
75
+ expect(body.error).toBe('Resource not found');
76
+ });
77
+
78
+ test('should include error code in response when present', async () => {
79
+ const context = createContext();
80
+ // HttpException 构造函数: (status, message, details?, code?, messageParams?)
81
+ const error = new HttpException(400, 'Validation failed', undefined, 'E001');
82
+
83
+ const response = await handleError(error, context);
84
+ const body = await response.json() as { error: string; code: string };
85
+
86
+ expect(body.code).toBe('E001');
87
+ });
88
+
89
+ test('should include details in response when present', async () => {
90
+ const context = createContext();
91
+ const error = new HttpException(400, 'Validation failed');
92
+ (error as any).details = { field: 'email', issue: 'invalid format' };
93
+
94
+ const response = await handleError(error, context);
95
+ const body = await response.json() as { error: string; details: unknown };
96
+
97
+ expect(body.details).toEqual({ field: 'email', issue: 'invalid format' });
98
+ });
57
99
  });
58
100
 
59
- test('should fallback to 500 for unknown errors', async () => {
60
- const ctx = createContext();
61
- const response = await handleError(new Error('unknown'), ctx);
62
- expect(response.status).toBe(500);
63
- const data = await response.json();
64
- expect(data.error).toBe('Internal Server Error');
101
+ describe('ValidationError handling', () => {
102
+ test('should handle ValidationError', async () => {
103
+ const context = createContext();
104
+ const error = new ValidationError('Validation failed', [
105
+ { path: 'email', message: 'Invalid email format' },
106
+ ]);
107
+
108
+ const response = await handleError(error, context);
109
+ const body = await response.json() as { error: string; code: string; issues: unknown[] };
110
+
111
+ expect(response.status).toBe(400);
112
+ expect(body.error).toBe('Validation failed');
113
+ expect(body.code).toBe('VALIDATION_FAILED');
114
+ expect(body.issues).toHaveLength(1);
115
+ });
116
+
117
+ test('should include all validation issues', async () => {
118
+ const context = createContext();
119
+ const error = new ValidationError('Multiple errors', [
120
+ { path: 'email', message: 'Invalid email' },
121
+ { path: 'name', message: 'Name is required' },
122
+ { path: 'age', message: 'Age must be positive' },
123
+ ]);
124
+
125
+ const response = await handleError(error, context);
126
+ const body = await response.json() as { issues: unknown[] };
127
+
128
+ expect(body.issues).toHaveLength(3);
129
+ });
65
130
  });
66
- });
67
131
 
132
+ describe('Unknown error handling', () => {
133
+ test('should handle Error instance', async () => {
134
+ const context = createContext();
135
+ const error = new Error('Something went wrong');
136
+
137
+ const response = await handleError(error, context);
138
+ const body = await response.json() as { error: string; details?: string };
139
+
140
+ expect(response.status).toBe(500);
141
+ expect(body.error).toBe('Internal Server Error');
142
+ });
68
143
 
144
+ test('should handle string error', async () => {
145
+ const context = createContext();
146
+ const error = 'String error message';
147
+
148
+ const response = await handleError(error, context);
149
+ const body = await response.json() as { error: string };
150
+
151
+ expect(response.status).toBe(500);
152
+ expect(body.error).toBe('Internal Server Error');
153
+ });
154
+
155
+ test('should handle null error', async () => {
156
+ const context = createContext();
157
+ const error = null;
158
+
159
+ const response = await handleError(error, context);
160
+ const body = await response.json() as { error: string };
161
+
162
+ expect(response.status).toBe(500);
163
+ expect(body.error).toBe('Internal Server Error');
164
+ });
165
+
166
+ test('should handle undefined error', async () => {
167
+ const context = createContext();
168
+ const error = undefined;
169
+
170
+ const response = await handleError(error, context);
171
+ const body = await response.json() as { error: string };
172
+
173
+ expect(response.status).toBe(500);
174
+ expect(body.error).toBe('Internal Server Error');
175
+ });
176
+ });
177
+ });
@@ -0,0 +1,105 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { ErrorMessageI18n, type SupportedLanguage } from '../../src/error/i18n';
4
+ import { ErrorCode } from '../../src/error/error-codes';
5
+
6
+ describe('Error I18n', () => {
7
+ describe('ErrorMessageI18n.getMessage', () => {
8
+ test('should return English message by default', () => {
9
+ const message = ErrorMessageI18n.getMessage(ErrorCode.INTERNAL_ERROR);
10
+ expect(message.toLowerCase()).toContain('internal');
11
+ });
12
+
13
+ test('should return Chinese message when specified', () => {
14
+ const message = ErrorMessageI18n.getMessage(ErrorCode.INTERNAL_ERROR, 'zh-CN');
15
+ expect(message).toBe('服务器内部错误');
16
+ });
17
+
18
+ test('should return Japanese message when available', () => {
19
+ const message = ErrorMessageI18n.getMessage(ErrorCode.INTERNAL_ERROR, 'ja');
20
+ expect(message).toBe('サーバー内部エラー');
21
+ });
22
+
23
+ test('should return Korean message when available', () => {
24
+ const message = ErrorMessageI18n.getMessage(ErrorCode.INTERNAL_ERROR, 'ko');
25
+ expect(message).toBe('서버 내부 오류');
26
+ });
27
+
28
+ test('should fallback to English when translation not available', () => {
29
+ // Use an error code that might not have translations in all languages
30
+ const message = ErrorMessageI18n.getMessage(ErrorCode.DATABASE_POOL_EXHAUSTED, 'ja');
31
+ // Should fallback to English
32
+ expect(message).toBeDefined();
33
+ });
34
+
35
+ test('should return message for auth errors', () => {
36
+ const messages = {
37
+ en: ErrorMessageI18n.getMessage(ErrorCode.AUTH_REQUIRED, 'en'),
38
+ zhCN: ErrorMessageI18n.getMessage(ErrorCode.AUTH_REQUIRED, 'zh-CN'),
39
+ };
40
+
41
+ expect(messages.en.toLowerCase()).toContain('authentication');
42
+ expect(messages.zhCN).toBe('需要认证');
43
+ });
44
+
45
+ test('should return message for validation errors', () => {
46
+ const message = ErrorMessageI18n.getMessage(ErrorCode.VALIDATION_FAILED, 'zh-CN');
47
+ expect(message).toBe('验证失败');
48
+ });
49
+
50
+ test('should support message parameters', () => {
51
+ // getMessage supports params as third argument
52
+ const message = ErrorMessageI18n.getMessage(ErrorCode.INTERNAL_ERROR, 'en', { detail: 'test' });
53
+ expect(message).toBeDefined();
54
+ });
55
+ });
56
+
57
+ describe('default language', () => {
58
+ test('should support multiple languages', () => {
59
+ // Test that different languages return different messages
60
+ const enMessage = ErrorMessageI18n.getMessage(ErrorCode.INTERNAL_ERROR, 'en');
61
+ const zhMessage = ErrorMessageI18n.getMessage(ErrorCode.INTERNAL_ERROR, 'zh-CN');
62
+
63
+ // English and Chinese messages should be different
64
+ expect(enMessage).not.toBe(zhMessage);
65
+ expect(zhMessage).toBe('服务器内部错误');
66
+ });
67
+ });
68
+
69
+ describe('ErrorMessageI18n.parseLanguageFromHeader', () => {
70
+ test('should parse zh-CN from header', () => {
71
+ const lang = ErrorMessageI18n.parseLanguageFromHeader('zh-CN,zh;q=0.9,en;q=0.8');
72
+ expect(lang).toBe('zh-CN');
73
+ });
74
+
75
+ test('should parse Japanese', () => {
76
+ const lang = ErrorMessageI18n.parseLanguageFromHeader('ja,en;q=0.8');
77
+ expect(lang).toBe('ja');
78
+ });
79
+
80
+ test('should parse Korean', () => {
81
+ const lang = ErrorMessageI18n.parseLanguageFromHeader('ko-KR,ko;q=0.9');
82
+ expect(lang).toBe('ko');
83
+ });
84
+
85
+ test('should return English by default', () => {
86
+ const lang = ErrorMessageI18n.parseLanguageFromHeader('fr,de;q=0.8');
87
+ expect(lang).toBe('en');
88
+ });
89
+
90
+ test('should return English when header is null', () => {
91
+ const lang = ErrorMessageI18n.parseLanguageFromHeader(null);
92
+ expect(lang).toBe('en');
93
+ });
94
+
95
+ test('should return English when header is undefined', () => {
96
+ const lang = ErrorMessageI18n.parseLanguageFromHeader(undefined);
97
+ expect(lang).toBe('en');
98
+ });
99
+
100
+ test('should handle zh without region code', () => {
101
+ const lang = ErrorMessageI18n.parseLanguageFromHeader('zh');
102
+ expect(lang).toBe('zh-CN');
103
+ });
104
+ });
105
+ });
@@ -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
+ });