@dangao/bun-server 2.0.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +4 -0
  2. package/dist/config/config-module.d.ts +3 -0
  3. package/dist/config/config-module.d.ts.map +1 -1
  4. package/dist/controller/controller.d.ts.map +1 -1
  5. package/dist/core/application.d.ts +6 -1
  6. package/dist/core/application.d.ts.map +1 -1
  7. package/dist/core/context.d.ts +10 -0
  8. package/dist/core/context.d.ts.map +1 -1
  9. package/dist/core/server.d.ts +5 -0
  10. package/dist/core/server.d.ts.map +1 -1
  11. package/dist/database/database-context.d.ts +25 -0
  12. package/dist/database/database-context.d.ts.map +1 -0
  13. package/dist/database/database-extension.d.ts +8 -9
  14. package/dist/database/database-extension.d.ts.map +1 -1
  15. package/dist/database/database-module.d.ts +7 -1
  16. package/dist/database/database-module.d.ts.map +1 -1
  17. package/dist/database/db-proxy.d.ts +12 -0
  18. package/dist/database/db-proxy.d.ts.map +1 -0
  19. package/dist/database/index.d.ts +6 -1
  20. package/dist/database/index.d.ts.map +1 -1
  21. package/dist/database/orm/transaction-interceptor.d.ts +0 -16
  22. package/dist/database/orm/transaction-interceptor.d.ts.map +1 -1
  23. package/dist/database/orm/transaction-manager.d.ts +10 -61
  24. package/dist/database/orm/transaction-manager.d.ts.map +1 -1
  25. package/dist/database/service.d.ts +4 -4
  26. package/dist/database/service.d.ts.map +1 -1
  27. package/dist/database/sql-manager.d.ts +14 -0
  28. package/dist/database/sql-manager.d.ts.map +1 -0
  29. package/dist/database/sqlite-adapter.d.ts +32 -0
  30. package/dist/database/sqlite-adapter.d.ts.map +1 -0
  31. package/dist/database/strategy-decorator.d.ts +8 -0
  32. package/dist/database/strategy-decorator.d.ts.map +1 -0
  33. package/dist/database/types.d.ts +122 -1
  34. package/dist/database/types.d.ts.map +1 -1
  35. package/dist/di/module-registry.d.ts.map +1 -1
  36. package/dist/index.d.ts +3 -3
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3544 -2876
  39. package/dist/microservice/service-registry/service-registry-module.d.ts +16 -0
  40. package/dist/microservice/service-registry/service-registry-module.d.ts.map +1 -1
  41. package/dist/router/index.d.ts +1 -0
  42. package/dist/router/index.d.ts.map +1 -1
  43. package/dist/router/registry.d.ts +1 -1
  44. package/dist/router/registry.d.ts.map +1 -1
  45. package/dist/router/route.d.ts +2 -1
  46. package/dist/router/route.d.ts.map +1 -1
  47. package/dist/router/router.d.ts +1 -1
  48. package/dist/router/router.d.ts.map +1 -1
  49. package/dist/router/timeout-decorator.d.ts +6 -0
  50. package/dist/router/timeout-decorator.d.ts.map +1 -0
  51. package/dist/validation/decorators.d.ts.map +1 -1
  52. package/docs/database.md +48 -15
  53. package/docs/idle-timeout.md +42 -0
  54. package/docs/lifecycle.md +6 -0
  55. package/docs/microservice-nacos.md +1 -0
  56. package/docs/microservice-service-registry.md +7 -0
  57. package/docs/zh/database.md +48 -15
  58. package/docs/zh/idle-timeout.md +41 -0
  59. package/docs/zh/lifecycle.md +5 -0
  60. package/docs/zh/microservice-nacos.md +1 -0
  61. package/docs/zh/microservice-service-registry.md +6 -0
  62. package/package.json +5 -4
  63. package/src/ai/providers/anthropic-provider.ts +1 -1
  64. package/src/ai/providers/google-provider.ts +1 -1
  65. package/src/ai/providers/ollama-provider.ts +1 -1
  66. package/src/ai/providers/openai-provider.ts +2 -2
  67. package/src/auth/jwt.ts +1 -1
  68. package/src/cache/interceptors.ts +3 -3
  69. package/src/cache/types.ts +10 -10
  70. package/src/client/runtime.ts +1 -1
  71. package/src/config/config-module.ts +46 -14
  72. package/src/config/service.ts +2 -2
  73. package/src/controller/controller.ts +11 -1
  74. package/src/controller/param-binder.ts +1 -1
  75. package/src/conversation/service.ts +1 -1
  76. package/src/core/application.ts +61 -2
  77. package/src/core/cluster.ts +4 -4
  78. package/src/core/context.ts +71 -0
  79. package/src/core/server.ts +10 -0
  80. package/src/dashboard/controller.ts +2 -2
  81. package/src/database/connection-manager.ts +4 -4
  82. package/src/database/database-context.ts +43 -0
  83. package/src/database/database-extension.ts +12 -45
  84. package/src/database/database-module.ts +254 -11
  85. package/src/database/db-proxy.ts +75 -0
  86. package/src/database/index.ts +29 -0
  87. package/src/database/orm/transaction-interceptor.ts +12 -149
  88. package/src/database/orm/transaction-manager.ts +143 -210
  89. package/src/database/service.ts +53 -30
  90. package/src/database/sql-manager.ts +62 -0
  91. package/src/database/sqlite-adapter.ts +121 -0
  92. package/src/database/strategy-decorator.ts +42 -0
  93. package/src/database/types.ts +133 -1
  94. package/src/debug/middleware.ts +2 -2
  95. package/src/di/module-registry.ts +21 -5
  96. package/src/error/handler.ts +3 -3
  97. package/src/events/event-module.ts +4 -4
  98. package/src/files/static-middleware.ts +2 -2
  99. package/src/files/storage.ts +1 -1
  100. package/src/index.ts +27 -1
  101. package/src/interceptor/builtin/log-interceptor.ts +1 -1
  102. package/src/mcp/server.ts +1 -1
  103. package/src/microservice/service-registry/service-registry-module.ts +25 -1
  104. package/src/middleware/builtin/error-handler.ts +2 -2
  105. package/src/middleware/builtin/file-upload.ts +1 -1
  106. package/src/middleware/builtin/rate-limit.ts +1 -1
  107. package/src/middleware/builtin/static-file.ts +2 -2
  108. package/src/prompt/stores/file-store.ts +4 -4
  109. package/src/request/body-parser.ts +3 -3
  110. package/src/router/index.ts +1 -0
  111. package/src/router/registry.ts +10 -1
  112. package/src/router/route.ts +31 -3
  113. package/src/router/router.ts +10 -1
  114. package/src/router/timeout-decorator.ts +35 -0
  115. package/src/security/filter.ts +1 -1
  116. package/src/security/guards/guard-registry.ts +1 -1
  117. package/src/session/middleware.ts +1 -1
  118. package/src/session/types.ts +5 -5
  119. package/src/testing/test-client.ts +1 -1
  120. package/src/validation/decorators.ts +70 -2
  121. package/src/validation/rules/common.ts +2 -2
  122. package/tests/config/config-module-extended.test.ts +24 -0
  123. package/tests/core/application.test.ts +10 -0
  124. package/tests/core/context.test.ts +52 -0
  125. package/tests/database/database-module.test.ts +92 -344
  126. package/tests/database/db-proxy.test.ts +93 -0
  127. package/tests/database/sql-manager.test.ts +43 -0
  128. package/tests/database/sqlite-adapter.test.ts +45 -0
  129. package/tests/database/strategy-decorator.test.ts +29 -0
  130. package/tests/database/transaction.test.ts +84 -222
  131. package/tests/di/lifecycle.test.ts +37 -0
  132. package/tests/error/error-handler.test.ts +24 -0
  133. package/tests/microservice/service-registry.test.ts +15 -0
  134. package/tests/router/timeout-decorator.test.ts +48 -0
  135. package/tests/validation/validation.test.ts +18 -0
