@hy_ong/zod-kit 0.0.4 → 0.0.6

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 (59) hide show
  1. package/.claude/settings.local.json +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +465 -97
  4. package/debug.js +21 -0
  5. package/debug.ts +16 -0
  6. package/dist/index.cjs +3127 -146
  7. package/dist/index.d.cts +3021 -25
  8. package/dist/index.d.ts +3021 -25
  9. package/dist/index.js +3081 -144
  10. package/eslint.config.mts +8 -0
  11. package/package.json +10 -9
  12. package/src/config.ts +1 -1
  13. package/src/i18n/locales/en.json +161 -25
  14. package/src/i18n/locales/zh-TW.json +165 -26
  15. package/src/index.ts +17 -7
  16. package/src/validators/common/boolean.ts +191 -0
  17. package/src/validators/common/date.ts +299 -0
  18. package/src/validators/common/datetime.ts +673 -0
  19. package/src/validators/common/email.ts +313 -0
  20. package/src/validators/common/file.ts +384 -0
  21. package/src/validators/common/id.ts +471 -0
  22. package/src/validators/common/number.ts +319 -0
  23. package/src/validators/common/password.ts +386 -0
  24. package/src/validators/common/text.ts +271 -0
  25. package/src/validators/common/time.ts +600 -0
  26. package/src/validators/common/url.ts +347 -0
  27. package/src/validators/taiwan/business-id.ts +262 -0
  28. package/src/validators/taiwan/fax.ts +327 -0
  29. package/src/validators/taiwan/mobile.ts +242 -0
  30. package/src/validators/taiwan/national-id.ts +425 -0
  31. package/src/validators/taiwan/postal-code.ts +1049 -0
  32. package/src/validators/taiwan/tel.ts +330 -0
  33. package/tests/common/boolean.test.ts +340 -92
  34. package/tests/common/date.test.ts +458 -0
  35. package/tests/common/datetime.test.ts +693 -0
  36. package/tests/common/email.test.ts +232 -60
  37. package/tests/common/file.test.ts +479 -0
  38. package/tests/common/id.test.ts +535 -0
  39. package/tests/common/number.test.ts +230 -60
  40. package/tests/common/password.test.ts +271 -44
  41. package/tests/common/text.test.ts +210 -13
  42. package/tests/common/time.test.ts +528 -0
  43. package/tests/common/url.test.ts +492 -67
  44. package/tests/taiwan/business-id.test.ts +240 -0
  45. package/tests/taiwan/fax.test.ts +463 -0
  46. package/tests/taiwan/mobile.test.ts +373 -0
  47. package/tests/taiwan/national-id.test.ts +435 -0
  48. package/tests/taiwan/postal-code.test.ts +705 -0
  49. package/tests/taiwan/tel.test.ts +467 -0
  50. package/eslint.config.mjs +0 -10
  51. package/src/common/boolean.ts +0 -36
  52. package/src/common/date.ts +0 -43
  53. package/src/common/email.ts +0 -44
  54. package/src/common/integer.ts +0 -46
  55. package/src/common/number.ts +0 -37
  56. package/src/common/password.ts +0 -33
  57. package/src/common/text.ts +0 -34
  58. package/src/common/url.ts +0 -37
  59. package/tests/common/integer.test.ts +0 -90
@@ -1,77 +1,247 @@
1
1
  import { describe, it, expect, beforeEach } from "vitest"
