@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.
- package/README.md +129 -21
- package/dist/di/decorators.d.ts +37 -0
- package/dist/di/decorators.d.ts.map +1 -1
- package/dist/di/index.d.ts +1 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/module-registry.d.ts +17 -0
- package/dist/di/module-registry.d.ts.map +1 -1
- package/dist/events/decorators.d.ts +52 -0
- package/dist/events/decorators.d.ts.map +1 -0
- package/dist/events/event-module.d.ts +97 -0
- package/dist/events/event-module.d.ts.map +1 -0
- package/dist/events/index.d.ts +5 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/service.d.ts +76 -0
- package/dist/events/service.d.ts.map +1 -0
- package/dist/events/types.d.ts +184 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1511 -11
- package/dist/security/filter.d.ts +23 -0
- package/dist/security/filter.d.ts.map +1 -1
- package/dist/security/guards/builtin/auth-guard.d.ts +44 -0
- package/dist/security/guards/builtin/auth-guard.d.ts.map +1 -0
- package/dist/security/guards/builtin/index.d.ts +3 -0
- package/dist/security/guards/builtin/index.d.ts.map +1 -0
- package/dist/security/guards/builtin/roles-guard.d.ts +66 -0
- package/dist/security/guards/builtin/roles-guard.d.ts.map +1 -0
- package/dist/security/guards/decorators.d.ts +50 -0
- package/dist/security/guards/decorators.d.ts.map +1 -0
- package/dist/security/guards/execution-context.d.ts +56 -0
- package/dist/security/guards/execution-context.d.ts.map +1 -0
- package/dist/security/guards/guard-registry.d.ts +67 -0
- package/dist/security/guards/guard-registry.d.ts.map +1 -0
- package/dist/security/guards/index.d.ts +7 -0
- package/dist/security/guards/index.d.ts.map +1 -0
- package/dist/security/guards/reflector.d.ts +57 -0
- package/dist/security/guards/reflector.d.ts.map +1 -0
- package/dist/security/guards/types.d.ts +126 -0
- package/dist/security/guards/types.d.ts.map +1 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/security-module.d.ts +20 -0
- package/dist/security/security-module.d.ts.map +1 -1
- package/dist/validation/class-validator.d.ts +108 -0
- package/dist/validation/class-validator.d.ts.map +1 -0
- package/dist/validation/custom-validator.d.ts +130 -0
- package/dist/validation/custom-validator.d.ts.map +1 -0
- package/dist/validation/errors.d.ts +22 -2
- package/dist/validation/errors.d.ts.map +1 -1
- package/dist/validation/index.d.ts +7 -1
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/rules/array.d.ts +33 -0
- package/dist/validation/rules/array.d.ts.map +1 -0
- package/dist/validation/rules/common.d.ts +90 -0
- package/dist/validation/rules/common.d.ts.map +1 -0
- package/dist/validation/rules/conditional.d.ts +30 -0
- package/dist/validation/rules/conditional.d.ts.map +1 -0
- package/dist/validation/rules/index.d.ts +5 -0
- package/dist/validation/rules/index.d.ts.map +1 -0
- package/dist/validation/rules/object.d.ts +30 -0
- package/dist/validation/rules/object.d.ts.map +1 -0
- package/dist/validation/types.d.ts +52 -1
- package/dist/validation/types.d.ts.map +1 -1
- package/docs/events.md +494 -0
- package/docs/guards.md +376 -0
- package/docs/guide.md +309 -1
- package/docs/request-lifecycle.md +444 -0
- package/docs/validation.md +407 -0
- package/docs/zh/events.md +494 -0
- package/docs/zh/guards.md +376 -0
- package/docs/zh/guide.md +309 -1
- package/docs/zh/request-lifecycle.md +444 -0
- package/docs/zh/validation.md +407 -0
- package/package.json +1 -1
- package/src/di/decorators.ts +46 -0
- package/src/di/index.ts +10 -1
- package/src/di/module-registry.ts +39 -0
- package/src/events/decorators.ts +103 -0
- package/src/events/event-module.ts +272 -0
- package/src/events/index.ts +32 -0
- package/src/events/service.ts +352 -0
- package/src/events/types.ts +223 -0
- package/src/index.ts +133 -1
- package/src/security/filter.ts +88 -8
- package/src/security/guards/builtin/auth-guard.ts +68 -0
- package/src/security/guards/builtin/index.ts +3 -0
- package/src/security/guards/builtin/roles-guard.ts +165 -0
- package/src/security/guards/decorators.ts +124 -0
- package/src/security/guards/execution-context.ts +152 -0
- package/src/security/guards/guard-registry.ts +164 -0
- package/src/security/guards/index.ts +7 -0
- package/src/security/guards/reflector.ts +99 -0
- package/src/security/guards/types.ts +144 -0
- package/src/security/index.ts +1 -0
- package/src/security/security-module.ts +72 -2
- package/src/validation/class-validator.ts +322 -0
- package/src/validation/custom-validator.ts +289 -0
- package/src/validation/errors.ts +50 -2
- package/src/validation/index.ts +103 -1
- package/src/validation/rules/array.ts +118 -0
- package/src/validation/rules/common.ts +286 -0
- package/src/validation/rules/conditional.ts +52 -0
- package/src/validation/rules/index.ts +51 -0
- package/src/validation/rules/object.ts +86 -0
- package/src/validation/types.ts +61 -1
- package/tests/auth/auth-decorators.test.ts +241 -0
- package/tests/auth/oauth2-service.test.ts +318 -0
- package/tests/cache/cache-decorators-extended.test.ts +272 -0
- package/tests/cache/cache-interceptors.test.ts +534 -0
- package/tests/cache/cache-service-proxy.test.ts +246 -0
- package/tests/cache/memory-cache-store.test.ts +155 -0
- package/tests/cache/redis-cache-store.test.ts +199 -0
- package/tests/config/config-center-integration.test.ts +334 -0
- package/tests/config/config-module-extended.test.ts +165 -0
- package/tests/controller/param-binder.test.ts +333 -0
- package/tests/di/global-module.test.ts +487 -0
- package/tests/error/error-handler.test.ts +166 -57
- package/tests/error/i18n-extended.test.ts +105 -0
- package/tests/events/event-decorators.test.ts +173 -0
- package/tests/events/event-emitter.test.ts +373 -0
- package/tests/events/event-listener-scanner.test.ts +114 -0
- package/tests/events/event-module.test.ts +204 -0
- package/tests/extensions/logger-module.test.ts +158 -0
- package/tests/files/file-storage.test.ts +136 -0
- package/tests/interceptor/base-interceptor.test.ts +605 -0
- package/tests/interceptor/builtin/cache-interceptor.test.ts +233 -86
- package/tests/interceptor/builtin/log-interceptor.test.ts +469 -0
- package/tests/interceptor/builtin/permission-interceptor.test.ts +219 -120
- package/tests/interceptor/interceptor-chain.test.ts +241 -189
- package/tests/interceptor/interceptor-metadata.test.ts +221 -0
- package/tests/microservice/circuit-breaker.test.ts +221 -0
- package/tests/microservice/service-client-decorators.test.ts +86 -0
- package/tests/microservice/service-client-interceptors.test.ts +274 -0
- package/tests/microservice/service-registry-decorators.test.ts +147 -0
- package/tests/microservice/tracer.test.ts +213 -0
- package/tests/microservice/tracing-collectors.test.ts +168 -0
- package/tests/middleware/builtin/middleware-builtin-extended.test.ts +237 -0
- package/tests/middleware/builtin/rate-limit.test.ts +257 -0
- package/tests/middleware/middleware-decorators.test.ts +222 -0
- package/tests/middleware/middleware-pipeline.test.ts +160 -0
- package/tests/queue/queue-decorators.test.ts +139 -0
- package/tests/queue/queue-service.test.ts +191 -0
- package/tests/request/body-parser-extended.test.ts +291 -0
- package/tests/request/request-wrapper.test.ts +319 -0
- package/tests/router/router-decorators.test.ts +260 -0
- package/tests/router/router-extended.test.ts +298 -0
- package/tests/security/guards/guards-integration.test.ts +371 -0
- package/tests/security/guards/guards.test.ts +775 -0
- package/tests/security/guards/reflector.test.ts +188 -0
- package/tests/security/security-filter.test.ts +182 -0
- package/tests/security/security-module-extended.test.ts +133 -0
- package/tests/security/security-module.test.ts +2 -2
- package/tests/session/memory-session-store.test.ts +172 -0
- package/tests/session/session-decorators.test.ts +163 -0
- package/tests/swagger/ui.test.ts +212 -0
- package/tests/validation/class-validator.test.ts +349 -0
- package/tests/validation/custom-validator.test.ts +335 -0
- 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
|
|
5
|
+
import { HttpException, BadRequestException, UnauthorizedException, ForbiddenException, NotFoundException } from '../../src/error';
|
|
6
6
|
import { ValidationError } from '../../src/validation';
|
|
7
|
-
import {
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
+
});
|