@dangao/bun-server 2.0.2 → 2.0.8

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 (57) hide show
  1. package/README.md +21 -8
  2. package/dist/config/config-module.d.ts +3 -0
  3. package/dist/config/config-module.d.ts.map +1 -1
  4. package/dist/core/context.d.ts +10 -0
  5. package/dist/core/context.d.ts.map +1 -1
  6. package/dist/database/service.d.ts +4 -4
  7. package/dist/database/service.d.ts.map +1 -1
  8. package/dist/index.js +236 -94
  9. package/dist/queue/queue-module.d.ts.map +1 -1
  10. package/dist/validation/decorators.d.ts.map +1 -1
  11. package/package.json +5 -4
  12. package/src/ai/providers/anthropic-provider.ts +1 -1
  13. package/src/ai/providers/google-provider.ts +1 -1
  14. package/src/ai/providers/ollama-provider.ts +1 -1
  15. package/src/ai/providers/openai-provider.ts +2 -2
  16. package/src/auth/jwt.ts +1 -1
  17. package/src/cache/interceptors.ts +3 -3
  18. package/src/cache/types.ts +10 -10
  19. package/src/client/runtime.ts +1 -1
  20. package/src/config/config-module.ts +46 -14
  21. package/src/config/service.ts +2 -2
  22. package/src/controller/param-binder.ts +1 -1
  23. package/src/conversation/service.ts +1 -1
  24. package/src/core/application.ts +1 -1
  25. package/src/core/cluster.ts +4 -4
  26. package/src/core/context.ts +71 -0
  27. package/src/dashboard/controller.ts +2 -2
  28. package/src/database/connection-manager.ts +4 -4
  29. package/src/database/service.ts +25 -28
  30. package/src/debug/middleware.ts +2 -2
  31. package/src/di/module-registry.ts +1 -1
  32. package/src/error/handler.ts +3 -3
  33. package/src/events/event-module.ts +4 -4
  34. package/src/files/static-middleware.ts +2 -2
  35. package/src/files/storage.ts +1 -1
  36. package/src/interceptor/builtin/log-interceptor.ts +1 -1
  37. package/src/mcp/server.ts +1 -1
  38. package/src/middleware/builtin/error-handler.ts +2 -2
  39. package/src/middleware/builtin/file-upload.ts +1 -1
  40. package/src/middleware/builtin/rate-limit.ts +1 -1
  41. package/src/middleware/builtin/static-file.ts +2 -2
  42. package/src/prompt/stores/file-store.ts +4 -4
  43. package/src/queue/queue-module.ts +4 -1
  44. package/src/request/body-parser.ts +3 -3
  45. package/src/security/filter.ts +1 -1
  46. package/src/security/guards/guard-registry.ts +1 -1
  47. package/src/session/middleware.ts +1 -1
  48. package/src/session/types.ts +5 -5
  49. package/src/testing/test-client.ts +1 -1
  50. package/src/validation/decorators.ts +70 -2
  51. package/src/validation/rules/common.ts +2 -2
  52. package/tests/config/config-module-extended.test.ts +24 -0
  53. package/tests/core/context.test.ts +52 -0
  54. package/tests/database/database-module.test.ts +87 -0
  55. package/tests/error/error-handler.test.ts +24 -0
  56. package/tests/queue/queue-module.test.ts +27 -0
  57. package/tests/validation/validation.test.ts +18 -0