@@ -1,237 +1,99 @@
1
- import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
2
- import {
3
- Transactional,
4
- TransactionManager,
5
- Propagation,
6
- IsolationLevel,
7
- TransactionStatus,
8
- TRANSACTION_SERVICE_TOKEN,
9
- } from '../../src/database/orm';
10
- import { DatabaseService, DATABASE_SERVICE_TOKEN } from '../../src/database';
11
- import { Container } from '../../src/di/container';
12
-
13
- describe('TransactionManager', () => {
14
- let container: Container;
15
- let databaseService: DatabaseService;
16
- let transactionManager: TransactionManager;
17
-
18
- beforeEach(async () => {
19
- container = new Container();
20
- databaseService = new DatabaseService({
21
- database: {
22
- type: 'sqlite',
23
- config: {
24
- path: ':memory:',
25
- },
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { runWithSession } from '../../src/database/database-context';
4
+ import { BunSQLManager } from '../../src/database/sql-manager';
5
+ import { TransactionManager } from '../../src/database/orm/transaction-manager';
6
+
7
+ function createReserved() {
8
+ const calls: string[] = [];
9
+ const reserved = (async () => [{ ok: true }]) as any;
10
+ reserved.begin = async <T>(fn: () => Promise<T>) => {
11
+ calls.push('begin');
12
+ const result = await fn();
13
+ calls.push('commit');
14
+ return result;
15
+ };
16
+ reserved.release = async () => {
17
+ calls.push('release');
18
+ };
19
+ return {
20
+ reserved,
21
+ calls,
22
+ };
23
+ }
24
+
25
+ describe('TransactionManager V2', () => {
26
+ test('should run transaction in reserved session', async () => {
27
+ const sqlManager = new BunSQLManager();
28
+ const manager = new TransactionManager(sqlManager as any);
29
+ const { reserved, calls } = createReserved();
30
+
31
+ const result = await runWithSession(
32
+ {
33
+ tenantId: 'default',
34
+ reserved,
26
35
  },
27
- });
28
- await databaseService.initialize();
29
-
30
- transactionManager = new TransactionManager(databaseService);
31
- container.registerInstance(DATABASE_SERVICE_TOKEN, databaseService);
32
- container.registerInstance(TRANSACTION_SERVICE_TOKEN, transactionManager);
33
-
34
- // 创建测试表
35
- databaseService.query(`
36
- CREATE TABLE IF NOT EXISTS test_accounts (
37
- id INTEGER PRIMARY KEY AUTOINCREMENT,
38
- name TEXT NOT NULL,
39
- balance REAL NOT NULL DEFAULT 0
40
- )
41
- `);
42
- });
43
-
44
- afterEach(async () => {
45
- await databaseService.closePool();
46
- });
47
-
48
- test('should begin and commit transaction', async () => {
49
- const context = await transactionManager.beginTransaction();
50
- expect(context.status).toBe(TransactionStatus.ACTIVE);
51
- expect(context.id).toBeDefined();
52
-
53
- // 插入数据
54
- await databaseService.query(
55
- 'INSERT INTO test_accounts (name, balance) VALUES (?, ?)',
56
- ['Alice', 100],
36
+ async () => await manager.runInTransaction(async () => 'ok'),
57
37
  );
58
38
 
59
- await transactionManager.commitTransaction(context.id);
60
-
61
- // 验证数据已提交
62
- const result = databaseService.query('SELECT * FROM test_accounts WHERE name = ?', ['Alice']);
63
- expect(Array.isArray(result) ? result.length : 0).toBeGreaterThan(0);
39
+ expect(result).toBe('ok');
40
+ expect(calls).toEqual(['begin', 'commit']);
64
41
  });
65
42
 
66
- test('should rollback transaction', async () => {
67
- const context = await transactionManager.beginTransaction();
68
-
69
- // 插入数据
70
- await databaseService.query(
71
- 'INSERT INTO test_accounts (name, balance) VALUES (?, ?)',
72
- ['Bob', 200],
73
- );
74
-
75
- await transactionManager.rollbackTransaction(context.id);
76
-
77
- // 验证数据已回滚
78
- const result = databaseService.query('SELECT * FROM test_accounts WHERE name = ?', ['Bob']);
79
- expect(Array.isArray(result) ? result.length : 0).toBe(0);
80
- });
81
-
82
- test('should create and rollback to savepoint', async () => {
83
- const context = await transactionManager.beginTransaction();
84
-
85
- // 插入第一条数据
86
- await databaseService.query(
87
- 'INSERT INTO test_accounts (name, balance) VALUES (?, ?)',
88
- ['Charlie', 300],
89
- );
90
-
91
- // 创建保存点
92
- const savepoint = await transactionManager.createSavepoint(context.id);
93
- expect(savepoint).toBeDefined();
94
- expect(context.level).toBe(1);
95
-
96
- // 插入第二条数据
97
- await databaseService.query(
98
- 'INSERT INTO test_accounts (name, balance) VALUES (?, ?)',
99
- ['David', 400],
43
+ test('should auto lazy reserve when transaction starts', async () => {
44
+ const sqlManager = new BunSQLManager();
45
+ const manager = new TransactionManager(sqlManager as any);
46
+ const { reserved, calls } = createReserved();
47
+
48
+ let reserveCalled = 0;
49
+ await runWithSession(
50
+ {
51
+ tenantId: 'default',
52
+ lazyReserve: async () => {
53
+ reserveCalled += 1;
54
+ return reserved;
55
+ },
56
+ },
57
+ async () => {
58
+ await manager.runInTransaction(async () => undefined);
59
+ },
100
60
  );
101
61
 
102
- // 回滚到保存点
103
- await transactionManager.rollbackToSavepoint(context.id, savepoint);
104
-
105
- // 提交事务
106
- await transactionManager.commitTransaction(context.id);
107
-
108
- // 验证:Charlie 应该存在,David 应该不存在(已回滚)
109
- const charlie = databaseService.query('SELECT * FROM test_accounts WHERE name = ?', ['Charlie']);
110
- const david = databaseService.query('SELECT * FROM test_accounts WHERE name = ?', ['David']);
111
-
112
- expect(Array.isArray(charlie) ? charlie.length : 0).toBeGreaterThan(0);
113
- expect(Array.isArray(david) ? david.length : 0).toBe(0);
62
+ expect(reserveCalled).toBe(1);
63
+ expect(calls).toEqual(['begin', 'commit']);
114
64
  });
115
65
 
116
- test('should get current transaction', async () => {
117
- expect(transactionManager.hasActiveTransaction()).toBe(false);
118
-
119
- const context = await transactionManager.beginTransaction();
120
- expect(transactionManager.hasActiveTransaction()).toBe(true);
121
-
122
- const current = transactionManager.getCurrentTransaction();
123
- expect(current).not.toBeNull();
124
- expect(current?.id).toBe(context.id);
125
-
126
- await transactionManager.commitTransaction(context.id);
127
- expect(transactionManager.hasActiveTransaction()).toBe(false);
128
- });
129
- });
130
-
131
- describe('@Transactional Decorator', () => {
132
- let container: Container;
133
- let databaseService: DatabaseService;
134
- let transactionManager: TransactionManager;
135
-
136
- beforeEach(async () => {
137
- container = new Container();
138
- databaseService = new DatabaseService({
139
- database: {
140
- type: 'sqlite',
141
- config: {
142
- path: ':memory:',
66
+ test('should run nested transaction via savepoint', async () => {
67
+ const sqlManager = new BunSQLManager();
68
+ const manager = new TransactionManager(sqlManager as any);
69
+ const savepointCommands: string[] = [];
70
+
71
+ const reserved = (async (
72
+ strings: TemplateStringsArray,
73
+ ...values: unknown[]
74
+ ) => {
75
+ savepointCommands.push(strings.join('?'));
76
+ return [];
77
+ }) as any;
78
+ reserved.begin = async <T>(fn: () => Promise<T>) => await fn();
79
+ reserved.release = async () => undefined;
80
+
81
+ await runWithSession(
82
+ {
83
+ tenantId: 'default',
84
+ reserved,
85
+ transaction: {
86
+ id: 'tx-1',
87
+ status: 'ACTIVE' as any,
88
+ level: 0,
89
+ savepoints: [],
143
90
  },
144
91
  },
145
- });
146
- await databaseService.initialize();
147
-
148
- transactionManager = new TransactionManager(databaseService);
149
- container.registerInstance(DATABASE_SERVICE_TOKEN, databaseService);
150
- container.registerInstance(TRANSACTION_SERVICE_TOKEN, transactionManager);
151
-
152
- // 创建测试表
153
- databaseService.query(`
154
- CREATE TABLE IF NOT EXISTS test_users (
155
- id INTEGER PRIMARY KEY AUTOINCREMENT,
156
- name TEXT NOT NULL,
157
- email TEXT NOT NULL UNIQUE
158
- )
159
- `);
160
- });
161
-
162
- afterEach(async () => {
163
- await databaseService.closePool();
164
- });
165
-
166
- test('should apply transaction to method', async () => {
167
- class UserService {
168
- @Transactional()
169
- public async createUser(name: string, email: string): Promise<void> {
170
- await databaseService.query(
171
- 'INSERT INTO test_users (name, email) VALUES (?, ?)',
172
- [name, email],
173
- );
174
- }
175
-
176
- @Transactional({ propagation: Propagation.REQUIRES_NEW })
177
- public async createUserInNewTransaction(name: string, email: string): Promise<void> {
178
- await databaseService.query(
179
- 'INSERT INTO test_users (name, email) VALUES (?, ?)',
180
- [name, email],
181
- );
182
- }
183
- }
184
-
185
- const service = new UserService();
186
- const prototype = UserService.prototype;
187
-
188
- // 使用 TransactionInterceptor 执行方法
189
- const { TransactionInterceptor } = await import('../../src/database/orm/transaction-interceptor');
190
- await TransactionInterceptor.executeWithTransaction(
191
- prototype,
192
- 'createUser',
193
- service.createUser.bind(service),
194
- ['Alice', 'alice@example.com'],
195
- container,
92
+ async () => {
93
+ await manager.runInNestedTransaction(async () => undefined);
94
+ },
196
95
  );
197
96
 
198
- // 验证数据已提交
199
- const result = databaseService.query('SELECT * FROM test_users WHERE name = ?', ['Alice']);
200
- expect(Array.isArray(result) ? result.length : 0).toBeGreaterThan(0);
201
- });
202
-
203
- test('should rollback on error', async () => {
204
- class UserService {
205
- @Transactional()
206
- public async createUserWithError(name: string, email: string): Promise<void> {
207
- await databaseService.query(
208
- 'INSERT INTO test_users (name, email) VALUES (?, ?)',
209
- [name, email],
210
- );
211
- throw new Error('Test error');
212
- }
213
- }
214
-
215
- const service = new UserService();
216
- const prototype = UserService.prototype;
217
-
218
- const { TransactionInterceptor } = await import('../../src/database/orm/transaction-interceptor');
219
-
220
- try {
221
- await TransactionInterceptor.executeWithTransaction(
222
- prototype,
223
- 'createUserWithError',
224
- service.createUserWithError.bind(service),
225
- ['Bob', 'bob@example.com'],
226
- container,
227
- );
228
- expect(true).toBe(false); // 不应该到达这里
229
- } catch (error) {
230
- expect(error).toBeInstanceOf(Error);
231
- }
232
-
233
- // 验证数据已回滚
234
- const result = databaseService.query('SELECT * FROM test_users WHERE name = ?', ['Bob']);
235
- expect(Array.isArray(result) ? result.length : 0).toBe(0);
97
+ expect(savepointCommands.some((item) => item.includes('SAVEPOINT'))).toBe(true);
236
98
  });
237
99
  });
@@ -136,5 +136,42 @@ describe('Lifecycle Hooks', () => {
136
136
  const shutdownEntry = calls.find((c) => c.startsWith('onApplicationShutdown'));
137
137
  expect(shutdownEntry).toBeDefined();
138
138
  });
139
+
140
+ test('should call onModuleInit once for duplicated provider instance', async () => {
141
+ const calls: string[] = [];
142
+
143
+ @Injectable()
144
+ class SharedService implements OnModuleInit {
145
+ public onModuleInit(): void {
146
+ calls.push('init');
147
+ }
148
+ }
149
+
150
+ const shared = new SharedService();
151
+
152
+ @Controller('/dup')
153
+ class DupController {
154
+ @GET('/')
155
+ public get(): string {
156
+ return 'ok';
157
+ }
158
+ }
159
+
160
+ @Module({
161
+ controllers: [DupController],
162
+ providers: [
163
+ { provide: SharedService, useValue: shared },
164
+ { provide: Symbol.for('dup-shared'), useValue: shared },
165
+ ],
166
+ })
167
+ class DupModule {}
168
+
169
+ const app = new Application({ port: 0, enableSignalHandlers: false });
170
+ app.registerModule(DupModule);
171
+ await app.listen(0);
172
+ await app.stop();
173
+
174
+ expect(calls.length).toBe(1);
175
+ });
139
176
  });
140
177
  });
@@ -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';
@@ -30,6 +30,21 @@ describe('ServiceRegistryModule', () => {
30
30
  (provider: any) => provider.provide === SERVICE_REGISTRY_TOKEN,
31
31
  );
32
32
  expect(serviceRegistryProvider).toBeDefined();
33
+ expect(ServiceRegistryModule.autoRegister).toBe(true);
34
+ });
35
+
36
+ test('should support autoRegister option', () => {
37
+ ServiceRegistryModule.forRoot({
38
+ provider: 'nacos',
39
+ autoRegister: false,
40
+ nacos: {
41
+ client: {
42
+ serverList: ['http://localhost:8848'],
43
+ namespaceId: 'public',
44
+ },
45
+ },
46
+ });
47
+ expect(ServiceRegistryModule.autoRegister).toBe(false);
33
48
  });
34
49
 
35
50
  test('should throw error when provider is not supported', () => {
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import { Context } from '../../src/core/context';
4
+ import { Route } from '../../src/router/route';
5
+ import {
6
+ IdleTimeout,
7
+ getIdleTimeout,
8
+ } from '../../src/router/timeout-decorator';
9
+
10
+ describe('IdleTimeout decorator', () => {
11
+ test('should get class level timeout', () => {
12
+ @IdleTimeout(500)
13
+ class Controller {
14
+ public list(): void {}
15
+ }
16
+ expect(getIdleTimeout(Controller as any, 'list')).toBe(500);
17
+ });
18
+
19
+ test('should prefer method level timeout', () => {
20
+ @IdleTimeout(500)
21
+ class Controller {
22
+ @IdleTimeout(100)
23
+ public list(): void {}
24
+ }
25
+ expect(getIdleTimeout(Controller as any, 'list')).toBe(100);
26
+ });
27
+ });
28
+
29
+ describe('Route timeout', () => {
30
+ test('should throw 408 when route execution times out', async () => {
31
+ const route = new Route(
32
+ 'GET',
33
+ '/slow',
34
+ async () => {
35
+ await new Promise((resolve) => setTimeout(resolve, 30));
36
+ return new Response('ok');
37
+ },
38
+ [],
39
+ undefined,
40
+ undefined,
41
+ 5,
42
+ );
43
+
44
+ const context = new Context(new Request('http://localhost/slow'));
45
+ await expect(route.execute(context)).rejects.toThrow('Request Timeout');
46
+ });
47
+ });
48
+
@@ -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