@hy_ong/zod-kit 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +23 -0
- package/LICENSE +21 -0
- package/README.md +7 -7
- package/debug.js +21 -0
- package/debug.ts +16 -0
- package/dist/index.cjs +1663 -189
- package/dist/index.d.cts +324 -32
- package/dist/index.d.ts +324 -32
- package/dist/index.js +1634 -187
- package/eslint.config.mts +8 -0
- package/package.json +10 -9
- package/src/config.ts +1 -1
- package/src/i18n/locales/en.json +123 -49
- package/src/i18n/locales/zh-TW.json +123 -46
- package/src/index.ts +13 -7
- package/src/validators/common/boolean.ts +97 -0
- package/src/validators/common/date.ts +171 -0
- package/src/validators/common/email.ts +200 -0
- package/src/validators/common/id.ts +259 -0
- package/src/validators/common/number.ts +194 -0
- package/src/validators/common/password.ts +214 -0
- package/src/validators/common/text.ts +151 -0
- package/src/validators/common/url.ts +207 -0
- package/src/validators/taiwan/business-id.ts +140 -0
- package/src/validators/taiwan/fax.ts +182 -0
- package/src/validators/taiwan/mobile.ts +110 -0
- package/src/validators/taiwan/national-id.ts +208 -0
- package/src/validators/taiwan/tel.ts +182 -0
- package/tests/common/boolean.test.ts +340 -92
- package/tests/common/date.test.ts +458 -0
- package/tests/common/email.test.ts +232 -60
- package/tests/common/id.test.ts +535 -0
- package/tests/common/number.test.ts +230 -60
- package/tests/common/password.test.ts +281 -54
- package/tests/common/text.test.ts +227 -30
- package/tests/common/url.test.ts +492 -67
- package/tests/taiwan/business-id.test.ts +240 -0
- package/tests/taiwan/fax.test.ts +463 -0
- package/tests/taiwan/mobile.test.ts +373 -0
- package/tests/taiwan/national-id.test.ts +435 -0
- package/tests/taiwan/tel.test.ts +467 -0
- package/eslint.config.mjs +0 -10
- package/src/common/boolean.ts +0 -37
- package/src/common/date.ts +0 -44
- package/src/common/email.ts +0 -45
- package/src/common/integer.ts +0 -47
- package/src/common/number.ts +0 -38
- package/src/common/password.ts +0 -34
- package/src/common/text.ts +0 -35
- package/src/common/url.ts +0 -38
- package/tests/common/integer.test.ts +0 -90
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest"
|
|
2
|
+
import { nationalId, setLocale, validateTaiwanNationalId, validateCitizenId, validateOldResidentId, validateNewResidentId } from "../../src"
|
|
3
|
+
|
|
4
|
+
describe("Taiwan nationalId() validator", () => {
|
|
5
|
+
beforeEach(() => setLocale("en"))
|
|
6
|
+
|
|
7
|
+
describe("basic functionality", () => {
|
|
8
|
+
it("should validate correct Taiwan national IDs (both types)", () => {
|
|
9
|
+
const schema = nationalId()
|
|
10
|
+
|
|
11
|
+
// Valid citizen IDs (身分證字號)
|
|
12
|
+
expect(schema.parse("A123456789")).toBe("A123456789")
|
|
13
|
+
expect(schema.parse("A223456781")).toBe("A223456781")
|
|
14
|
+
expect(schema.parse("B123456780")).toBe("B123456780")
|
|
15
|
+
|
|
16
|
+
// Valid old resident IDs (舊式居留證)
|
|
17
|
+
expect(schema.parse("AA00000001")).toBe("AA00000001")
|
|
18
|
+
expect(schema.parse("AB00000009")).toBe("AB00000009")
|
|
19
|
+
expect(schema.parse("BC00000002")).toBe("BC00000002")
|
|
20
|
+
|
|
21
|
+
// Valid new resident IDs (新式居留證)
|
|
22
|
+
expect(schema.parse("A800000005")).toBe("A800000005")
|
|
23
|
+
expect(schema.parse("A900000007")).toBe("A900000007")
|
|
24
|
+
expect(schema.parse("B800000006")).toBe("B800000006")
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("should reject invalid Taiwan national IDs", () => {
|
|
28
|
+
const schema = nationalId()
|
|
29
|
+
|
|
30
|
+
// Invalid formats
|
|
31
|
+
expect(() => schema.parse("A12345678")).toThrow("Invalid Taiwan National ID") // Too short
|
|
32
|
+
expect(() => schema.parse("A1234567890")).toThrow("Invalid Taiwan National ID") // Too long
|
|
33
|
+
expect(() => schema.parse("1234567890")).toThrow("Invalid Taiwan National ID") // No letter
|
|
34
|
+
expect(() => schema.parse("AB23456789")).toThrow("Invalid Taiwan National ID") // Wrong format
|
|
35
|
+
|
|
36
|
+
// Invalid checksums
|
|
37
|
+
expect(() => schema.parse("A123456788")).toThrow("Invalid Taiwan National ID")
|
|
38
|
+
expect(() => schema.parse("A223456782")).toThrow("Invalid Taiwan National ID")
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("should handle case conversion", () => {
|
|
42
|
+
const schema = nationalId()
|
|
43
|
+
|
|
44
|
+
expect(schema.parse("a123456789")).toBe("A123456789")
|
|
45
|
+
expect(schema.parse("b123456780")).toBe("B123456780")
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe("type-specific validation", () => {
|
|
50
|
+
it("should validate only citizen IDs when type is 'citizen'", () => {
|
|
51
|
+
const schema = nationalId({ type: "citizen" })
|
|
52
|
+
|
|
53
|
+
// Should accept citizen IDs
|
|
54
|
+
expect(schema.parse("A123456789")).toBe("A123456789")
|
|
55
|
+
expect(schema.parse("B123456780")).toBe("B123456780")
|
|
56
|
+
|
|
57
|
+
// Should reject resident IDs
|
|
58
|
+
expect(() => schema.parse("AA00000001")).toThrow("Invalid Taiwan National ID")
|
|
59
|
+
expect(() => schema.parse("A800000005")).toThrow("Invalid Taiwan National ID")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("should validate only resident IDs when type is 'resident'", () => {
|
|
63
|
+
const schema = nationalId({ type: "resident" })
|
|
64
|
+
|
|
65
|
+
// Should accept old resident IDs
|
|
66
|
+
expect(schema.parse("AA00000001")).toBe("AA00000001")
|
|
67
|
+
expect(schema.parse("ZB12345676")).toBe("ZB12345676")
|
|
68
|
+
|
|
69
|
+
// Should accept new resident IDs
|
|
70
|
+
expect(schema.parse("A800000005")).toBe("A800000005")
|
|
71
|
+
expect(schema.parse("Z812345672")).toBe("Z812345672")
|
|
72
|
+
|
|
73
|
+
// Should reject citizen IDs
|
|
74
|
+
expect(() => schema.parse("A123456789")).toThrow("Invalid Taiwan National ID")
|
|
75
|
+
expect(() => schema.parse("B123456780")).toThrow("Invalid Taiwan National ID")
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it("should validate both types when type is 'both' (default)", () => {
|
|
79
|
+
const schema = nationalId({ type: "both" })
|
|
80
|
+
|
|
81
|
+
// Should accept all valid formats
|
|
82
|
+
expect(schema.parse("A123456789")).toBe("A123456789") // Citizen
|
|
83
|
+
expect(schema.parse("AA00000001")).toBe("AA00000001") // Old resident
|
|
84
|
+
expect(schema.parse("A800000005")).toBe("A800000005") // New resident
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe("allowOldResident option", () => {
|
|
89
|
+
it("should allow old resident IDs by default", () => {
|
|
90
|
+
const schema = nationalId({ type: "resident" })
|
|
91
|
+
|
|
92
|
+
// Should accept old resident IDs (default allowOldResident=true)
|
|
93
|
+
expect(schema.parse("AA00000001")).toBe("AA00000001")
|
|
94
|
+
expect(schema.parse("AB00000009")).toBe("AB00000009")
|
|
95
|
+
expect(schema.parse("ZB12345676")).toBe("ZB12345676")
|
|
96
|
+
|
|
97
|
+
// Should also accept new resident IDs
|
|
98
|
+
expect(schema.parse("A800000005")).toBe("A800000005")
|
|
99
|
+
expect(schema.parse("A912345673")).toBe("A912345673")
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("should reject old resident IDs when allowOldResident=false", () => {
|
|
103
|
+
const schema = nationalId({ type: "resident", allowOldResident: false })
|
|
104
|
+
|
|
105
|
+
// Should reject old resident IDs
|
|
106
|
+
expect(() => schema.parse("AA00000001")).toThrow("Invalid Taiwan National ID")
|
|
107
|
+
expect(() => schema.parse("AB00000009")).toThrow("Invalid Taiwan National ID")
|
|
108
|
+
expect(() => schema.parse("ZB12345676")).toThrow("Invalid Taiwan National ID")
|
|
109
|
+
|
|
110
|
+
// Should still accept new resident IDs
|
|
111
|
+
expect(schema.parse("A800000005")).toBe("A800000005")
|
|
112
|
+
expect(schema.parse("A912345673")).toBe("A912345673")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it("should work with type='both' and allowOldResident=false", () => {
|
|
116
|
+
const schema = nationalId({ type: "both", allowOldResident: false })
|
|
117
|
+
|
|
118
|
+
// Should accept citizen IDs
|
|
119
|
+
expect(schema.parse("A123456789")).toBe("A123456789")
|
|
120
|
+
expect(schema.parse("B123456780")).toBe("B123456780")
|
|
121
|
+
|
|
122
|
+
// Should accept new resident IDs
|
|
123
|
+
expect(schema.parse("A800000005")).toBe("A800000005")
|
|
124
|
+
expect(schema.parse("A912345673")).toBe("A912345673")
|
|
125
|
+
|
|
126
|
+
// Should reject old resident IDs
|
|
127
|
+
expect(() => schema.parse("AA00000001")).toThrow("Invalid Taiwan National ID")
|
|
128
|
+
expect(() => schema.parse("AB00000009")).toThrow("Invalid Taiwan National ID")
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it("should not affect citizen IDs validation", () => {
|
|
132
|
+
const schemaWithOld = nationalId({ type: "citizen", allowOldResident: true })
|
|
133
|
+
const schemaWithoutOld = nationalId({ type: "citizen", allowOldResident: false })
|
|
134
|
+
|
|
135
|
+
// Both should accept citizen IDs
|
|
136
|
+
expect(schemaWithOld.parse("A123456789")).toBe("A123456789")
|
|
137
|
+
expect(schemaWithoutOld.parse("A123456789")).toBe("A123456789")
|
|
138
|
+
|
|
139
|
+
// Both should reject resident IDs (since type is citizen)
|
|
140
|
+
expect(() => schemaWithOld.parse("AA00000001")).toThrow("Invalid Taiwan National ID")
|
|
141
|
+
expect(() => schemaWithoutOld.parse("AA00000001")).toThrow("Invalid Taiwan National ID")
|
|
142
|
+
expect(() => schemaWithOld.parse("A800000005")).toThrow("Invalid Taiwan National ID")
|
|
143
|
+
expect(() => schemaWithoutOld.parse("A800000005")).toThrow("Invalid Taiwan National ID")
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe("utility functions", () => {
|
|
148
|
+
describe("validateCitizenId", () => {
|
|
149
|
+
it("should correctly validate citizen IDs", () => {
|
|
150
|
+
// Valid citizen IDs
|
|
151
|
+
expect(validateCitizenId("A123456789")).toBe(true)
|
|
152
|
+
expect(validateCitizenId("B123456780")).toBe(true)
|
|
153
|
+
expect(validateCitizenId("Z187654324")).toBe(true)
|
|
154
|
+
|
|
155
|
+
// Invalid citizen IDs
|
|
156
|
+
expect(validateCitizenId("A123456788")).toBe(false) // Wrong checksum
|
|
157
|
+
expect(validateCitizenId("A323456789")).toBe(false) // Invalid gender digit
|
|
158
|
+
expect(validateCitizenId("AA00000001")).toBe(false) // Wrong format
|
|
159
|
+
expect(validateCitizenId("A12345678")).toBe(false) // Too short
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
describe("validateOldResidentId", () => {
|
|
164
|
+
it("should correctly validate old resident IDs", () => {
|
|
165
|
+
// Valid old resident IDs
|
|
166
|
+
expect(validateOldResidentId("AA00000001")).toBe(true)
|
|
167
|
+
expect(validateOldResidentId("AC12345677")).toBe(true)
|
|
168
|
+
expect(validateOldResidentId("ZB12345676")).toBe(true)
|
|
169
|
+
expect(validateOldResidentId("AD12345675")).toBe(true)
|
|
170
|
+
|
|
171
|
+
// Invalid old resident IDs
|
|
172
|
+
expect(validateOldResidentId("AE12345678")).toBe(false) // Invalid gender code
|
|
173
|
+
expect(validateOldResidentId("A123456789")).toBe(false) // Wrong format
|
|
174
|
+
expect(validateOldResidentId("AA1234567")).toBe(false) // Too short
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe("validateNewResidentId", () => {
|
|
179
|
+
it("should correctly validate new resident IDs", () => {
|
|
180
|
+
// Valid new resident IDs
|
|
181
|
+
expect(validateNewResidentId("A800000005")).toBe(true)
|
|
182
|
+
expect(validateNewResidentId("A912345673")).toBe(true)
|
|
183
|
+
expect(validateNewResidentId("Z812345672")).toBe(true)
|
|
184
|
+
expect(validateNewResidentId("Z912345674")).toBe(true)
|
|
185
|
+
|
|
186
|
+
// Invalid new resident IDs
|
|
187
|
+
expect(validateNewResidentId("A712345678")).toBe(false) // Invalid gender digit
|
|
188
|
+
expect(validateNewResidentId("A123456789")).toBe(false) // Wrong format
|
|
189
|
+
expect(validateNewResidentId("A81234567")).toBe(false) // Too short
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe("validateTaiwanNationalId", () => {
|
|
194
|
+
it("should validate with type parameter", () => {
|
|
195
|
+
// Test with citizen type
|
|
196
|
+
expect(validateTaiwanNationalId("A123456789", "citizen")).toBe(true)
|
|
197
|
+
expect(validateTaiwanNationalId("AA00000001", "citizen")).toBe(false)
|
|
198
|
+
|
|
199
|
+
// Test with resident type
|
|
200
|
+
expect(validateTaiwanNationalId("AA00000001", "resident")).toBe(true)
|
|
201
|
+
expect(validateTaiwanNationalId("A800000005", "resident")).toBe(true)
|
|
202
|
+
expect(validateTaiwanNationalId("A123456789", "resident")).toBe(false)
|
|
203
|
+
|
|
204
|
+
// Test with both type
|
|
205
|
+
expect(validateTaiwanNationalId("A123456789", "both")).toBe(true)
|
|
206
|
+
expect(validateTaiwanNationalId("AA00000001", "both")).toBe(true)
|
|
207
|
+
expect(validateTaiwanNationalId("A800000005", "both")).toBe(true)
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe("required/optional behavior", () => {
|
|
213
|
+
it("should handle required=true (default)", () => {
|
|
214
|
+
const schema = nationalId()
|
|
215
|
+
|
|
216
|
+
expect(() => schema.parse("")).toThrow("Required")
|
|
217
|
+
expect(() => schema.parse(null)).toThrow()
|
|
218
|
+
expect(() => schema.parse(undefined)).toThrow()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("should handle required=false", () => {
|
|
222
|
+
const schema = nationalId({ required: false })
|
|
223
|
+
|
|
224
|
+
expect(schema.parse("")).toBe(null)
|
|
225
|
+
expect(schema.parse(null)).toBe(null)
|
|
226
|
+
expect(schema.parse(undefined)).toBe(null)
|
|
227
|
+
expect(schema.parse("A123456789")).toBe("A123456789")
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it("should use default values", () => {
|
|
231
|
+
const requiredSchema = nationalId({ defaultValue: "A123456789" })
|
|
232
|
+
const optionalSchema = nationalId({ required: false, defaultValue: "A123456789" })
|
|
233
|
+
|
|
234
|
+
expect(requiredSchema.parse("")).toBe("A123456789")
|
|
235
|
+
expect(optionalSchema.parse("")).toBe("A123456789")
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe("transform function", () => {
|
|
240
|
+
it("should apply custom transform", () => {
|
|
241
|
+
const schema = nationalId({
|
|
242
|
+
transform: (val) => val.replace(/[-\s]/g, "").toUpperCase(),
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
expect(schema.parse("a12-345-6789")).toBe("A123456789")
|
|
246
|
+
expect(schema.parse("z18 765 4324")).toBe("Z187654324")
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it("should apply transform before validation", () => {
|
|
250
|
+
const schema = nationalId({
|
|
251
|
+
transform: (val) => val.replace(/\s+/g, "").toUpperCase(),
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
expect(schema.parse(" a123456789 ")).toBe("A123456789")
|
|
255
|
+
expect(() => schema.parse(" a123456788 ")).toThrow("Invalid Taiwan National ID")
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
describe("input preprocessing", () => {
|
|
260
|
+
it("should handle string conversion and case normalization", () => {
|
|
261
|
+
const schema = nationalId()
|
|
262
|
+
|
|
263
|
+
// Test automatic uppercase conversion
|
|
264
|
+
expect(schema.parse("a123456789")).toBe("A123456789")
|
|
265
|
+
expect(schema.parse("z187654324")).toBe("Z187654324")
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
it("should trim whitespace", () => {
|
|
269
|
+
const schema = nationalId()
|
|
270
|
+
|
|
271
|
+
expect(schema.parse(" A123456789 ")).toBe("A123456789")
|
|
272
|
+
expect(schema.parse("\tZ187654324\n")).toBe("Z187654324")
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
describe("i18n support", () => {
|
|
277
|
+
it("should use English messages by default", () => {
|
|
278
|
+
setLocale("en")
|
|
279
|
+
const schema = nationalId()
|
|
280
|
+
|
|
281
|
+
expect(() => schema.parse("")).toThrow("Required")
|
|
282
|
+
expect(() => schema.parse("A123456788")).toThrow("Invalid Taiwan National ID")
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it("should use Chinese messages when locale is zh-TW", () => {
|
|
286
|
+
setLocale("zh-TW")
|
|
287
|
+
const schema = nationalId()
|
|
288
|
+
|
|
289
|
+
expect(() => schema.parse("")).toThrow("必填")
|
|
290
|
+
expect(() => schema.parse("A123456788")).toThrow("無效的身分證字號")
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it("should support custom i18n messages", () => {
|
|
294
|
+
const schema = nationalId({
|
|
295
|
+
i18n: {
|
|
296
|
+
en: {
|
|
297
|
+
required: "National ID is required",
|
|
298
|
+
invalid: "National ID is invalid",
|
|
299
|
+
},
|
|
300
|
+
"zh-TW": {
|
|
301
|
+
required: "請輸入身分證字號",
|
|
302
|
+
invalid: "身分證字號格式錯誤",
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
setLocale("en")
|
|
308
|
+
expect(() => schema.parse("")).toThrow("National ID is required")
|
|
309
|
+
expect(() => schema.parse("A123456788")).toThrow("National ID is invalid")
|
|
310
|
+
|
|
311
|
+
setLocale("zh-TW")
|
|
312
|
+
expect(() => schema.parse("")).toThrow("請輸入身分證字號")
|
|
313
|
+
expect(() => schema.parse("A123456788")).toThrow("身分證字號格式錯誤")
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
describe("real world Taiwan national IDs", () => {
|
|
318
|
+
it("should validate known citizen ID patterns", () => {
|
|
319
|
+
const schema = nationalId({ type: "citizen" })
|
|
320
|
+
|
|
321
|
+
// Test various city codes and gender combinations
|
|
322
|
+
const validCitizenIds = [
|
|
323
|
+
"A123456789", // Taipei male
|
|
324
|
+
"A223456781", // Taipei female
|
|
325
|
+
"B123456780", // Taichung male
|
|
326
|
+
"B223456782", // Taichung female
|
|
327
|
+
"Z187654324", // Lianjiang male
|
|
328
|
+
"Z287654326", // Lianjiang female
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
validCitizenIds.forEach((id) => {
|
|
332
|
+
expect(schema.parse(id)).toBe(id)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it("should validate known old resident ID patterns", () => {
|
|
337
|
+
const schema = nationalId({ type: "resident" })
|
|
338
|
+
|
|
339
|
+
// Test old format resident IDs
|
|
340
|
+
const validOldResidentIds = [
|
|
341
|
+
"AA00000001", // Male
|
|
342
|
+
"AB00000009", // Female
|
|
343
|
+
"AC12345677", // Male
|
|
344
|
+
"AD12345675", // Female
|
|
345
|
+
"ZA12345678", // Male
|
|
346
|
+
"ZB12345676", // Female
|
|
347
|
+
]
|
|
348
|
+
|
|
349
|
+
validOldResidentIds.forEach((id) => {
|
|
350
|
+
expect(schema.parse(id)).toBe(id)
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it("should validate known new resident ID patterns", () => {
|
|
355
|
+
const schema = nationalId({ type: "resident" })
|
|
356
|
+
|
|
357
|
+
// Test new format resident IDs
|
|
358
|
+
const validNewResidentIds = [
|
|
359
|
+
"A800000005", // Male
|
|
360
|
+
"A912345673", // Female
|
|
361
|
+
"B800000006", // Male
|
|
362
|
+
"B912345674", // Female
|
|
363
|
+
"Z812345672", // Male
|
|
364
|
+
"Z912345674", // Female
|
|
365
|
+
]
|
|
366
|
+
|
|
367
|
+
validNewResidentIds.forEach((id) => {
|
|
368
|
+
expect(schema.parse(id)).toBe(id)
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it("should reject common invalid patterns", () => {
|
|
373
|
+
const schema = nationalId()
|
|
374
|
+
|
|
375
|
+
const invalidIds = [
|
|
376
|
+
"A000000000", // All zeros
|
|
377
|
+
"A111111111", // All ones
|
|
378
|
+
"A123456788", // Wrong checksum
|
|
379
|
+
"A323456789", // Invalid gender digit for citizen
|
|
380
|
+
"A712345678", // Invalid gender digit for new resident
|
|
381
|
+
"AE12345678", // Invalid gender code for old resident
|
|
382
|
+
"1123456789", // No letter
|
|
383
|
+
"A023456789", // Invalid gender digit
|
|
384
|
+
"A12345678", // Too short
|
|
385
|
+
"A1234567890", // Too long
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
invalidIds.forEach((id) => {
|
|
389
|
+
expect(() => schema.parse(id)).toThrow()
|
|
390
|
+
})
|
|
391
|
+
})
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
describe("edge cases", () => {
|
|
395
|
+
it("should handle all valid city codes", () => {
|
|
396
|
+
const schema = nationalId({ type: "citizen" })
|
|
397
|
+
|
|
398
|
+
// Test all valid city codes
|
|
399
|
+
const cityCodes = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
|
|
400
|
+
|
|
401
|
+
cityCodes.forEach((code) => {
|
|
402
|
+
// Test with a known valid pattern (need to calculate actual valid IDs)
|
|
403
|
+
const testId = code + "123456789"
|
|
404
|
+
try {
|
|
405
|
+
schema.parse(testId)
|
|
406
|
+
// If no error thrown, the ID is valid
|
|
407
|
+
} catch (error: any) {
|
|
408
|
+
// If error thrown, it's expected for invalid checksums
|
|
409
|
+
expect(error.message).toContain("Invalid Taiwan National ID")
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it("should handle empty and whitespace inputs", () => {
|
|
415
|
+
const schema = nationalId()
|
|
416
|
+
const optionalSchema = nationalId({ required: false })
|
|
417
|
+
|
|
418
|
+
expect(() => schema.parse("")).toThrow("Required")
|
|
419
|
+
expect(() => schema.parse(" ")).toThrow("Required")
|
|
420
|
+
expect(() => schema.parse("\t\n")).toThrow("Required")
|
|
421
|
+
|
|
422
|
+
expect(optionalSchema.parse("")).toBe(null)
|
|
423
|
+
expect(optionalSchema.parse(" ")).toBe(null)
|
|
424
|
+
expect(optionalSchema.parse("\t\n")).toBe(null)
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it("should preserve valid format after transformation", () => {
|
|
428
|
+
const schema = nationalId({
|
|
429
|
+
transform: (val) => val.replace(/[^A-Z0-9]/g, "").toUpperCase(),
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
expect(schema.parse("a-1@2#3$4%5^6&7*8(9)")).toBe("A123456789")
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
})
|