@dangao/bun-server 1.7.1 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +129 -21
  2. package/dist/di/decorators.d.ts +37 -0
  3. package/dist/di/decorators.d.ts.map +1 -1
  4. package/dist/di/index.d.ts +1 -1
  5. package/dist/di/index.d.ts.map +1 -1
  6. package/dist/di/module-registry.d.ts +17 -0
  7. package/dist/di/module-registry.d.ts.map +1 -1
  8. package/dist/events/decorators.d.ts +52 -0
  9. package/dist/events/decorators.d.ts.map +1 -0
  10. package/dist/events/event-module.d.ts +97 -0
  11. package/dist/events/event-module.d.ts.map +1 -0
  12. package/dist/events/index.d.ts +5 -0
  13. package/dist/events/index.d.ts.map +1 -0
  14. package/dist/events/service.d.ts +76 -0
  15. package/dist/events/service.d.ts.map +1 -0
  16. package/dist/events/types.d.ts +184 -0
  17. package/dist/events/types.d.ts.map +1 -0
  18. package/dist/index.d.ts +5 -3
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1511 -11
  21. package/dist/security/filter.d.ts +23 -0
  22. package/dist/security/filter.d.ts.map +1 -1
  23. package/dist/security/guards/builtin/auth-guard.d.ts +44 -0
  24. package/dist/security/guards/builtin/auth-guard.d.ts.map +1 -0
  25. package/dist/security/guards/builtin/index.d.ts +3 -0
  26. package/dist/security/guards/builtin/index.d.ts.map +1 -0
  27. package/dist/security/guards/builtin/roles-guard.d.ts +66 -0
  28. package/dist/security/guards/builtin/roles-guard.d.ts.map +1 -0
  29. package/dist/security/guards/decorators.d.ts +50 -0
  30. package/dist/security/guards/decorators.d.ts.map +1 -0
  31. package/dist/security/guards/execution-context.d.ts +56 -0
  32. package/dist/security/guards/execution-context.d.ts.map +1 -0
  33. package/dist/security/guards/guard-registry.d.ts +67 -0
  34. package/dist/security/guards/guard-registry.d.ts.map +1 -0
  35. package/dist/security/guards/index.d.ts +7 -0
  36. package/dist/security/guards/index.d.ts.map +1 -0
  37. package/dist/security/guards/reflector.d.ts +57 -0
  38. package/dist/security/guards/reflector.d.ts.map +1 -0
  39. package/dist/security/guards/types.d.ts +126 -0
  40. package/dist/security/guards/types.d.ts.map +1 -0
  41. package/dist/security/index.d.ts +1 -0
  42. package/dist/security/index.d.ts.map +1 -1
  43. package/dist/security/security-module.d.ts +20 -0
  44. package/dist/security/security-module.d.ts.map +1 -1
  45. package/dist/validation/class-validator.d.ts +108 -0
  46. package/dist/validation/class-validator.d.ts.map +1 -0
  47. package/dist/validation/custom-validator.d.ts +130 -0
  48. package/dist/validation/custom-validator.d.ts.map +1 -0
  49. package/dist/validation/errors.d.ts +22 -2
  50. package/dist/validation/errors.d.ts.map +1 -1
  51. package/dist/validation/index.d.ts +7 -1
  52. package/dist/validation/index.d.ts.map +1 -1
  53. package/dist/validation/rules/array.d.ts +33 -0
  54. package/dist/validation/rules/array.d.ts.map +1 -0
  55. package/dist/validation/rules/common.d.ts +90 -0
  56. package/dist/validation/rules/common.d.ts.map +1 -0
  57. package/dist/validation/rules/conditional.d.ts +30 -0
  58. package/dist/validation/rules/conditional.d.ts.map +1 -0
  59. package/dist/validation/rules/index.d.ts +5 -0
  60. package/dist/validation/rules/index.d.ts.map +1 -0
  61. package/dist/validation/rules/object.d.ts +30 -0
  62. package/dist/validation/rules/object.d.ts.map +1 -0
  63. package/dist/validation/types.d.ts +52 -1
  64. package/dist/validation/types.d.ts.map +1 -1
  65. package/docs/events.md +494 -0
  66. package/docs/guards.md +376 -0
  67. package/docs/guide.md +309 -1
  68. package/docs/request-lifecycle.md +444 -0
  69. package/docs/validation.md +407 -0
  70. package/docs/zh/events.md +494 -0
  71. package/docs/zh/guards.md +376 -0
  72. package/docs/zh/guide.md +309 -1
  73. package/docs/zh/request-lifecycle.md +444 -0
  74. package/docs/zh/validation.md +407 -0
  75. package/package.json +1 -1
  76. package/src/di/decorators.ts +46 -0
  77. package/src/di/index.ts +10 -1
  78. package/src/di/module-registry.ts +39 -0
  79. package/src/events/decorators.ts +103 -0
  80. package/src/events/event-module.ts +272 -0
  81. package/src/events/index.ts +32 -0
  82. package/src/events/service.ts +352 -0
  83. package/src/events/types.ts +223 -0
  84. package/src/index.ts +133 -1
  85. package/src/security/filter.ts +88 -8
  86. package/src/security/guards/builtin/auth-guard.ts +68 -0
  87. package/src/security/guards/builtin/index.ts +3 -0
  88. package/src/security/guards/builtin/roles-guard.ts +165 -0
  89. package/src/security/guards/decorators.ts +124 -0
  90. package/src/security/guards/execution-context.ts +152 -0
  91. package/src/security/guards/guard-registry.ts +164 -0
  92. package/src/security/guards/index.ts +7 -0
  93. package/src/security/guards/reflector.ts +99 -0
  94. package/src/security/guards/types.ts +144 -0
  95. package/src/security/index.ts +1 -0
  96. package/src/security/security-module.ts +72 -2
  97. package/src/validation/class-validator.ts +322 -0
  98. package/src/validation/custom-validator.ts +289 -0
  99. package/src/validation/errors.ts +50 -2
  100. package/src/validation/index.ts +103 -1
  101. package/src/validation/rules/array.ts +118 -0
  102. package/src/validation/rules/common.ts +286 -0
  103. package/src/validation/rules/conditional.ts +52 -0
  104. package/src/validation/rules/index.ts +51 -0
  105. package/src/validation/rules/object.ts +86 -0
  106. package/src/validation/types.ts +61 -1
  107. package/tests/auth/auth-decorators.test.ts +241 -0
  108. package/tests/auth/oauth2-service.test.ts +318 -0
  109. package/tests/cache/cache-decorators-extended.test.ts +272 -0
  110. package/tests/cache/cache-interceptors.test.ts +534 -0
  111. package/tests/cache/cache-service-proxy.test.ts +246 -0
  112. package/tests/cache/memory-cache-store.test.ts +155 -0
  113. package/tests/cache/redis-cache-store.test.ts +199 -0
  114. package/tests/config/config-center-integration.test.ts +334 -0
  115. package/tests/config/config-module-extended.test.ts +165 -0
  116. package/tests/controller/param-binder.test.ts +333 -0
  117. package/tests/di/global-module.test.ts +487 -0
  118. package/tests/error/error-handler.test.ts +166 -57
  119. package/tests/error/i18n-extended.test.ts +105 -0
  120. package/tests/events/event-decorators.test.ts +173 -0
  121. package/tests/events/event-emitter.test.ts +373 -0
  122. package/tests/events/event-listener-scanner.test.ts +114 -0
  123. package/tests/events/event-module.test.ts +204 -0
  124. package/tests/extensions/logger-module.test.ts +158 -0
  125. package/tests/files/file-storage.test.ts +136 -0
  126. package/tests/interceptor/base-interceptor.test.ts +605 -0
  127. package/tests/interceptor/builtin/cache-interceptor.test.ts +233 -86
  128. package/tests/interceptor/builtin/log-interceptor.test.ts +469 -0
  129. package/tests/interceptor/builtin/permission-interceptor.test.ts +219 -120
  130. package/tests/interceptor/interceptor-chain.test.ts +241 -189
  131. package/tests/interceptor/interceptor-metadata.test.ts +221 -0
  132. package/tests/microservice/circuit-breaker.test.ts +221 -0
  133. package/tests/microservice/service-client-decorators.test.ts +86 -0
  134. package/tests/microservice/service-client-interceptors.test.ts +274 -0
  135. package/tests/microservice/service-registry-decorators.test.ts +147 -0
  136. package/tests/microservice/tracer.test.ts +213 -0
  137. package/tests/microservice/tracing-collectors.test.ts +168 -0
  138. package/tests/middleware/builtin/middleware-builtin-extended.test.ts +237 -0
  139. package/tests/middleware/builtin/rate-limit.test.ts +257 -0
  140. package/tests/middleware/middleware-decorators.test.ts +222 -0
  141. package/tests/middleware/middleware-pipeline.test.ts +160 -0
  142. package/tests/queue/queue-decorators.test.ts +139 -0
  143. package/tests/queue/queue-service.test.ts +191 -0
  144. package/tests/request/body-parser-extended.test.ts +291 -0
  145. package/tests/request/request-wrapper.test.ts +319 -0
  146. package/tests/router/router-decorators.test.ts +260 -0
  147. package/tests/router/router-extended.test.ts +298 -0
  148. package/tests/security/guards/guards-integration.test.ts +371 -0
  149. package/tests/security/guards/guards.test.ts +775 -0
  150. package/tests/security/guards/reflector.test.ts +188 -0
  151. package/tests/security/security-filter.test.ts +182 -0
  152. package/tests/security/security-module-extended.test.ts +133 -0
  153. package/tests/security/security-module.test.ts +2 -2
  154. package/tests/session/memory-session-store.test.ts +172 -0
  155. package/tests/session/session-decorators.test.ts +163 -0
  156. package/tests/swagger/ui.test.ts +212 -0
  157. package/tests/validation/class-validator.test.ts +349 -0
  158. package/tests/validation/custom-validator.test.ts +335 -0
  159. package/tests/validation/rules.test.ts +543 -0