@@ -217,7 +217,7 @@ export class EventModule {
217
217
  if (eventModuleRef) {
218
218
  try {
219
219
  eventEmitter = eventModuleRef.container.resolve<EventEmitter>(EVENT_EMITTER_TOKEN);
220
- } catch {
220
+ } catch (_error) {
221
221
  // 忽略错误
222
222
  }
223
223
  }
@@ -264,7 +264,7 @@ export class EventModule {
264
264
  if (moduleRef) {
265
265
  try {
266
266
  return moduleRef.container.resolve<EventEmitter>(EVENT_EMITTER_TOKEN);
267
- } catch {
267
+ } catch (_error) {
268
268
  // 忽略错误,尝试从传入的容器获取
269
269
  }
270
270
  }
@@ -273,7 +273,7 @@ export class EventModule {
273
273
  if (container) {
274
274
  try {
275
275
  return container.resolve<EventEmitter>(EVENT_EMITTER_TOKEN);
276
- } catch {
276
+ } catch (_error) {
277
277
  // 忽略错误
278
278
  }
279
279
  }
@@ -307,7 +307,7 @@ export class EventModule {
307
307
  let eventEmitter: EventEmitter | undefined;
308
308
  try {
309
309
  eventEmitter = eventModuleRef.container.resolve<EventEmitter>(EVENT_EMITTER_TOKEN);
310
- } catch {
310
+ } catch (_error) {
311
311
  return false;
312
312
  }
313
313
 
@@ -41,11 +41,11 @@ export function createStaticFileMiddleware(options: StaticFileOptions): Middlewa
41
41
  'Content-Type': file.type || 'application/octet-stream',
42
42
  },
43
43
  });
44
- } catch {
44
+ } catch (_error) {
45
45
  if (fallthrough) {
46
46
  return await next();
47
47
  }
48
- return context.createResponse({ error: 'File Not Found' }, { status: 404 });
48
+ return context.createErrorResponse({ error: 'File Not Found' }, { status: 404 });
49
49
  }
50
50
  };
51
51
  }
@@ -18,7 +18,7 @@ async function fileExists(path: string): Promise<boolean> {
18
18
  try {
19
19
  await access(path, constants.F_OK);
20
20
  return true;
21
- } catch {
21
+ } catch (_error) {
22
22
  return false;
23
23
  }
24
24
  }