2
- import { setLocale, number, Locale } from "../../src"
3
-
4
- const locales = [
5
- {
6
- locale: "en",
7
- messages: {
8
- required: "Required",
9
- min: "Must be at least 5",
10
- max: "Must be at most 10",
11
- },
12
- },
13
- {
14
- locale: "zh-TW",
15
- messages: {
16
- required: "必填",
17
- min: "最小值 5",
18
- max: "最大值 10",
19
- },
20
- },
21
- ] as const
22
-
23
- describe.each(locales)("number() locale: $locale", ({ locale, messages }) => {
24
- beforeEach(() => setLocale(locale as Locale))
25
-
26
- it("should pass with valid number", () => {
27
- const schema = number()
28
- expect(schema.parse(42)).toBe(42)
2
+ import { number, setLocale } from "../../src"
3
+
4
+ describe("number() features", () => {
5
+ beforeEach(() => setLocale("en"))
6
+
7
+ describe("type validation", () => {
8
+ it("should accept integers when type='integer'", () => {
9
+ const schema = number({ type: "integer" })
10
+ expect(schema.parse(42)).toBe(42)
11
+ expect(schema.parse("123")).toBe(123)
12
+ })
13
+
14
+ it("should reject floats when type='integer'", () => {
15
+ const schema = number({ type: "integer" })
16
+ expect(() => schema.parse(3.14)).toThrow("Must be an integer")
17
+ expect(() => schema.parse("3.14")).toThrow("Must be an integer")
18
+ })
19
+
20
+ it("should accept floats when type='float'", () => {
21
+ const schema = number({ type: "float" })
22
+ expect(schema.parse(3.14)).toBe(3.14)
23
+ expect(schema.parse("3.14")).toBe(3.14)
24
+ })
25
+
26
+ it("should reject integers when type='float'", () => {
27
+ const schema = number({ type: "float" })
28
+ expect(() => schema.parse(42)).toThrow("Must be a decimal number")
29
+ expect(() => schema.parse("42")).toThrow("Must be a decimal number")
30
+ })
31
+
32
+ it("should accept both when type='both' (default)", () => {
33
+ const schema = number({ type: "both" })
34
+ expect(schema.parse(42)).toBe(42)
35
+ expect(schema.parse(3.14)).toBe(3.14)
36
+ })
29
37
  })
30
38
 
31
- it("should parse string number", () => {
32
- const schema = number()
33
- expect(schema.parse("123")).toBe(123)
39
+ describe("sign validation", () => {
40
+ it("should enforce positive numbers", () => {
41
+ const schema = number({ positive: true })
42
+ expect(schema.parse(5)).toBe(5)
43
+ expect(() => schema.parse(0)).toThrow("Must be positive")
44
+ expect(() => schema.parse(-5)).toThrow("Must be positive")
45
+ })
46
+
47
+ it("should enforce negative numbers", () => {
48
+ const schema = number({ negative: true })
49
+ expect(schema.parse(-5)).toBe(-5)
50
+ expect(() => schema.parse(0)).toThrow("Must be negative")
51
+ expect(() => schema.parse(5)).toThrow("Must be negative")
52
+ })
53
+
54
+ it("should enforce non-negative numbers", () => {
55
+ const schema = number({ nonNegative: true })
56
+ expect(schema.parse(0)).toBe(0)
57
+ expect(schema.parse(5)).toBe(5)
58
+ expect(() => schema.parse(-5)).toThrow("Must be non-negative")
59
+ })
60
+
61
+ it("should enforce non-positive numbers", () => {
62
+ const schema = number({ nonPositive: true })
63
+ expect(schema.parse(0)).toBe(0)
64
+ expect(schema.parse(-5)).toBe(-5)
65
+ expect(() => schema.parse(5)).toThrow("Must be non-positive")
66
+ })
34
67
  })
35
68
 
36
- it("should fail for empty string when required", () => {
37
- const schema = number({ required: true })
38
- expect(() => schema.parse("")).toThrow(messages.required)
39
- expect(() => schema.parse(null)).toThrow(messages.required)
40
- expect(() => schema.parse(undefined)).toThrow(messages.required)
69
+ describe("multipleOf validation", () => {
70
+ it("should enforce multiple of constraint", () => {
71
+ const schema = number({ multipleOf: 5 })
72
+ expect(schema.parse(0)).toBe(0)
73
+ expect(schema.parse(5)).toBe(5)
74
+ expect(schema.parse(10)).toBe(10)
75
+ expect(schema.parse(-5)).toBe(-5)
76
+ expect(() => schema.parse(3)).toThrow("Must be a multiple of 5")
77
+ expect(() => schema.parse(7)).toThrow("Must be a multiple of 5")
78
+ })
41
79
  })
42
80
 
43
- it("should allow null if not required", () => {
44
- const schema = number({ required: false })
45
- expect(schema.parse("")).toBe(null)
46
- expect(schema.parse(null)).toBe(null)
47
- expect(schema.parse(undefined)).toBe(null)
81
+ describe("precision validation", () => {
82
+ it("should enforce decimal precision", () => {
83
+ const schema = number({ precision: 2 })
84
+ expect(schema.parse(3.14)).toBe(3.14)
85
+ expect(schema.parse(3.1)).toBe(3.1)
86
+ expect(schema.parse(3)).toBe(3)
87
+ expect(() => schema.parse(3.141)).toThrow("Must have at most 2 decimal places")
88
+ expect(() => schema.parse(3.1234)).toThrow("Must have at most 2 decimal places")
89
+ })
90
+ })
91
+
92
+ describe("finite validation", () => {
93
+ it("should reject infinite values by default", () => {
94
+ const schema = number()
95
+ expect(() => schema.parse(Infinity)).toThrow("Must be a finite number")
96
+ expect(() => schema.parse(-Infinity)).toThrow("Must be a finite number")
97
+ })
98
+
99
+ it("should allow infinite values when finite=false", () => {
100
+ const schema = number({ finite: false })
101
+ expect(schema.parse(Infinity)).toBe(Infinity)
102
+ expect(schema.parse(-Infinity)).toBe(-Infinity)
103
+ })
48
104
  })
49
105
 
50
- it("should apply default value with not required", () => {
51
- const schema = number({ required: false, defaultValue: 42 })
52
- expect(schema.parse("")).toBe(42)
53
- expect(schema.parse(null)).toBe(42)
54
- expect(schema.parse(undefined)).toBe(42)
106
+ describe("parseCommas functionality", () => {
107
+ it("should parse comma-separated numbers", () => {
108
+ const schema = number({ parseCommas: true })
109
+ expect(schema.parse("1,234")).toBe(1234)
110
+ expect(schema.parse("1,234.56")).toBe(1234.56)
111
+ expect(schema.parse("12,345,678")).toBe(12345678)
112
+ })
113
+
114
+ it("should not parse commas by default", () => {
115
+ const schema = number()
116
+ expect(() => schema.parse("1,234")).toThrow("Must be a valid number")
117
+ })
55
118
  })
56
119
 
57
- it("should apply default value with required", () => {
58
- const schema = number({ required: true, defaultValue: 42 })
59
- expect(schema.parse("")).toBe(42)
60
- expect(schema.parse(null)).toBe(42)
61
- expect(schema.parse(undefined)).toBe(42)
120
+ describe("transform functionality", () => {
121
+ it("should apply transform function", () => {
122
+ const schema = number({
123
+ transform: (val) => Math.round(val * 100) / 100, // Round to 2 decimals
124
+ })
125
+ expect(schema.parse(3.14159)).toBe(3.14)
126
+ expect(schema.parse("2.71828")).toBe(2.72)
127
+ })
62
128
  })
63
129
 
64
- it("should enforce min", () => {
65
- const schema = number({ min: 5 })
66
- expect(schema.parse(5)).toBe(5)
67
- expect(() => schema.parse(2)).toThrow(messages.min)
68
- expect(() => schema.parse("2")).toThrow(messages.min)
130
+ describe("custom i18n messages", () => {
131
+ it("should use custom messages when provided", () => {
132
+ const schema = number({
133
+ type: "integer",
134
+ min: 0,
135
+ i18n: {
136
+ en: {
137
+ required: "Custom required message",
138
+ integer: "Custom integer message",
139
+ min: "Custom min: at least ${min}",
140
+ },
141
+ "zh-TW": {
142
+ required: "客製化必填訊息",
143
+ integer: "客製化整數訊息",
144
+ min: "客製化最小值: 至少 ${min}",
145
+ },
146
+ },
147
+ })
148
+
149
+ expect(() => schema.parse("")).toThrow("Custom required message")
150
+ expect(() => schema.parse(3.14)).toThrow("Custom integer message")
151
+ expect(() => schema.parse(-1)).toThrow("Custom min: at least 0")
152
+ })
153
+
154
+ it("should fallback to default messages when custom not provided", () => {
155
+ const schema = number({
156
+ type: "integer",
157
+ max: 10,
158
+ i18n: {
159
+ en: {
160
+ required: "Custom required message",
161
+ },
162
+ "zh-TW": {
163
+ required: "客製化必填訊息",
164
+ },
165
+ },
166
+ })
167
+
168
+ expect(() => schema.parse("")).toThrow("Custom required message")
169
+ expect(() => schema.parse(3.14)).toThrow("Must be an integer")
170
+ expect(() => schema.parse(15)).toThrow("Must be at most 10")
171
+ })
172
+
173
+ it("should use correct locale for custom messages", () => {
174
+ setLocale("en")
175
+ const schemaEn = number({
176
+ type: "integer",
177
+ i18n: {
178
+ en: {
179
+ integer: "English integer message",
180
+ },
181
+ "zh-TW": {
182
+ integer: "繁體中文整數訊息",
183
+ },
184
+ },
185
+ })
186
+ expect(() => schemaEn.parse(3.14)).toThrow("English integer message")
187
+
188
+ setLocale("zh-TW")
189
+ const schemaZh = number({
190
+ type: "integer",
191
+ i18n: {
192
+ en: {
193
+ integer: "English integer message",
194
+ },
195
+ "zh-TW": {
196
+ integer: "繁體中文整數訊息",
197
+ },
198
+ },
199
+ })
200
+ expect(() => schemaZh.parse(3.14)).toThrow("繁體中文整數訊息")
201
+ })
69
202
  })
70
203
 
71
- it("should enforce max", () => {
72
- const schema = number({ max: 10 })
73
- expect(schema.parse(10)).toBe(10)
74
- expect(() => schema.parse(11)).toThrow(messages.max)
75
- expect(() => schema.parse("11")).toThrow(messages.max)
204
+ describe("complex scenarios", () => {
205
+ it("should work with multiple validations", () => {
206
+ const schema = number({
207
+ type: "integer",
208
+ min: 1,
209
+ max: 100,
210
+ multipleOf: 5,
211
+ positive: true,
212
+ })
213
+
214
+ expect(schema.parse(5)).toBe(5)
215
+ expect(schema.parse(10)).toBe(10)
216
+ expect(schema.parse(95)).toBe(95)
217
+
218
+ expect(() => schema.parse(0)).toThrow("Must be positive")
219
+ expect(() => schema.parse(3)).toThrow("Must be a multiple of 5")
220
+ expect(() => schema.parse(105)).toThrow("Must be at most 100")
221
+ expect(() => schema.parse(5.5)).toThrow("Must be an integer")
222
+ })
223
+
224
+ it("should handle null values correctly when not required", () => {
225
+ const schema = number({
226
+ required: false,
227
+ type: "integer",
228
+ min: 0,
229
+ })
230
+
231
+ expect(schema.parse(null)).toBe(null)
232
+ expect(schema.parse("")).toBe(null)
233
+ expect(schema.parse(undefined)).toBe(null)
234
+ expect(schema.parse(5)).toBe(5)
235
+ })
236
+
237
+ it("should work with parseCommas and transform together", () => {
238
+ const schema = number({
239
+ parseCommas: true,
240
+ transform: (val) => Math.floor(val / 100) * 100, // Round down to nearest 100
241
+ })
242
+
243
+ expect(schema.parse("1,234")).toBe(1200)
244
+ expect(schema.parse("5,678.90")).toBe(5600)
245
+ })
76
246
  })
77
247
  })
@@ -6,24 +6,38 @@ const locales = [
6
6
  locale: "en",
7
7
  messages: {
8
8
  required: "Required",
9
- min: "Must be at least 5 characters",
10
- max: "Must be at most 5 characters",
9
+ min: "Must be at least 8 characters",
10
+ max: "Must be at most 20 characters",
11
11
  uppercase: "Must include at least one uppercase letter",
12
12
  lowercase: "Must include at least one lowercase letter",
13
13
  digits: "Must include at least one digit",
14
14
  special: "Must include at least one special character",
15
+ noRepeating: "Must not contain repeating characters",
16
+ noSequential: "Must not contain sequential characters",
17
+ noCommonWords: "Must not contain common words or patterns",
18
+ minStrength: "Password strength must be at least strong",
19
+ excludes: "Must not contain test",
20
+ includes: "Must include @",
21
+ invalid: "Invalid password format",
15
22
  },
16
23
  },
17
24
  {
18
25
  locale: "zh-TW",
19
26
  messages: {
20
27
  required: "必填",
21
- min: "長度至少 5 字元",
22
- max: "長度最多 5 字元",
28
+ min: "長度至少 8 字元",
29
+ max: "長度最多 20 字元",
23
30
  uppercase: "必須包含至少一個大寫字母",
24
31
  lowercase: "必須包含至少一個小寫字母",
25
32
  digits: "必須包含至少一個數字",
26
33
  special: "必須包含至少一個特殊符號",
34
+ noRepeating: "不可包含重複字元",
35
+ noSequential: "不可包含連續字元",
36
+ noCommonWords: "不可包含常見密碼或模式",
37
+ minStrength: "密碼強度必須至少為 strong",
38
+ excludes: "不得包含「test」",
39
+ includes: "必須包含「@」",
40
+ invalid: "密碼格式錯誤",
27
41
  },
28
42
  },
29
43
  ] as const
@@ -31,59 +45,272 @@ const locales = [
31
45
  describe.each(locales)("password() locale: $locale", ({ locale, messages }) => {
32
46
  beforeEach(() => setLocale(locale as Locale))
33
47
 
34
- it("should pass with valid string", () => {
35
- const schema = password()
36
- expect(schema.parse("hello")).toBe("hello")
37
- })
48
+ describe("basic functionality", () => {
49
+ it("should pass with valid string", () => {
50
+ const schema = password()
51
+ expect(schema.parse("hello")).toBe("hello")
52
+ })
53
+
54
+ it("should fail with empty string when required", () => {
55
+ const schema = password()
56
+ expect(() => schema.parse("")).toThrow(messages.required)
57
+ expect(() => schema.parse(null)).toThrow(messages.required)
58
+ expect(() => schema.parse(undefined)).toThrow(messages.required)
59
+ })
60
+
61
+ it("should pass with null when not required", () => {
62
+ const schema = password({ required: false })
63
+ expect(schema.parse("")).toBe(null)
64
+ expect(schema.parse(null)).toBe(null)
65
+ expect(schema.parse(undefined)).toBe(null)
66
+ })
67
+
68
+ it("should handle default values", () => {
69
+ const schema = password({ required: false, defaultValue: "default123" })
70
+ expect(schema.parse("")).toBe("default123")
71
+ expect(schema.parse(null)).toBe("default123")
72
+ expect(schema.parse(undefined)).toBe("default123")
73
+ expect(schema.parse("actual")).toBe("actual")
74
+ })
38
75
 
39
- it("should fail with empty string when required", () => {
40
- const schema = password()
41
- expect(() => schema.parse("")).toThrow(messages.required)
42
- expect(() => schema.parse(null)).toThrow(messages.required)
43
- expect(() => schema.parse(undefined)).toThrow(messages.required)
76
+ it("should apply transform function", () => {
77
+ const schema = password({ transform: (val) => val.toLowerCase() })
78
+ expect(schema.parse("HELLO")).toBe("hello")
79
+ })
44
80
  })
45
81
 
46
- it("should pass with null when not required", () => {
47
- const schema = password({ required: false })
48
- expect(schema.parse("")).toBe(null)
49
- expect(schema.parse(null)).toBe(null)
50
- expect(schema.parse(undefined)).toBe(null)
82
+ describe("length validation", () => {
83
+ it("should fail with string shorter than min", () => {
84
+ const schema = password({ min: 8 })
85
+ expect(() => schema.parse("short")).toThrow(messages.min)
86
+ })
87
+
88
+ it("should fail with string longer than max", () => {
89
+ const schema = password({ max: 20 })
90
+ expect(() => schema.parse("this_is_a_very_long_password_that_exceeds_limit")).toThrow(messages.max)
91
+ })
92
+
93
+ it("should pass with valid length", () => {
94
+ const schema = password({ min: 5, max: 15 })
95
+ expect(schema.parse("validpassword")).toBe("validpassword")
96
+ })
51
97
  })
52
98
 
53
- it("should fail with string shorter than min", () => {
54
- const schema = password({ min: 5 })
55
- expect(() => schema.parse("hi")).toThrow(messages.min)
56
- expect(() => schema.parse("")).toThrow(messages.min)
57
- expect(() => schema.parse(null)).toThrow(messages.min)
58
- expect(() => schema.parse(undefined)).toThrow(messages.min)
99
+ describe("character requirements", () => {
100
+ it("should enforce uppercase requirement", () => {
101
+ const schema = password({ uppercase: true })
102
+ expect(() => schema.parse("lowercase1!")).toThrow(messages.uppercase)
103
+ expect(schema.parse("Hello1!")).toBe("Hello1!")
104
+ })
105
+
106
+ it("should enforce lowercase requirement", () => {
107
+ const schema = password({ lowercase: true })
108
+ expect(() => schema.parse("UPPERCASE1!")).toThrow(messages.lowercase)
109
+ expect(schema.parse("Test1!")).toBe("Test1!")
110
+ })
111
+
112
+ it("should enforce digit requirement", () => {
113
+ const schema = password({ digits: true })
114
+ expect(() => schema.parse("NoDigits!")).toThrow(messages.digits)
115
+ expect(schema.parse("With1!")).toBe("With1!")
116
+ })
117
+
118
+ it("should enforce special character requirement", () => {
119
+ const schema = password({ special: true })
120
+ expect(() => schema.parse("NoSpecial1")).toThrow(messages.special)
121
+ expect(schema.parse("Valid1!")).toBe("Valid1!")
122
+ })
123
+
124
+ it("should enforce multiple requirements", () => {
125
+ const schema = password({ uppercase: true, lowercase: true, digits: true, special: true })
126
+ expect(() => schema.parse("nouppercase1!")).toThrow(messages.uppercase)
127
+ expect(() => schema.parse("NOLOWERCASE1!")).toThrow(messages.lowercase)
128
+ expect(() => schema.parse("NoDigits!")).toThrow(messages.digits)
129
+ expect(() => schema.parse("NoSpecial1")).toThrow(messages.special)
130
+ expect(schema.parse("Valid1!")).toBe("Valid1!")
131
+ })
59
132
  })
60
133
 
61
- it("should fail with string longer than max", () => {
62
- const schema = password({ max: 5 })
63
- expect(() => schema.parse("hello world")).toThrow(messages.max)
134
+ describe("advanced security features", () => {
135
+ it("should enforce no repeating characters", () => {
136
+ const schema = password({ noRepeating: true })
137
+ expect(() => schema.parse("password111")).toThrow(messages.noRepeating)
138
+ expect(() => schema.parse("helllo")).toThrow(messages.noRepeating)
139
+ expect(schema.parse("password")).toBe("password")
140
+ })
141
+
142
+ it("should enforce no sequential characters", () => {
143
+ const schema = password({ noSequential: true })
144
+ expect(() => schema.parse("abc123")).toThrow(messages.noSequential)
145
+ expect(() => schema.parse("xyz789")).toThrow(messages.noSequential)
146
+ expect(() => schema.parse("password456")).toThrow(messages.noSequential)
147
+ expect(schema.parse("random135")).toBe("random135")
148
+ })
149
+
150
+ it("should enforce no common words", () => {
151
+ const schema = password({ noCommonWords: true })
152
+ expect(() => schema.parse("password123")).toThrow(messages.noCommonWords)
153
+ expect(() => schema.parse("admin")).toThrow(messages.noCommonWords)
154
+ expect(() => schema.parse("qwerty")).toThrow(messages.noCommonWords)
155
+ expect(schema.parse("randomtext")).toBe("randomtext")
156
+ })
157
+
158
+ it("should enforce minimum strength", () => {
159
+ const schema = password({ minStrength: "strong" })
160
+ expect(() => schema.parse("weak")).toThrow(messages.minStrength)
161
+ expect(() => schema.parse("123456")).toThrow(messages.minStrength)
162
+ expect(schema.parse("StrongP@ssw0rd")).toBe("StrongP@ssw0rd")
163
+ })
64
164
  })
65
165
 
66
- it("should enforce uppercase requirement", () => {
67
- const schema = password({ uppercase: true })
68
- expect(() => schema.parse("lowercase1!")).toThrow(messages.uppercase)
69
- expect(schema.parse("Hello1!")).toBe("Hello1!")
166
+ describe("content validation", () => {
167
+ it("should enforce includes requirement", () => {
168
+ const schema = password({ includes: "@" })
169
+ expect(() => schema.parse("password")).toThrow(messages.includes)
170
+ expect(schema.parse("user@pass")).toBe("user@pass")
171
+ })
172
+
173
+ it("should enforce excludes requirement with string", () => {
174
+ const schema = password({ excludes: "test" })
175
+ expect(() => schema.parse("testpassword")).toThrow(messages.excludes)
176
+ expect(schema.parse("password")).toBe("password")
177
+ })
178
+
179
+ it("should enforce excludes requirement with array", () => {
180
+ const schema = password({ excludes: ["test", "admin"] })
181
+ expect(() => schema.parse("testpassword")).toThrow()
182
+ expect(() => schema.parse("adminpass")).toThrow()
183
+ expect(schema.parse("password")).toBe("password")
184
+ })
185
+
186
+ it("should enforce regex requirement", () => {
187
+ const schema = password({ regex: /^[a-zA-Z0-9!@#$%^&*()]+$/ })
188
+ expect(() => schema.parse("invalid-password")).toThrow(messages.invalid)
189
+ expect(schema.parse("Valid123!")).toBe("Valid123!")
190
+ })
70
191
  })
71
192
 
72
- it("should enforce lowercase requirement", () => {
73
- const schema = password({ lowercase: true })
74
- expect(() => schema.parse("UPPERCASE1!")).toThrow(messages.lowercase)
75
- expect(schema.parse("Test1!")).toBe("Test1!")
193
+ describe("custom i18n messages", () => {
194
+ it("should use custom i18n messages when provided", () => {
195
+ const customMessages = {
196
+ en: {
197
+ required: "Custom required message",
198
+ min: "Custom min ${min} message",
199
+ uppercase: "Custom uppercase message",
200
+ },
201
+ "zh-TW": {
202
+ required: "自訂必填訊息",
203
+ min: "自訂最小長度 ${min} 訊息",
204
+ uppercase: "自訂大寫字母訊息",
205
+ },
206
+ }
207
+
208
+ const schema = password({
209
+ min: 8,
210
+ uppercase: true,
211
+ i18n: customMessages,
212
+ })
213
+
214
+ if (locale === "en") {
215
+ expect(() => schema.parse("")).toThrow("Custom required message")
216
+ expect(() => schema.parse("short")).toThrow("Custom min 8 message")
217
+ expect(() => schema.parse("nouppercase")).toThrow("Custom uppercase message")
218
+ } else {
219
+ expect(() => schema.parse("")).toThrow("自訂必填訊息")
220
+ expect(() => schema.parse("short")).toThrow("自訂最小長度 8 訊息")
221
+ expect(() => schema.parse("nouppercase")).toThrow("自訂大寫字母訊息")
222
+ }
223
+ })
224
+
225
+ it("should fallback to default i18n when custom message not provided", () => {
226
+ const customMessages = {
227
+ en: { required: "Custom required message" },
228
+ "zh-TW": { required: "自訂必填訊息" },
229
+ }
230
+
231
+ const schema = password({
232
+ uppercase: true,
233
+ i18n: customMessages,
234
+ })
235
+
236
+ if (locale === "en") {
237
+ expect(() => schema.parse("")).toThrow("Custom required message")
238
+ expect(() => schema.parse("nouppercase")).toThrow(messages.uppercase)
239
+ } else {
240
+ expect(() => schema.parse("")).toThrow("自訂必填訊息")
241
+ expect(() => schema.parse("nouppercase")).toThrow(messages.uppercase)
242
+ }
243
+ })
76
244
  })
77
245
 
78
- it("should enforce digit requirement", () => {
79
- const schema = password({ digits: true })
80
- expect(() => schema.parse("NoDigits!")).toThrow(messages.digits)
81
- expect(schema.parse("With1!")).toBe("With1!")
246
+ describe("complex scenarios", () => {
247
+ it("should handle all features combined", () => {
248
+ const schema = password({
249
+ min: 12,
250
+ max: 50,
251
+ uppercase: true,
252
+ lowercase: true,
253
+ digits: true,
254
+ special: true,
255
+ noRepeating: true,
256
+ noSequential: true,
257
+ noCommonWords: true,
258
+ minStrength: "strong",
259
+ includes: "@",
260
+ excludes: ["test", "admin"],
261
+ })
262
+
263
+ // Should fail for various reasons
264
+ expect(() => schema.parse("short")).toThrow()
265
+ expect(() => schema.parse("NoLowercase123!@")).toThrow()
266
+ expect(() => schema.parse("nouppercase123!@")).toThrow()
267
+ expect(() => schema.parse("NoDigits!@")).toThrow()
268
+ expect(() => schema.parse("NoSpecial123@")).toThrow()
269
+ expect(() => schema.parse("Repeating111!@")).toThrow()
270
+ expect(() => schema.parse("Sequential123!@")).toThrow()
271
+ expect(() => schema.parse("Password123!@")).toThrow()
272
+ expect(() => schema.parse("Complex123!")).toThrow() // Missing @
273
+ expect(() => schema.parse("TestComplex123!@")).toThrow() // Contains "test"
274
+
275
+ // Should pass
276
+ expect(schema.parse("C0mpl3x@P@ssw0rd")).toBe("C0mpl3x@P@ssw0rd")
277
+ })
278
+
279
+ it("should work with optional password and all features", () => {
280
+ const schema = password({
281
+ required: false,
282
+ min: 8,
283
+ uppercase: true,
284
+ lowercase: true,
285
+ digits: true,
286
+ special: true,
287
+ })
288
+
289
+ expect(schema.parse(null)).toBe(null)
290
+ expect(schema.parse("")).toBe(null)
291
+ expect(() => schema.parse("weak")).toThrow()
292
+ expect(schema.parse("Strong1!")).toBe("Strong1!")
293
+ })
82
294
  })
83
295
 
84
- it("should enforce special character requirement", () => {
85
- const schema = password({ special: true })
86
- expect(() => schema.parse("NoSpecial1")).toThrow(messages.special)
87
- expect(schema.parse("Valid1!")).toBe("Valid1!")
296
+ describe("password strength calculation", () => {
297
+ const testStrengthScenarios = [
298
+ { password: "weak", expectedToPass: false, minStrength: "medium" as const },
299
+ { password: "Medium1", expectedToPass: false, minStrength: "strong" as const },
300
+ { password: "Strong1!", expectedToPass: true, minStrength: "medium" as const },
301
+ { password: "VeryStrongP@ssw0rd123", expectedToPass: true, minStrength: "strong" as const },
302
+ ]
303
+
304
+ testStrengthScenarios.forEach(({ password: testPassword, expectedToPass, minStrength }) => {
305
+ it(`should ${expectedToPass ? "pass" : "fail"} for password "${testPassword}" with minStrength "${minStrength}"`, () => {
306
+ const schema = password({ minStrength })
307
+
308
+ if (expectedToPass) {
309
+ expect(schema.parse(testPassword)).toBe(testPassword)
310
+ } else {
311
+ expect(() => schema.parse(testPassword)).toThrow()
312
+ }
313
+ })
314
+ })
88
315
  })
89
- })
316
+ })