@@ -0,0 +1,349 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import 'reflect-metadata';
3
+
4
+ import {
5
+ ValidateClass,
6
+ Property,
7
+ NestedProperty,
8
+ ArrayNestedProperty,
9
+ validateObject,
10
+ validateObjectSync,
11
+ getClassValidationMetadata,
12
+ isValidateClass,
13
+ IsString,
14
+ IsEmail,
15
+ IsNumber,
16
+ IsOptional,
17
+ MinLength,
18
+ IsInt,
19
+ Min,
20
+ Max,
21
+ IsArray,
22
+ ArrayMinSize,
23
+ IsNotEmpty,
24
+ ValidationError,
25
+ } from '../../src/validation';
26
+
27
+ describe('Class Validator', () => {
28
+ describe('ValidateClass decorator', () => {
29
+ test('should mark class as validate class', () => {
30
+ @ValidateClass()
31
+ class TestDto {
32
+ public name: string = '';
33
+ }
34
+
35
+ expect(isValidateClass(TestDto)).toBe(true);
36
+ });
37
+
38
+ test('should return false for non-decorated class', () => {
39
+ class TestDto {
40
+ public name: string = '';
41
+ }
42
+
43
+ expect(isValidateClass(TestDto)).toBe(false);
44
+ });
45
+ });
46
+
47
+ describe('Property decorator', () => {
48
+ test('should store validation metadata', () => {
49
+ @ValidateClass()
50
+ class TestDto {
51
+ @Property(IsString(), MinLength(2))
52
+ public name: string = '';
53
+
54
+ @Property(IsEmail())
55
+ public email: string = '';
56
+ }
57
+
58
+ const metadata = getClassValidationMetadata(TestDto);
59
+ expect(metadata.length).toBe(2);
60
+
61
+ const nameMetadata = metadata.find((m) => m.property === 'name');
62
+ expect(nameMetadata).toBeDefined();
63
+ expect(nameMetadata!.rules.length).toBe(2);
64
+ expect(nameMetadata!.rules[0].name).toBe('isString');
65
+ expect(nameMetadata!.rules[1].name).toBe('minLength');
66
+
67
+ const emailMetadata = metadata.find((m) => m.property === 'email');
68
+ expect(emailMetadata).toBeDefined();
69
+ expect(emailMetadata!.rules.length).toBe(1);
70
+ expect(emailMetadata!.rules[0].name).toBe('isEmail');
71
+ });
72
+ });
73
+
74
+ describe('validateObject', () => {
75
+ @ValidateClass()
76
+ class CreateUserDto {
77
+ @Property(IsString(), MinLength(2))
78
+ public name: string = '';
79
+
80
+ @Property(IsEmail())
81
+ public email: string = '';
82
+
83
+ @Property(IsOptional(), IsInt(), Min(0), Max(150))
84
+ public age?: number;
85
+ }
86
+
87
+ test('should pass for valid object', () => {
88
+ const dto = {
89
+ name: 'John',
90
+ email: 'john@example.com',
91
+ age: 25,
92
+ };
93
+
94
+ expect(() => validateObject(dto, CreateUserDto)).not.toThrow();
95
+ });
96
+
97
+ test('should pass for valid object without optional fields', () => {
98
+ const dto = {
99
+ name: 'John',
100
+ email: 'john@example.com',
101
+ };
102
+
103
+ expect(() => validateObject(dto, CreateUserDto)).not.toThrow();
104
+ });
105
+
106
+ test('should throw ValidationError for invalid name', () => {
107
+ const dto = {
108
+ name: 'A',
109
+ email: 'john@example.com',
110
+ };
111
+
112
+ expect(() => validateObject(dto, CreateUserDto)).toThrow(ValidationError);
113
+ });
114
+
115
+ test('should throw ValidationError for invalid email', () => {
116
+ const dto = {
117
+ name: 'John',
118
+ email: 'invalid-email',
119
+ };
120
+
121
+ expect(() => validateObject(dto, CreateUserDto)).toThrow(ValidationError);
122
+ });
123
+
124
+ test('should throw ValidationError for invalid age', () => {
125
+ const dto = {
126
+ name: 'John',
127
+ email: 'john@example.com',
128
+ age: -1,
129
+ };
130
+
131
+ expect(() => validateObject(dto, CreateUserDto)).toThrow(ValidationError);
132
+ });
133
+
134
+ test('should collect all errors', () => {
135
+ const dto = {
136
+ name: 'A',
137
+ email: 'invalid',
138
+ age: -1,
139
+ };
140
+
141
+ try {
142
+ validateObject(dto, CreateUserDto);
143
+ expect(true).toBe(false); // Should not reach here
144
+ } catch (error) {
145
+ expect(error).toBeInstanceOf(ValidationError);
146
+ const validationError = error as ValidationError;
147
+ expect(validationError.issues.length).toBeGreaterThanOrEqual(3);
148
+ }
149
+ });
150
+
151
+ test('should stop at first error when stopAtFirstError is true', () => {
152
+ const dto = {
153
+ name: 'A',
154
+ email: 'invalid',
155
+ age: -1,
156
+ };
157
+
158
+ try {
159
+ validateObject(dto, CreateUserDto, { stopAtFirstError: true });
160
+ expect(true).toBe(false);
161
+ } catch (error) {
162
+ expect(error).toBeInstanceOf(ValidationError);
163
+ const validationError = error as ValidationError;
164
+ expect(validationError.issues.length).toBe(1);
165
+ }
166
+ });
167
+ });
168
+
169
+ describe('validateObjectSync', () => {
170
+ @ValidateClass()
171
+ class SimpleDto {
172
+ @Property(IsString())
173
+ public name: string = '';
174
+ }
175
+
176
+ test('should return valid: true for valid object', () => {
177
+ const result = validateObjectSync({ name: 'test' }, SimpleDto);
178
+ expect(result.valid).toBe(true);
179
+ expect(result.issues.length).toBe(0);
180
+ });
181
+
182
+ test('should return valid: false with issues for invalid object', () => {
183
+ const result = validateObjectSync({ name: 123 }, SimpleDto);
184
+ expect(result.valid).toBe(false);
185
+ expect(result.issues.length).toBeGreaterThan(0);
186
+ });
187
+ });
188
+
189
+ describe('Nested validation', () => {
190
+ @ValidateClass()
191
+ class AddressDto {
192
+ @Property(IsString(), IsNotEmpty())
193
+ public city: string = '';
194
+
195
+ @Property(IsString(), IsNotEmpty())
196
+ public street: string = '';
197
+ }
198
+
199
+ @ValidateClass()
200
+ class UserWithAddressDto {
201
+ @Property(IsString(), MinLength(2))
202
+ public name: string = '';
203
+
204
+ @NestedProperty(AddressDto)
205
+ public address: AddressDto = new AddressDto();
206
+ }
207
+
208
+ test('should validate nested object', () => {
209
+ const dto = {
210
+ name: 'John',
211
+ address: {
212
+ city: 'Beijing',
213
+ street: 'Main St',
214
+ },
215
+ };
216
+
217
+ expect(() => validateObject(dto, UserWithAddressDto)).not.toThrow();
218
+ });
219
+
220
+ test('should fail for invalid nested object', () => {
221
+ const dto = {
222
+ name: 'John',
223
+ address: {
224
+ city: '',
225
+ street: 'Main St',
226
+ },
227
+ };
228
+
229
+ expect(() => validateObject(dto, UserWithAddressDto)).toThrow(ValidationError);
230
+ });
231
+
232
+ test('should include property path in error', () => {
233
+ const dto = {
234
+ name: 'John',
235
+ address: {
236
+ city: '',
237
+ street: '',
238
+ },
239
+ };
240
+
241
+ try {
242
+ validateObject(dto, UserWithAddressDto);
243
+ expect(true).toBe(false);
244
+ } catch (error) {
245
+ const validationError = error as ValidationError;
246
+ const cityError = validationError.issues.find((i) => i.property?.includes('city'));
247
+ expect(cityError).toBeDefined();
248
+ }
249
+ });
250
+ });
251
+
252
+ describe('Array nested validation', () => {
253
+ @ValidateClass()
254
+ class ItemDto {
255
+ @Property(IsString(), IsNotEmpty())
256
+ public name: string = '';
257
+
258
+ @Property(IsNumber(), Min(0))
259
+ public price: number = 0;
260
+ }
261
+
262
+ @ValidateClass()
263
+ class OrderDto {
264
+ @Property(IsString())
265
+ public orderId: string = '';
266
+
267
+ @Property(IsArray(), ArrayMinSize(1))
268
+ @ArrayNestedProperty(ItemDto)
269
+ public items: ItemDto[] = [];
270
+ }
271
+
272
+ test('should validate array of nested objects', () => {
273
+ const dto = {
274
+ orderId: '123',
275
+ items: [
276
+ { name: 'Item 1', price: 10 },
277
+ { name: 'Item 2', price: 20 },
278
+ ],
279
+ };
280
+
281
+ expect(() => validateObject(dto, OrderDto)).not.toThrow();
282
+ });
283
+
284
+ test('should fail for invalid item in array', () => {
285
+ const dto = {
286
+ orderId: '123',
287
+ items: [
288
+ { name: 'Item 1', price: 10 },
289
+ { name: '', price: -5 },
290
+ ],
291
+ };
292
+
293
+ expect(() => validateObject(dto, OrderDto)).toThrow(ValidationError);
294
+ });
295
+
296
+ test('should include array index in error path', () => {
297
+ const dto = {
298
+ orderId: '123',
299
+ items: [
300
+ { name: 'Item 1', price: 10 },
301
+ { name: '', price: 20 },
302
+ ],
303
+ };
304
+
305
+ try {
306
+ validateObject(dto, OrderDto);
307
+ expect(true).toBe(false);
308
+ } catch (error) {
309
+ const validationError = error as ValidationError;
310
+ const itemError = validationError.issues.find((i) => i.property?.includes('[1]'));
311
+ expect(itemError).toBeDefined();
312
+ }
313
+ });
314
+ });
315
+
316
+ describe('ValidationError methods', () => {
317
+ test('getFlattened should flatten nested errors', () => {
318
+ const issues = [
319
+ {
320
+ property: 'user',
321
+ rule: 'validateNested',
322
+ message: 'Nested validation failed',
323
+ children: [
324
+ { property: 'name', rule: 'isString', message: 'Must be string' },
325
+ { property: 'email', rule: 'isEmail', message: 'Must be email' },
326
+ ],
327
+ },
328
+ ];
329
+
330
+ const error = new ValidationError('Validation failed', issues);
331
+ const flattened = error.getFlattened();
332
+
333
+ expect(flattened.length).toBe(2);
334
+ expect(flattened[0].property).toBe('user.name');
335
+ expect(flattened[1].property).toBe('user.email');
336
+ });
337
+
338
+ test('toJSON should return serializable object', () => {
339
+ const error = new ValidationError('Test error', [
340
+ { property: 'name', rule: 'isString', message: 'Must be string' },
341
+ ]);
342
+
343
+ const json = error.toJSON();
344
+ expect(json.name).toBe('ValidationError');
345
+ expect(json.message).toBe('Test error');
346
+ expect(json.issues).toBeDefined();
347
+ });
348
+ });
349
+ });
@@ -0,0 +1,335 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+
3
+ import {
4
+ createCustomValidator,
5
+ createSimpleValidator,
6
+ createRegexValidator,
7
+ // 内置扩展验证器
8
+ IsPhoneNumber,
9
+ IsIdCard,
10
+ IsIPv4,
11
+ IsPort,
12
+ IsPostalCode,
13
+ IsCreditCard,
14
+ IsHexColor,
15
+ IsMacAddress,
16
+ IsSemVer,
17
+ IsDivisibleBy,
18
+ IsBetween,
19
+ Contains,
20
+ NotContains,
21
+ } from '../../src/validation';
22
+
23
+ describe('Custom Validator Factory', () => {
24
+ describe('createCustomValidator', () => {
25
+ test('should create validator with arguments', () => {
26
+ const IsGreaterThan = createCustomValidator(
27
+ 'isGreaterThan',
28
+ (value: unknown, min: number) => typeof value === 'number' && value > min,
29
+ (min: number) => `必须大于 ${min}`,
30
+ );
31
+
32
+ const rule = IsGreaterThan(10)();
33
+ expect(rule.name).toBe('isGreaterThan');
34
+ expect(rule.message).toBe('必须大于 10');
35
+ expect(rule.validate(11)).toBe(true);
36
+ expect(rule.validate(10)).toBe(false);
37
+ expect(rule.validate(5)).toBe(false);
38
+ });
39
+
40
+ test('should support custom message', () => {
41
+ const IsGreaterThan = createCustomValidator(
42
+ 'isGreaterThan',
43
+ (value: unknown, min: number) => typeof value === 'number' && value > min,
44
+ (min: number) => `必须大于 ${min}`,
45
+ );
46
+
47
+ const rule = IsGreaterThan(5)({ message: 'Custom error message' });
48
+ expect(rule.message).toBe('Custom error message');
49
+ });
50
+
51
+ test('should support static message', () => {
52
+ const IsPositiveNumber = createCustomValidator(
53
+ 'isPositiveNumber',
54
+ (value: unknown) => typeof value === 'number' && value > 0,
55
+ '必须是正数',
56
+ );
57
+
58
+ const rule = IsPositiveNumber()();
59
+ expect(rule.message).toBe('必须是正数');
60
+ });
61
+ });
62
+
63
+ describe('createSimpleValidator', () => {
64
+ test('should create simple validator without arguments', () => {
65
+ const IsEven = createSimpleValidator(
66
+ 'isEven',
67
+ (value) => typeof value === 'number' && value % 2 === 0,
68
+ '必须是偶数',
69
+ );
70
+
71
+ const rule = IsEven();
72
+ expect(rule.name).toBe('isEven');
73
+ expect(rule.message).toBe('必须是偶数');
74
+ expect(rule.validate(2)).toBe(true);
75
+ expect(rule.validate(4)).toBe(true);
76
+ expect(rule.validate(3)).toBe(false);
77
+ });
78
+
79
+ test('should support custom message', () => {
80
+ const IsEven = createSimpleValidator(
81
+ 'isEven',
82
+ (value) => typeof value === 'number' && value % 2 === 0,
83
+ '必须是偶数',
84
+ );
85
+
86
+ const rule = IsEven({ message: 'Value must be even' });
87
+ expect(rule.message).toBe('Value must be even');
88
+ });
89
+ });
90
+
91
+ describe('createRegexValidator', () => {
92
+ test('should create regex-based validator', () => {
93
+ const IsSlug = createRegexValidator(
94
+ 'isSlug',
95
+ /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
96
+ '必须是有效的 slug 格式',
97
+ );
98
+
99
+ const rule = IsSlug();
100
+ expect(rule.name).toBe('isSlug');
101
+ expect(rule.validate('hello-world')).toBe(true);
102
+ expect(rule.validate('test123')).toBe(true);
103
+ expect(rule.validate('Hello-World')).toBe(false);
104
+ expect(rule.validate('hello_world')).toBe(false);
105
+ });
106
+ });
107
+ });
108
+
109
+ describe('Built-in Extended Validators', () => {
110
+ describe('IsPhoneNumber', () => {
111
+ const rule = IsPhoneNumber();
112
+
113
+ test('should pass for valid Chinese phone numbers', () => {
114
+ expect(rule.validate('13812345678')).toBe(true);
115
+ expect(rule.validate('15912345678')).toBe(true);
116
+ expect(rule.validate('18612345678')).toBe(true);
117
+ });
118
+
119
+ test('should fail for invalid phone numbers', () => {
120
+ expect(rule.validate('12345678901')).toBe(false);
121
+ expect(rule.validate('1381234567')).toBe(false);
122
+ expect(rule.validate('138123456789')).toBe(false);
123
+ expect(rule.validate('abc12345678')).toBe(false);
124
+ });
125
+ });
126
+
127
+ describe('IsIdCard', () => {
128
+ const rule = IsIdCard();
129
+
130
+ test('should pass for valid ID cards', () => {
131
+ // 18位身份证
132
+ expect(rule.validate('110101199003078534')).toBe(true);
133
+ // 15位身份证
134
+ expect(rule.validate('110101900307853')).toBe(true);
135
+ });
136
+
137
+ test('should fail for invalid ID cards', () => {
138
+ expect(rule.validate('12345678901234567')).toBe(false);
139
+ expect(rule.validate('abc')).toBe(false);
140
+ });
141
+ });
142
+
143
+ describe('IsIPv4', () => {
144
+ const rule = IsIPv4();
145
+
146
+ test('should pass for valid IPv4 addresses', () => {
147
+ expect(rule.validate('192.168.1.1')).toBe(true);
148
+ expect(rule.validate('0.0.0.0')).toBe(true);
149
+ expect(rule.validate('255.255.255.255')).toBe(true);
150
+ });
151
+
152
+ test('should fail for invalid IPv4 addresses', () => {
153
+ expect(rule.validate('256.1.1.1')).toBe(false);
154
+ expect(rule.validate('192.168.1')).toBe(false);
155
+ expect(rule.validate('192.168.1.1.1')).toBe(false);
156
+ expect(rule.validate('192.168.1.01')).toBe(false); // Leading zero
157
+ });
158
+ });
159
+
160
+ describe('IsPort', () => {
161
+ const rule = IsPort();
162
+
163
+ test('should pass for valid ports', () => {
164
+ expect(rule.validate(0)).toBe(true);
165
+ expect(rule.validate(80)).toBe(true);
166
+ expect(rule.validate(443)).toBe(true);
167
+ expect(rule.validate(65535)).toBe(true);
168
+ expect(rule.validate('8080')).toBe(true);
169
+ });
170
+
171
+ test('should fail for invalid ports', () => {
172
+ expect(rule.validate(-1)).toBe(false);
173
+ expect(rule.validate(65536)).toBe(false);
174
+ expect(rule.validate('abc')).toBe(false);
175
+ });
176
+ });
177
+
178
+ describe('IsPostalCode', () => {
179
+ const rule = IsPostalCode();
180
+
181
+ test('should pass for valid Chinese postal codes', () => {
182
+ expect(rule.validate('100000')).toBe(true);
183
+ expect(rule.validate('518000')).toBe(true);
184
+ });
185
+
186
+ test('should fail for invalid postal codes', () => {
187
+ expect(rule.validate('010000')).toBe(false); // Starts with 0
188
+ expect(rule.validate('1000000')).toBe(false);
189
+ expect(rule.validate('12345')).toBe(false);
190
+ });
191
+ });
192
+
193
+ describe('IsCreditCard', () => {
194
+ const rule = IsCreditCard();
195
+
196
+ test('should pass for valid credit card numbers (Luhn check)', () => {
197
+ expect(rule.validate('4532015112830366')).toBe(true);
198
+ expect(rule.validate('4532-0151-1283-0366')).toBe(true);
199
+ expect(rule.validate('4532 0151 1283 0366')).toBe(true);
200
+ });
201
+
202
+ test('should fail for invalid credit card numbers', () => {
203
+ expect(rule.validate('1234567890123456')).toBe(false);
204
+ expect(rule.validate('abc')).toBe(false);
205
+ });
206
+ });
207
+
208
+ describe('IsHexColor', () => {
209
+ const rule = IsHexColor();
210
+
211
+ test('should pass for valid hex colors', () => {
212
+ expect(rule.validate('#fff')).toBe(true);
213
+ expect(rule.validate('#FFF')).toBe(true);
214
+ expect(rule.validate('#ffffff')).toBe(true);
215
+ expect(rule.validate('#FFFFFF')).toBe(true);
216
+ });
217
+
218
+ test('should fail for invalid hex colors', () => {
219
+ expect(rule.validate('fff')).toBe(false);
220
+ expect(rule.validate('#ffff')).toBe(false);
221
+ expect(rule.validate('#ggg')).toBe(false);
222
+ });
223
+ });
224
+
225
+ describe('IsMacAddress', () => {
226
+ const rule = IsMacAddress();
227
+
228
+ test('should pass for valid MAC addresses', () => {
229
+ expect(rule.validate('00:1A:2B:3C:4D:5E')).toBe(true);
230
+ expect(rule.validate('00-1A-2B-3C-4D-5E')).toBe(true);
231
+ expect(rule.validate('00:1a:2b:3c:4d:5e')).toBe(true);
232
+ });
233
+
234
+ test('should fail for invalid MAC addresses', () => {
235
+ expect(rule.validate('00:1A:2B:3C:4D')).toBe(false);
236
+ expect(rule.validate('00:1A:2B:3C:4D:5E:6F')).toBe(false);
237
+ expect(rule.validate('invalid')).toBe(false);
238
+ });
239
+ });
240
+
241
+ describe('IsSemVer', () => {
242
+ const rule = IsSemVer();
243
+
244
+ test('should pass for valid semantic versions', () => {
245
+ expect(rule.validate('1.0.0')).toBe(true);
246
+ expect(rule.validate('0.0.1')).toBe(true);
247
+ expect(rule.validate('1.0.0-alpha')).toBe(true);
248
+ expect(rule.validate('1.0.0-alpha.1')).toBe(true);
249
+ expect(rule.validate('1.0.0+build')).toBe(true);
250
+ expect(rule.validate('1.0.0-alpha+build')).toBe(true);
251
+ });
252
+
253
+ test('should fail for invalid semantic versions', () => {
254
+ expect(rule.validate('1.0')).toBe(false);
255
+ expect(rule.validate('1')).toBe(false);
256
+ expect(rule.validate('v1.0.0')).toBe(false);
257
+ expect(rule.validate('1.0.0.0')).toBe(false);
258
+ });
259
+ });
260
+
261
+ describe('IsDivisibleBy', () => {
262
+ test('should pass when divisible', () => {
263
+ const rule = IsDivisibleBy(5)();
264
+ expect(rule.validate(10)).toBe(true);
265
+ expect(rule.validate(15)).toBe(true);
266
+ expect(rule.validate(0)).toBe(true);
267
+ });
268
+
269
+ test('should fail when not divisible', () => {
270
+ const rule = IsDivisibleBy(5)();
271
+ expect(rule.validate(7)).toBe(false);
272
+ expect(rule.validate(12)).toBe(false);
273
+ });
274
+
275
+ test('should have correct message', () => {
276
+ const rule = IsDivisibleBy(3)();
277
+ expect(rule.message).toBe('必须能被 3 整除');
278
+ });
279
+ });
280
+
281
+ describe('IsBetween', () => {
282
+ test('should pass for values in range', () => {
283
+ const rule = IsBetween(1, 10)();
284
+ expect(rule.validate(1)).toBe(true);
285
+ expect(rule.validate(5)).toBe(true);
286
+ expect(rule.validate(10)).toBe(true);
287
+ });
288
+
289
+ test('should fail for values out of range', () => {
290
+ const rule = IsBetween(1, 10)();
291
+ expect(rule.validate(0)).toBe(false);
292
+ expect(rule.validate(11)).toBe(false);
293
+ });
294
+
295
+ test('should have correct message', () => {
296
+ const rule = IsBetween(1, 100)();
297
+ expect(rule.message).toBe('必须在 1 和 100 之间');
298
+ });
299
+ });
300
+
301
+ describe('Contains', () => {
302
+ test('should pass when string contains substring', () => {
303
+ const rule = Contains('hello')();
304
+ expect(rule.validate('hello world')).toBe(true);
305
+ expect(rule.validate('say hello')).toBe(true);
306
+ });
307
+
308
+ test('should fail when string does not contain substring', () => {
309
+ const rule = Contains('hello')();
310
+ expect(rule.validate('hi world')).toBe(false);
311
+ });
312
+
313
+ test('should have correct message', () => {
314
+ const rule = Contains('test')();
315
+ expect(rule.message).toBe('必须包含 "test"');
316
+ });
317
+ });
318
+
319
+ describe('NotContains', () => {
320
+ test('should pass when string does not contain substring', () => {
321
+ const rule = NotContains('bad')();
322
+ expect(rule.validate('good word')).toBe(true);
323
+ });
324
+
325
+ test('should fail when string contains substring', () => {
326
+ const rule = NotContains('bad')();
327
+ expect(rule.validate('bad word')).toBe(false);
328
+ });
329
+
330
+ test('should have correct message', () => {
331
+ const rule = NotContains('spam')();
332
+ expect(rule.message).toBe('不能包含 "spam"');
333
+ });
334
+ });
335
+ });