@@ -101,7 +101,7 @@ export class LogInterceptor extends BaseInterceptor {
101
101
  let logger: Logger | undefined;
102
102
  try {
103
103
  logger = container.resolve<Logger>(LOGGER_TOKEN);
104
- } catch {
104
+ } catch (_error) {
105
105
  // Logger not available, use console
106
106
  }
107
107
 
package/src/mcp/server.ts CHANGED
@@ -69,7 +69,7 @@ export class McpServer {
69
69
  const pingInterval = setInterval(() => {
70
70
  try {
71
71
  controller.enqueue(encoder.encode(': ping\n\n'));
72
- } catch {
72
+ } catch (_error) {
73
73
  clearInterval(pingInterval);
74
74
  }
75
75
  }, 15000);
@@ -63,7 +63,7 @@ export function createErrorHandlingMiddleware(
63
63
  }
64
64
 
65
65
  if (error instanceof ValidationError) {
66
- return context.createResponse(
66
+ return context.createErrorResponse(
67
67
  {
68
68
  error: error.message,
69
69
  issues: error.issues,
@@ -84,7 +84,7 @@ export function createErrorHandlingMiddleware(
84
84
  }
85
85
 
86
86
  if (error instanceof Error) {
87
- return context.createResponse(
87
+ return context.createErrorResponse(
88
88
  {
89
89
  error: error.message,
90
90
  },
@@ -25,7 +25,7 @@ export function createFileUploadMiddleware(options: FileUploadOptions = {}): Mid
25
25
  for (const file of fileList) {
26
26
  if (file.size > maxSize) {
27
27
  context.setStatus(413);
28
- return context.createResponse({ error: `File ${file.name} exceeds max size` });
28
+ return context.createErrorResponse({ error: `File ${file.name} exceeds max size` });
29
29
  }
30
30
  }
31
31
  }
@@ -193,7 +193,7 @@ export function createRateLimitMiddleware(options: RateLimitOptions): Middleware
193
193
  // 检查是否超过限制
194
194
  if (currentCount > max) {
195
195
  context.setStatus(statusCode);
196
- return context.createResponse({
196
+ return context.createErrorResponse({
197
197
  error: message,
198
198
  retryAfter: Math.ceil(windowMs / 1000),
199
199
  });
@@ -52,7 +52,7 @@ export function createStaticFileMiddleware(options: StaticFileOptions): Middlewa
52
52
  const segments = cleanPath.split('/').filter((segment) => segment.length > 0);
53
53
  if (segments.some((segment) => segment === '..')) {
54
54
  context.setStatus(403);
55
- return context.createResponse({ error: 'Forbidden' });
55
+ return context.createErrorResponse({ error: 'Forbidden' });
56
56
  }
57
57
 
58
58
  let targetPath = resolve(root, cleanPath);
@@ -62,7 +62,7 @@ export function createStaticFileMiddleware(options: StaticFileOptions): Middlewa
62
62
 
63
63
  if (!isSubPath(root, targetPath)) {
64
64
  context.setStatus(403);
65
- return context.createResponse({ error: 'Forbidden' });
65
+ return context.createErrorResponse({ error: 'Forbidden' });
66
66
  }
67
67
 
68
68
  const file = Bun.file(targetPath);
@@ -74,7 +74,7 @@ export class FilePromptStore implements PromptStore {
74
74
  try {
75
75
  const path = `${this.promptsDir}/${id}.json`;
76
76
  await Bun.file(path).exists() && Bun.write(path, ''); // Soft delete (empty file)
77
- } catch {
77
+ } catch (_error) {
78
78
  // ignore
79
79
  }
80
80
  }
@@ -105,11 +105,11 @@ export class FilePromptStore implements PromptStore {
105
105
  await this.memory.create({ id, ...data }).catch(() => {
106
106
  // Template already exists — skip
107
107
  });
108
- } catch {
108
+ } catch (_error) {
109
109
  // Skip malformed files
110
110
  }
111
111
  }
112
- } catch {
112
+ } catch (_error) {
113
113
  // Directory doesn't exist — start empty
114
114
  }
115
115
  }
@@ -128,7 +128,7 @@ export class FilePromptStore implements PromptStore {
128
128
  2,
129
129
  );
130
130
  await Bun.write(`${this.promptsDir}/${template.id}.json`, content);
131
- } catch {
131
+ } catch (_error) {
132
132
  // Ignore write errors (e.g., read-only filesystem)
133
133
  }
134
134
  }
@@ -37,11 +37,14 @@ export class QueueModule {
37
37
  provide: QUEUE_SERVICE_TOKEN,
38
38
  useValue: service,
39
39
  },
40
+ {
41
+ provide: QueueService,
42
+ useValue: service,
43
+ },
40
44
  {
41
45
  provide: QUEUE_OPTIONS_TOKEN,
42
46
  useValue: options,
43
47
  },
44
- QueueService,
45
48
  );
46
49
 
47
50
  // 动态更新模块元数据
@@ -57,10 +57,10 @@ export class BodyParser {
57
57
  // 尝试解析为 JSON
58
58
  try {
59
59
  return JSON.parse(text);
60
- } catch {
60
+ } catch (_error) {
61
61
  return text;
62
62
  }
63
- } catch {
63
+ } catch (_error) {
64
64
  return undefined;
65
65
  }
66
66
  }
@@ -72,7 +72,7 @@ export class BodyParser {
72
72
  }
73
73
  try {
74
74
  return JSON.parse(fallbackText);
75
- } catch {
75
+ } catch (_error) {
76
76
  return fallbackText;
77
77
  }
78
78
  } catch (error) {
@@ -71,7 +71,7 @@ export function createSecurityFilter(config: SecurityFilterConfig): Middleware {
71
71
  const { ControllerRegistry } = require('../controller/controller');
72
72
  cachedContainer = ControllerRegistry.getInstance().getContainer();
73
73
  return cachedContainer;
74
- } catch {
74
+ } catch (_error) {
75
75
  return null;
76
76
  }
77
77
  };
@@ -73,7 +73,7 @@ export class GuardRegistry {
73
73
  this.guardInstances.set(guard as Constructor<CanActivate>, instance);
74
74
  return instance;
75
75
  }
76
- } catch {
76
+ } catch (_error) {
77
77
  // 如果容器解析失败,继续尝试手动实例化
78
78
  }
79
79
 
@@ -17,7 +17,7 @@ export function createSessionMiddleware(
17
17
  sessionService = container.resolve<SessionService>(
18
18
  SESSION_SERVICE_TOKEN,
19
19
  );
20
- } catch {
20
+ } catch (_error) {
21
21
  // 如果 SessionService 未注册,跳过 Session 处理
22
22
  return await next();
23
23
  }
@@ -231,7 +231,7 @@ export class RedisSessionStore implements SessionStore {
231
231
  return undefined;
232
232
  }
233
233
  return JSON.parse(value) as Session;
234
- } catch {
234
+ } catch (_error) {
235
235
  return undefined;
236
236
  }
237
237
  }
