@dangao/bun-server 1.8.0 → 1.8.2
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/docs/api.md +194 -81
- package/docs/extensions.md +53 -0
- package/docs/guide.md +243 -1
- package/docs/microservice-config-center.md +73 -74
- package/docs/microservice-nacos.md +89 -90
- package/docs/microservice-service-registry.md +85 -86
- package/docs/microservice.md +142 -137
- package/docs/request-lifecycle.md +45 -4
- package/docs/symbol-interface-pattern.md +106 -106
- package/docs/zh/api.md +458 -18
- package/docs/zh/extensions.md +53 -0
- package/docs/zh/guide.md +251 -4
- package/docs/zh/microservice-config-center.md +258 -0
- package/docs/zh/microservice-nacos.md +346 -0
- package/docs/zh/microservice-service-registry.md +306 -0
- package/docs/zh/microservice.md +680 -0
- package/docs/zh/request-lifecycle.md +43 -5
- package/package.json +1 -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/error/error-handler.test.ts +166 -57
- package/tests/error/i18n-extended.test.ts +105 -0
- package/tests/events/event-listener-scanner.test.ts +114 -0
- package/tests/events/event-module.test.ts +133 -302
- 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/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/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
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { describe, expect, test, beforeEach } from 'bun:test';
|
|
2
|
+
import 'reflect-metadata';
|
|
3
|
+
|
|
4
|
+
import { ParamBinder } from '../../src/controller/param-binder';
|
|
5
|
+
import {
|
|
6
|
+
Body,
|
|
7
|
+
Query,
|
|
8
|
+
Param,
|
|
9
|
+
Header,
|
|
10
|
+
Context as ContextDecorator,
|
|
11
|
+
QueryMap,
|
|
12
|
+
HeaderMap,
|
|
13
|
+
} from '../../src/controller/decorators';
|
|
14
|
+
import { Context } from '../../src/core/context';
|
|
15
|
+
import { Container } from '../../src/di/container';
|
|
16
|
+
|
|
17
|
+
describe('ParamBinder', () => {
|
|
18
|
+
let container: Container;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
container = new Container();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('bind', () => {
|
|
25
|
+
test('should bind @Body parameter', async () => {
|
|
26
|
+
class TestController {
|
|
27
|
+
public testMethod(@Body() body: unknown): void {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const request = new Request('http://localhost/test', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({ name: 'test' }),
|
|
34
|
+
});
|
|
35
|
+
const context = new Context(request, container);
|
|
36
|
+
|
|
37
|
+
const params = await ParamBinder.bind(
|
|
38
|
+
TestController.prototype,
|
|
39
|
+
'testMethod',
|
|
40
|
+
context,
|
|
41
|
+
container,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(params[0]).toEqual({ name: 'test' });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should bind @Body with key parameter', async () => {
|
|
48
|
+
class TestController {
|
|
49
|
+
public testMethod(@Body('name') name: string): void {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const request = new Request('http://localhost/test', {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ name: 'alice', age: 25 }),
|
|
56
|
+
});
|
|
57
|
+
const context = new Context(request, container);
|
|
58
|
+
|
|
59
|
+
const params = await ParamBinder.bind(
|
|
60
|
+
TestController.prototype,
|
|
61
|
+
'testMethod',
|
|
62
|
+
context,
|
|
63
|
+
container,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(params[0]).toBe('alice');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should bind @Query parameter', async () => {
|
|
70
|
+
class TestController {
|
|
71
|
+
public testMethod(@Query('id') id: string): void {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const request = new Request('http://localhost/test?id=123');
|
|
75
|
+
const context = new Context(request, container);
|
|
76
|
+
|
|
77
|
+
const params = await ParamBinder.bind(
|
|
78
|
+
TestController.prototype,
|
|
79
|
+
'testMethod',
|
|
80
|
+
context,
|
|
81
|
+
container,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(params[0]).toBe('123');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should bind @Param parameter', async () => {
|
|
88
|
+
class TestController {
|
|
89
|
+
public testMethod(@Param('id') id: string): void {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const request = new Request('http://localhost/users/456');
|
|
93
|
+
const context = new Context(request, container);
|
|
94
|
+
context.params = { id: '456' };
|
|
95
|
+
|
|
96
|
+
const params = await ParamBinder.bind(
|
|
97
|
+
TestController.prototype,
|
|
98
|
+
'testMethod',
|
|
99
|
+
context,
|
|
100
|
+
container,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(params[0]).toBe('456');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('should bind @Header parameter', async () => {
|
|
107
|
+
class TestController {
|
|
108
|
+
public testMethod(@Header('x-custom') custom: string): void {}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const request = new Request('http://localhost/test', {
|
|
112
|
+
headers: { 'X-Custom': 'custom-value' },
|
|
113
|
+
});
|
|
114
|
+
const context = new Context(request, container);
|
|
115
|
+
|
|
116
|
+
const params = await ParamBinder.bind(
|
|
117
|
+
TestController.prototype,
|
|
118
|
+
'testMethod',
|
|
119
|
+
context,
|
|
120
|
+
container,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
expect(params[0]).toBe('custom-value');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should bind @Context parameter', async () => {
|
|
127
|
+
class TestController {
|
|
128
|
+
public testMethod(@ContextDecorator() ctx: Context): void {}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const request = new Request('http://localhost/test');
|
|
132
|
+
const context = new Context(request, container);
|
|
133
|
+
|
|
134
|
+
const params = await ParamBinder.bind(
|
|
135
|
+
TestController.prototype,
|
|
136
|
+
'testMethod',
|
|
137
|
+
context,
|
|
138
|
+
container,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// 返回的是 context 或 AsyncLocalStorage 中的 context
|
|
142
|
+
expect(params[0]).toBeDefined();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('should bind multiple parameters in correct order', async () => {
|
|
146
|
+
class TestController {
|
|
147
|
+
public testMethod(
|
|
148
|
+
@Query('name') name: string,
|
|
149
|
+
@Query('age') age: string,
|
|
150
|
+
@Header('x-token') token: string,
|
|
151
|
+
): void {}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const request = new Request('http://localhost/test?name=alice&age=25', {
|
|
155
|
+
headers: { 'X-Token': 'abc123' },
|
|
156
|
+
});
|
|
157
|
+
const context = new Context(request, container);
|
|
158
|
+
|
|
159
|
+
const params = await ParamBinder.bind(
|
|
160
|
+
TestController.prototype,
|
|
161
|
+
'testMethod',
|
|
162
|
+
context,
|
|
163
|
+
container,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(params[0]).toBe('alice');
|
|
167
|
+
expect(params[1]).toBe('25');
|
|
168
|
+
expect(params[2]).toBe('abc123');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should return empty array for method without decorators', async () => {
|
|
172
|
+
class TestController {
|
|
173
|
+
public testMethod(arg: string): void {}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const request = new Request('http://localhost/test');
|
|
177
|
+
const context = new Context(request, container);
|
|
178
|
+
|
|
179
|
+
const params = await ParamBinder.bind(
|
|
180
|
+
TestController.prototype,
|
|
181
|
+
'testMethod',
|
|
182
|
+
context,
|
|
183
|
+
container,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(params).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('should handle @QueryMap parameter', async () => {
|
|
190
|
+
class TestController {
|
|
191
|
+
public testMethod(@QueryMap() query: Record<string, string>): void {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const request = new Request('http://localhost/test?foo=bar&baz=qux');
|
|
195
|
+
const context = new Context(request, container);
|
|
196
|
+
|
|
197
|
+
const params = await ParamBinder.bind(
|
|
198
|
+
TestController.prototype,
|
|
199
|
+
'testMethod',
|
|
200
|
+
context,
|
|
201
|
+
container,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(params[0]).toEqual({ foo: 'bar', baz: 'qux' });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('should handle @QueryMap with transform', async () => {
|
|
208
|
+
class TestController {
|
|
209
|
+
public testMethod(
|
|
210
|
+
@QueryMap({
|
|
211
|
+
transform: (map) => ({ ...map, transformed: true }),
|
|
212
|
+
})
|
|
213
|
+
query: Record<string, unknown>,
|
|
214
|
+
): void {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const request = new Request('http://localhost/test?foo=bar');
|
|
218
|
+
const context = new Context(request, container);
|
|
219
|
+
|
|
220
|
+
const params = await ParamBinder.bind(
|
|
221
|
+
TestController.prototype,
|
|
222
|
+
'testMethod',
|
|
223
|
+
context,
|
|
224
|
+
container,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect((params[0] as any).transformed).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('should handle @HeaderMap parameter', async () => {
|
|
231
|
+
class TestController {
|
|
232
|
+
public testMethod(@HeaderMap() headers: Record<string, string>): void {}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const request = new Request('http://localhost/test', {
|
|
236
|
+
headers: { 'X-Custom': 'value1', 'X-Another': 'value2' },
|
|
237
|
+
});
|
|
238
|
+
const context = new Context(request, container);
|
|
239
|
+
|
|
240
|
+
const params = await ParamBinder.bind(
|
|
241
|
+
TestController.prototype,
|
|
242
|
+
'testMethod',
|
|
243
|
+
context,
|
|
244
|
+
container,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect((params[0] as any)['x-custom']).toBe('value1');
|
|
248
|
+
expect((params[0] as any)['x-another']).toBe('value2');
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('should handle @HeaderMap with pick', async () => {
|
|
252
|
+
class TestController {
|
|
253
|
+
public testMethod(
|
|
254
|
+
@HeaderMap({ pick: ['x-custom'] })
|
|
255
|
+
headers: Record<string, string>,
|
|
256
|
+
): void {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const request = new Request('http://localhost/test', {
|
|
260
|
+
headers: { 'X-Custom': 'value1', 'X-Another': 'value2' },
|
|
261
|
+
});
|
|
262
|
+
const context = new Context(request, container);
|
|
263
|
+
|
|
264
|
+
const params = await ParamBinder.bind(
|
|
265
|
+
TestController.prototype,
|
|
266
|
+
'testMethod',
|
|
267
|
+
context,
|
|
268
|
+
container,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
expect((params[0] as any)['x-custom']).toBe('value1');
|
|
272
|
+
expect((params[0] as any)['x-another']).toBeUndefined();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('should handle missing body key', async () => {
|
|
276
|
+
class TestController {
|
|
277
|
+
public testMethod(@Body('missing') value: string): void {}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const request = new Request('http://localhost/test', {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
283
|
+
body: JSON.stringify({ name: 'test' }),
|
|
284
|
+
});
|
|
285
|
+
const context = new Context(request, container);
|
|
286
|
+
|
|
287
|
+
const params = await ParamBinder.bind(
|
|
288
|
+
TestController.prototype,
|
|
289
|
+
'testMethod',
|
|
290
|
+
context,
|
|
291
|
+
container,
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
expect(params[0]).toBeUndefined();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('should return null for missing query param', async () => {
|
|
298
|
+
class TestController {
|
|
299
|
+
public testMethod(@Query('missing') value: string): void {}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const request = new Request('http://localhost/test');
|
|
303
|
+
const context = new Context(request, container);
|
|
304
|
+
|
|
305
|
+
const params = await ParamBinder.bind(
|
|
306
|
+
TestController.prototype,
|
|
307
|
+
'testMethod',
|
|
308
|
+
context,
|
|
309
|
+
container,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
expect(params[0]).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('should return undefined for missing path param', async () => {
|
|
316
|
+
class TestController {
|
|
317
|
+
public testMethod(@Param('missing') value: string): void {}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const request = new Request('http://localhost/test');
|
|
321
|
+
const context = new Context(request, container);
|
|
322
|
+
|
|
323
|
+
const params = await ParamBinder.bind(
|
|
324
|
+
TestController.prototype,
|
|
325
|
+
'testMethod',
|
|
326
|
+
context,
|
|
327
|
+
container,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(params[0]).toBeUndefined();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -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
|
+
});
|