@@ -243,7 +243,7 @@ export class RedisSessionStore implements SessionStore {
243
243
  PX: maxAge,
244
244
  });
245
245
  return true;
246
- } catch {
246
+ } catch (_error) {
247
247
  return false;
248
248
  }
249
249
  }
@@ -252,7 +252,7 @@ export class RedisSessionStore implements SessionStore {
252
252
  try {
253
253
  await this.client.del(this.getKey(sessionId));
254
254
  return true;
255
- } catch {
255
+ } catch (_error) {
256
256
  return false;
257
257
  }
258
258
  }
@@ -261,7 +261,7 @@ export class RedisSessionStore implements SessionStore {
261
261
  try {
262
262
  const result = await this.client.exists(this.getKey(sessionId));
263
263
  return result === 1;
264
- } catch {
264
+ } catch (_error) {
265
265
  return false;
266
266
  }
267
267
  }
@@ -282,7 +282,7 @@ export class RedisSessionStore implements SessionStore {
282
282
  }
283
283
 
284
284
  return false;
285
- } catch {
285
+ } catch (_error) {
286
286
  return false;
287
287
  }
288
288
  }
@@ -97,7 +97,7 @@ export class TestHttpClient {
97
97
  let body: unknown;
98
98
  try {
99
99
  body = JSON.parse(text);
100
- } catch {
100
+ } catch (_error) {
101
101
  body = text;
102
102
  }
103
103
 
@@ -58,11 +58,79 @@ export function IsNumber(options: RuleOption = {}): ValidationRuleDefinition {
58
58
  }
59
59
 
60
60
  export function IsEmail(options: RuleOption = {}): ValidationRuleDefinition {
61
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
62
61
  return {
63
62
  name: 'isEmail',
64
63
  message: options.message ?? '必须是合法的邮箱地址',
65
- validate: (value) => typeof value === 'string' && emailRegex.test(value),
64
+ validate: (value) => {
65
+ if (typeof value !== 'string') {
66
+ return false;
67
+ }
68
+
69
+ const email = value.trim();
70
+ if (!email || email.length > 254) {
71
+ return false;
72
+ }
73
+ if (email.includes(' ')) {
74
+ return false;
75
+ }
76
+
77
+ const atIndex = email.indexOf('@');
78
+ if (atIndex <= 0 || atIndex !== email.lastIndexOf('@')) {
79
+ return false;
80
+ }
81
+
82
+ const local = email.slice(0, atIndex);
83
+ const domain = email.slice(atIndex + 1);
84
+ if (!local || !domain || local.length > 64) {
85
+ return false;
86
+ }
87
+ if (local.startsWith('.') || local.endsWith('.') || local.includes('..')) {
88
+ return false;
89
+ }
90
+ if (domain.startsWith('.') || domain.endsWith('.') || domain.includes('..')) {
91
+ return false;
92
+ }
93
+ if (!domain.includes('.')) {
94
+ return false;
95
+ }
96
+
97
+ for (const ch of local) {
98
+ const code = ch.charCodeAt(0);
99
+ const isAlphaNum =
100
+ (code >= 48 && code <= 57) ||
101
+ (code >= 65 && code <= 90) ||
102
+ (code >= 97 && code <= 122);
103
+ const isAllowedSymbol = "!#$%&'*+/=?^_`{|}~.-".includes(ch);
104
+ if (!isAlphaNum && !isAllowedSymbol) {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ const labels = domain.split('.');
110
+ if (labels.length < 2) {
111
+ return false;
112
+ }
113
+ for (const label of labels) {
114
+ if (!label || label.length > 63) {
115
+ return false;
116
+ }
117
+ if (label.startsWith('-') || label.endsWith('-')) {
118
+ return false;
119
+ }
120
+ for (const ch of label) {
121
+ const code = ch.charCodeAt(0);
122
+ const isAlphaNum =
123
+ (code >= 48 && code <= 57) ||
124
+ (code >= 65 && code <= 90) ||
125
+ (code >= 97 && code <= 122);
126
+ if (!isAlphaNum && ch !== '-') {
127
+ return false;
128
+ }
129
+ }
130
+ }
131
+
132
+ return labels[labels.length - 1]!.length >= 2;
133
+ },
66
134
  };
67
135
  }
68
136
 
@@ -191,7 +191,7 @@ export function IsUrl(options: RuleOptions = {}): ValidationRuleDefinition {
191
191
  try {
192
192
  new URL(value);
193
193
  return true;
194
- } catch {
194
+ } catch (_error) {
195
195
  return false;
196
196
  }
197
197
  },
@@ -212,7 +212,7 @@ export function IsJSON(options: RuleOptions = {}): ValidationRuleDefinition {
212
212
  try {
213
213
  JSON.parse(value);
214
214
  return true;
215
- } catch {
215
+ } catch (_error) {
216
216
  return false;
217
217
  }
218
218
  },
@@ -161,4 +161,28 @@ describe('ConfigModule.setValueByPath', () => {
161
161
  setValueByPath(obj, 'a.b', 'value');
162
162
  expect((obj.a as any).b).toBe('value');
163
163
  });
164
+
165
+ test('should reject dangerous prototype pollution paths', () => {
166
+ const obj: Record<string, unknown> = {};
167
+ expect(() => setValueByPath(obj, '__proto__.polluted', 'yes')).toThrow();
168
+ expect(() => setValueByPath(obj, 'constructor.prototype.evil', 'yes')).toThrow();
169
+ expect(() => setValueByPath(obj, 'safe.__proto__.value', 'yes')).toThrow();
170
+ expect(() => setValueByPath(obj, 'safe. __proto__ .value', 'yes')).toThrow();
171
+ expect(({} as any).polluted).toBeUndefined();
172
+ expect(({} as any).evil).toBeUndefined();
173
+ });
174
+
175
+ test('should reject empty path segments', () => {
176
+ const obj: Record<string, unknown> = {};
177
+ expect(() => setValueByPath(obj, 'a..b', 'value')).toThrow();
178
+ expect(() => setValueByPath(obj, '.a', 'value')).toThrow();
179
+ expect(() => setValueByPath(obj, 'a.', 'value')).toThrow();
180
+ expect(() => setValueByPath(obj, 'a. .b', 'value')).toThrow();
181
+ });
182
+
183
+ test('should normalize whitespace around path segments', () => {
184
+ const obj: Record<string, unknown> = {};
185
+ setValueByPath(obj, ' a . b . c ', 'value');
186
+ expect((obj as any).a.b.c).toBe('value');
187
+ });
164
188
  });
@@ -82,5 +82,57 @@ describe('Context', () => {
82
82
  const response = context.createResponse({ error: 'Not Found' });
83
83
  expect(response.status).toBe(404);
84
84
  });
85
+
86
+ test('createResponse should keep business fields unchanged', async () => {
87
+ const request = new Request('http://localhost:3000/api/users');
88
+ const context = new Context(request);
89
+
90
+ const response = context.createResponse({
91
+ stack: 'business-stack-value',
92
+ data: { trace: 'business-trace-value' },
93
+ });
94
+ const body = (await response.json()) as {
95
+ stack: string;
96
+ data: { trace: string };
97
+ };
98
+
99
+ expect(body.stack).toBe('business-stack-value');
100
+ expect(body.data.trace).toBe('business-trace-value');
101
+ });
102
+
103
+ test('createErrorResponse should redact sensitive fields', async () => {
104
+ const request = new Request('http://localhost:3000/api/users');
105
+ const context = new Context(request);
106
+ context.setStatus(400);
107
+
108
+ const response = context.createErrorResponse({
109
+ error: 'bad request',
110
+ stack: 'hidden',
111
+ details: {
112
+ trace: 'hidden-trace',
113
+ cause: 'hidden-cause',
114
+ },
115
+ items: [
116
+ {
117
+ trace: 'hidden-array-trace',
118
+ safe: 'value',
119
+ },
120
+ ],
121
+ });
122
+ const body = (await response.json()) as {
123
+ error: string;
124
+ stack?: string;
125
+ details: { trace?: string; cause?: string };
126
+ items: Array<{ trace?: string; safe: string }>;
127
+ };
128
+
129
+ expect(response.status).toBe(400);
130
+ expect(body.error).toBe('bad request');
131
+ expect(body.stack).toBeUndefined();
132
+ expect(body.details.trace).toBeUndefined();
133
+ expect(body.details.cause).toBeUndefined();
134
+ expect(body.items[0]?.trace).toBeUndefined();
135
+ expect(body.items[0]?.safe).toBe('value');
136
+ });
85
137
  });
86
138
 
@@ -214,6 +214,93 @@ describe('DatabaseService', () => {
214
214
  });
215
215
  });
216
216
 
217
+ describe('DatabaseService Bun.SQL parameter binding', () => {
218
+ test('should pass parameters as Bun.SQL template values', async () => {
219
+ const service = new DatabaseService({
220
+ database: {
221
+ type: 'postgres',
222
+ config: {
223
+ host: 'localhost',
224
+ port: 5432,
225
+ database: 'test',
226
+ user: 'test',
227
+ password: 'test',
228
+ },
229
+ },
230
+ });
231
+
232
+ let capturedTemplate: TemplateStringsArray | null = null;
233
+ let capturedValues: unknown[] = [];
234
+ const mockConnection = async (
235
+ template: TemplateStringsArray,
236
+ ...values: unknown[]
237
+ ): Promise<Array<Record<string, unknown>>> => {
238
+ capturedTemplate = template;
239
+ capturedValues = values;
240
+ return [{ ok: true }];
241
+ };
242
+
243
+ (service as unknown as { getConnection: () => unknown }).getConnection = () =>
244
+ mockConnection;
245
+ (
246
+ service as unknown as {
247
+ getDatabaseType: () => 'sqlite' | 'postgres' | 'mysql';
248
+ }
249
+ ).getDatabaseType = () => 'postgres';
250
+
251
+ const result = (await service.query<{ ok: boolean }>(
252
+ 'SELECT * FROM users WHERE id = ? AND name = ?',
253
+ [1, "O'Reilly"],
254
+ )) as Array<{ ok: boolean }>;
255
+
256
+ expect(Array.isArray(result)).toBe(true);
257
+ expect(result[0]?.ok).toBe(true);
258
+ expect(capturedValues).toEqual([1, "O'Reilly"]);
259
+ expect(capturedTemplate).toBeDefined();
260
+ expect(capturedTemplate?.length).toBe(3);
261
+ expect(capturedTemplate?.[0]).toBe('SELECT * FROM users WHERE id = ');
262
+ expect(capturedTemplate?.[1]).toBe(' AND name = ');
263
+ expect(capturedTemplate?.[2]).toBe('');
264
+ });
265
+
266
+ test('should throw when placeholders do not match params count', async () => {
267
+ const service = new DatabaseService({
268
+ database: {
269
+ type: 'mysql',
270
+ config: {
271
+ host: 'localhost',
272
+ port: 3306,
273
+ database: 'test',
274
+ user: 'test',
275
+ password: 'test',
276
+ },
277
+ },
278
+ });
279
+
280
+ const mockConnection = async (): Promise<Array<Record<string, unknown>>> => [];
281
+ (service as unknown as { getConnection: () => unknown }).getConnection = () =>
282
+ mockConnection;
283
+ (
284
+ service as unknown as {
285
+ getDatabaseType: () => 'sqlite' | 'postgres' | 'mysql';
286
+ }
287
+ ).getDatabaseType = () => 'mysql';
288
+
289
+ await expect(
290
+ service.query('SELECT * FROM users WHERE id = ?', [1, 2]) as Promise<
291
+ unknown[]
292
+ >,
293
+ ).rejects.toThrow(
294
+ 'Bun.SQL parameterized queries are not fully supported. Consider using template string queries.',
295
+ );
296
+ await expect(
297
+ service.query('SELECT * FROM users WHERE id = ?', [1, 2]) as Promise<
298
+ unknown[]
299
+ >,
300
+ ).rejects.toThrow('Original error: SQL placeholders count does not match parameters count');
301
+ });
302
+ });
303
+
217
304
  describe('ConnectionPool', () => {
218
305
  test('should create connection pool', () => {
219
306
  const { ConnectionPool } = require('../../src/database/connection-pool');
@@ -140,6 +140,30 @@ describe('handleError', () => {
140
140
  expect(body.error).toBe('Internal Server Error');
141
141
  });
142
142
 
143
+ test('should not expose stack field in error response body', async () => {
144
+ const previousNodeEnv = process.env.NODE_ENV;
145
+ process.env.NODE_ENV = 'development';
146
+ const context = createContext();
147
+ const error = new Error('Stack should be hidden from response body');
148
+ error.stack = 'sensitive stack trace';
149
+
150
+ try {
151
+ const response = await handleError(error, context);
152
+ const body = await response.json() as {
153
+ error: string;
154
+ details?: string;
155
+ stack?: string;
156
+ };
157
+
158
+ expect(response.status).toBe(500);
159
+ expect(body.error).toBe('Internal Server Error');
160
+ expect(body.details).toBe('Stack should be hidden from response body');
161
+ expect(body.stack).toBeUndefined();
162
+ } finally {
163
+ process.env.NODE_ENV = previousNodeEnv;
164
+ }
165
+ });
166
+
143
167
  test('should handle string error', async () => {
144
168
  const context = createContext();
145
169
  const error = 'String error message';
@@ -37,6 +37,33 @@ describe('QueueModule', () => {
37
37
  expect(queueProvider.useValue).toBeInstanceOf(QueueService);
38
38
  });
39
39
 
40
+ test('should reuse same QueueService instance for class and token providers', () => {
41
+ QueueModule.forRoot({
42
+ enableWorker: true,
43
+ concurrency: 1,
44
+ });
45
+
46
+ const metadata = Reflect.getMetadata(MODULE_METADATA_KEY, QueueModule);
47
+ expect(metadata).toBeDefined();
48
+ expect(metadata.providers).toBeDefined();
49
+
50
+ const tokenProvider = metadata.providers.find(
51
+ (provider: any) => provider.provide === QUEUE_SERVICE_TOKEN,
52
+ );
53
+ const classProvider = metadata.providers.find(
54
+ (provider: any) => provider.provide === QueueService,
55
+ );
56
+
57
+ expect(tokenProvider).toBeDefined();
58
+ expect(classProvider).toBeDefined();
59
+ expect(tokenProvider.useValue).toBeInstanceOf(QueueService);
60
+ expect(classProvider.useValue).toBe(tokenProvider.useValue);
61
+
62
+ // Regression guard: worker path needs an initialized store.
63
+ const queueService = tokenProvider.useValue as QueueService & { store?: unknown };
64
+ expect(queueService.store).toBeDefined();
65
+ });
66
+
40
67
  test('should use custom store when provided', () => {
41
68
  const customStore = new MemoryQueueStore();
42
69
  QueueModule.forRoot({
@@ -37,6 +37,24 @@ describe('Validation Decorators', () => {
37
37
  const metadata = [{ index: 0, rules: [IsOptional(), IsString()] }];
38
38
  expect(() => validateParameters([undefined], metadata)).not.toThrow();
39
39
  });
40
+
41
+ test('IsEmail should accept common valid emails', () => {
42
+ const metadata = [{ index: 0, rules: [IsEmail()] }];
43
+ expect(() => validateParameters(['test@example.com'], metadata)).not.toThrow();
44
+ expect(() => validateParameters(['user.name+tag@sub.example.co'], metadata)).not.toThrow();
45
+ });
46
+
47
+ test('IsEmail should reject invalid or risky inputs', () => {
48
+ const metadata = [{ index: 0, rules: [IsEmail()] }];
49
+ const tooLongLocal = `${'a'.repeat(65)}@example.com`;
50
+ const tooLongEmail = `${'a'.repeat(245)}@ex.com`;
51
+
52
+ expect(() => validateParameters(['not-email'], metadata)).toThrow(ValidationError);
53
+ expect(() => validateParameters(['a..b@example.com'], metadata)).toThrow(ValidationError);
54
+ expect(() => validateParameters(['a@-example.com'], metadata)).toThrow(ValidationError);
55
+ expect(() => validateParameters([tooLongLocal], metadata)).toThrow(ValidationError);
56
+ expect(() => validateParameters([tooLongEmail], metadata)).toThrow(ValidationError);
57
+ });
40
58
  });
41
59
 
42
60