@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.
- package/.claude/settings.local.json +28 -0
- package/LICENSE +21 -0
- package/README.md +465 -97
- package/debug.js +21 -0
- package/debug.ts +16 -0
- package/dist/index.cjs +3127 -146
- package/dist/index.d.cts +3021 -25
- package/dist/index.d.ts +3021 -25
- package/dist/index.js +3081 -144
- package/eslint.config.mts +8 -0
- package/package.json +10 -9
- package/src/config.ts +1 -1
- package/src/i18n/locales/en.json +161 -25
- package/src/i18n/locales/zh-TW.json +165 -26
- package/src/index.ts +17 -7
- package/src/validators/common/boolean.ts +191 -0
- package/src/validators/common/date.ts +299 -0
- package/src/validators/common/datetime.ts +673 -0
- package/src/validators/common/email.ts +313 -0
- package/src/validators/common/file.ts +384 -0
- package/src/validators/common/id.ts +471 -0
- package/src/validators/common/number.ts +319 -0
- package/src/validators/common/password.ts +386 -0
- package/src/validators/common/text.ts +271 -0
- package/src/validators/common/time.ts +600 -0
- package/src/validators/common/url.ts +347 -0
- package/src/validators/taiwan/business-id.ts +262 -0
- package/src/validators/taiwan/fax.ts +327 -0
- package/src/validators/taiwan/mobile.ts +242 -0
- package/src/validators/taiwan/national-id.ts +425 -0
- package/src/validators/taiwan/postal-code.ts +1049 -0
- package/src/validators/taiwan/tel.ts +330 -0
- package/tests/common/boolean.test.ts +340 -92
- package/tests/common/date.test.ts +458 -0
- package/tests/common/datetime.test.ts +693 -0
- package/tests/common/email.test.ts +232 -60
- package/tests/common/file.test.ts +479 -0
- package/tests/common/id.test.ts +535 -0
- package/tests/common/number.test.ts +230 -60
- package/tests/common/password.test.ts +271 -44
- package/tests/common/text.test.ts +210 -13
- package/tests/common/time.test.ts +528 -0
- 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/postal-code.test.ts +705 -0
- package/tests/taiwan/tel.test.ts +467 -0
- package/eslint.config.mjs +0 -10
- package/src/common/boolean.ts +0 -36
- package/src/common/date.ts +0 -43
- package/src/common/email.ts +0 -44
- package/src/common/integer.ts +0 -46
- package/src/common/number.ts +0 -37
- package/src/common/password.ts +0 -33
- package/src/common/text.ts +0 -34
- package/src/common/url.ts +0 -37
- package/tests/common/integer.test.ts +0 -90
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest"
|
|
2
|
+
import { file, setLocale } from "../../src"
|
|
3
|
+
|
|
4
|
+
describe("file() features", () => {
|
|
5
|
+
beforeEach(() => setLocale("en"))
|
|
6
|
+
|
|
7
|
+
// Helper function to create mock files for testing
|
|
8
|
+
const createMockFile = (name: string, size: number, type: string, content?: string): File => {
|
|
9
|
+
// For very large sizes, don't actually create large content
|
|
10
|
+
const actualContent = content || (size > 1024 * 1024 ? "x".repeat(1024) : "x".repeat(size))
|
|
11
|
+
const blob = new Blob([actualContent], { type })
|
|
12
|
+
const file = new File([blob], name, { type })
|
|
13
|
+
|
|
14
|
+
// Mock the size property since File constructor doesn't always set the exact size
|
|
15
|
+
Object.defineProperty(file, 'size', {
|
|
16
|
+
value: size,
|
|
17
|
+
writable: false,
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: false
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
return file
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("basic file validation", () => {
|
|
26
|
+
it("should accept valid File objects", () => {
|
|
27
|
+
const schema = file()
|
|
28
|
+
const mockFile = createMockFile("test.txt", 1024, "text/plain")
|
|
29
|
+
expect(schema.parse(mockFile)).toBe(mockFile)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("should reject non-File objects", () => {
|
|
33
|
+
const schema = file()
|
|
34
|
+
expect(() => schema.parse("not a file")).toThrow()
|
|
35
|
+
expect(() => schema.parse(123)).toThrow()
|
|
36
|
+
expect(() => schema.parse({})).toThrow()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("should handle required validation", () => {
|
|
40
|
+
const schema = file()
|
|
41
|
+
expect(() => schema.parse(null)).toThrow("Required")
|
|
42
|
+
expect(() => schema.parse(undefined)).toThrow("Required")
|
|
43
|
+
expect(() => schema.parse("")).toThrow("Required")
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("should allow null when not required", () => {
|
|
47
|
+
const schema = file({ required: false })
|
|
48
|
+
expect(schema.parse(null)).toBe(null)
|
|
49
|
+
expect(schema.parse(undefined)).toBe(null)
|
|
50
|
+
expect(schema.parse("")).toBe(null)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe("file size validation", () => {
|
|
55
|
+
it("should validate maximum file size", () => {
|
|
56
|
+
const schema = file({ maxSize: 1024 }) // 1KB
|
|
57
|
+
const smallFile = createMockFile("small.txt", 512, "text/plain")
|
|
58
|
+
const largeFile = createMockFile("large.txt", 2048, "text/plain")
|
|
59
|
+
|
|
60
|
+
expect(schema.parse(smallFile)).toBe(smallFile)
|
|
61
|
+
expect(() => schema.parse(largeFile)).toThrow("File size must not exceed 1 KB")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("should validate minimum file size", () => {
|
|
65
|
+
const schema = file({ minSize: 1024 }) // 1KB
|
|
66
|
+
const smallFile = createMockFile("small.txt", 512, "text/plain")
|
|
67
|
+
const largeFile = createMockFile("large.txt", 2048, "text/plain")
|
|
68
|
+
|
|
69
|
+
expect(() => schema.parse(smallFile)).toThrow("File size must be at least 1 KB")
|
|
70
|
+
expect(schema.parse(largeFile)).toBe(largeFile)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("should validate both min and max size", () => {
|
|
74
|
+
const schema = file({ minSize: 1024, maxSize: 4096 }) // 1KB - 4KB
|
|
75
|
+
const tooSmall = createMockFile("small.txt", 512, "text/plain")
|
|
76
|
+
const justRight = createMockFile("medium.txt", 2048, "text/plain")
|
|
77
|
+
const tooLarge = createMockFile("large.txt", 8192, "text/plain")
|
|
78
|
+
|
|
79
|
+
expect(() => schema.parse(tooSmall)).toThrow("File size must be at least 1 KB")
|
|
80
|
+
expect(schema.parse(justRight)).toBe(justRight)
|
|
81
|
+
expect(() => schema.parse(tooLarge)).toThrow("File size must not exceed 4 KB")
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe("file type validation", () => {
|
|
86
|
+
it("should accept allowed MIME types", () => {
|
|
87
|
+
const schema = file({ type: "text/plain" })
|
|
88
|
+
const textFile = createMockFile("test.txt", 1024, "text/plain")
|
|
89
|
+
const imageFile = createMockFile("test.jpg", 1024, "image/jpeg")
|
|
90
|
+
|
|
91
|
+
expect(schema.parse(textFile)).toBe(textFile)
|
|
92
|
+
expect(() => schema.parse(imageFile)).toThrow("File type must be one of: text/plain")
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("should accept multiple allowed MIME types", () => {
|
|
96
|
+
const schema = file({ type: ["text/plain", "image/jpeg", "application/pdf"] })
|
|
97
|
+
const textFile = createMockFile("test.txt", 1024, "text/plain")
|
|
98
|
+
const imageFile = createMockFile("test.jpg", 1024, "image/jpeg")
|
|
99
|
+
const pdfFile = createMockFile("test.pdf", 1024, "application/pdf")
|
|
100
|
+
const videoFile = createMockFile("test.mp4", 1024, "video/mp4")
|
|
101
|
+
|
|
102
|
+
expect(schema.parse(textFile)).toBe(textFile)
|
|
103
|
+
expect(schema.parse(imageFile)).toBe(imageFile)
|
|
104
|
+
expect(schema.parse(pdfFile)).toBe(pdfFile)
|
|
105
|
+
expect(() => schema.parse(videoFile)).toThrow("File type must be one of: text/plain, image/jpeg, application/pdf")
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it("should reject blacklisted MIME types", () => {
|
|
109
|
+
const schema = file({ typeBlacklist: ["application/x-executable", "application/x-virus"] })
|
|
110
|
+
const textFile = createMockFile("test.txt", 1024, "text/plain")
|
|
111
|
+
const exeFile = createMockFile("test.exe", 1024, "application/x-executable")
|
|
112
|
+
|
|
113
|
+
expect(schema.parse(textFile)).toBe(textFile)
|
|
114
|
+
expect(() => schema.parse(exeFile)).toThrow("File type must be one of: application/x-executable, application/x-virus")
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe("file extension validation", () => {
|
|
119
|
+
it("should validate single file extension", () => {
|
|
120
|
+
const schema = file({ extension: ".txt" })
|
|
121
|
+
const txtFile = createMockFile("test.txt", 1024, "text/plain")
|
|
122
|
+
const jpgFile = createMockFile("test.jpg", 1024, "image/jpeg")
|
|
123
|
+
|
|
124
|
+
expect(schema.parse(txtFile)).toBe(txtFile)
|
|
125
|
+
expect(() => schema.parse(jpgFile)).toThrow("File extension must be one of: .txt")
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it("should validate multiple file extensions", () => {
|
|
129
|
+
const schema = file({ extension: [".txt", ".pdf", ".doc"] })
|
|
130
|
+
const txtFile = createMockFile("test.txt", 1024, "text/plain")
|
|
131
|
+
const pdfFile = createMockFile("test.pdf", 1024, "application/pdf")
|
|
132
|
+
const jpgFile = createMockFile("test.jpg", 1024, "image/jpeg")
|
|
133
|
+
|
|
134
|
+
expect(schema.parse(txtFile)).toBe(txtFile)
|
|
135
|
+
expect(schema.parse(pdfFile)).toBe(pdfFile)
|
|
136
|
+
expect(() => schema.parse(jpgFile)).toThrow("File extension must be one of: .txt, .pdf, .doc")
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it("should handle extensions without dots", () => {
|
|
140
|
+
const schema = file({ extension: ["txt", "pdf"] })
|
|
141
|
+
const txtFile = createMockFile("test.txt", 1024, "text/plain")
|
|
142
|
+
const pdfFile = createMockFile("test.pdf", 1024, "application/pdf")
|
|
143
|
+
|
|
144
|
+
expect(schema.parse(txtFile)).toBe(txtFile)
|
|
145
|
+
expect(schema.parse(pdfFile)).toBe(pdfFile)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("should reject blacklisted extensions", () => {
|
|
149
|
+
const schema = file({ extensionBlacklist: [".exe", ".bat", ".cmd"] })
|
|
150
|
+
const txtFile = createMockFile("test.txt", 1024, "text/plain")
|
|
151
|
+
const exeFile = createMockFile("virus.exe", 1024, "application/x-executable")
|
|
152
|
+
|
|
153
|
+
expect(schema.parse(txtFile)).toBe(txtFile)
|
|
154
|
+
expect(() => schema.parse(exeFile)).toThrow("File extension .exe, .bat, .cmd is not allowed")
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it("should handle case sensitivity", () => {
|
|
158
|
+
const caseSensitiveSchema = file({ extension: [".txt"], caseSensitive: true })
|
|
159
|
+
const caseInsensitiveSchema = file({ extension: [".txt"], caseSensitive: false })
|
|
160
|
+
const upperCaseFile = createMockFile("test.TXT", 1024, "text/plain")
|
|
161
|
+
|
|
162
|
+
expect(() => caseSensitiveSchema.parse(upperCaseFile)).toThrow("File extension must be one of: .txt")
|
|
163
|
+
expect(caseInsensitiveSchema.parse(upperCaseFile)).toBe(upperCaseFile)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe("file name validation", () => {
|
|
168
|
+
it("should validate file name patterns", () => {
|
|
169
|
+
const schema = file({ namePattern: /^[a-zA-Z0-9_-]+\.(txt|pdf)$/ })
|
|
170
|
+
const validFile = createMockFile("valid_file-123.txt", 1024, "text/plain")
|
|
171
|
+
const invalidFile = createMockFile("invalid file!.txt", 1024, "text/plain")
|
|
172
|
+
|
|
173
|
+
expect(schema.parse(validFile)).toBe(validFile)
|
|
174
|
+
expect(() => schema.parse(invalidFile)).toThrow("File name must match pattern")
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it("should validate file name patterns with strings", () => {
|
|
178
|
+
const schema = file({ namePattern: "^report_\\d{4}\\.pdf$" })
|
|
179
|
+
const validFile = createMockFile("report_2023.pdf", 1024, "application/pdf")
|
|
180
|
+
const invalidFile = createMockFile("report_abc.pdf", 1024, "application/pdf")
|
|
181
|
+
|
|
182
|
+
expect(schema.parse(validFile)).toBe(validFile)
|
|
183
|
+
expect(() => schema.parse(invalidFile)).toThrow("File name must match pattern")
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("should reject blacklisted name patterns", () => {
|
|
187
|
+
const schema = file({ nameBlacklist: [/temp/i, /^test/] })
|
|
188
|
+
const goodFile = createMockFile("document.pdf", 1024, "application/pdf")
|
|
189
|
+
const tempFile = createMockFile("TempFile.txt", 1024, "text/plain")
|
|
190
|
+
const testFile = createMockFile("test_data.csv", 1024, "text/csv")
|
|
191
|
+
|
|
192
|
+
expect(schema.parse(goodFile)).toBe(goodFile)
|
|
193
|
+
expect(() => schema.parse(tempFile)).toThrow("File name must not match pattern")
|
|
194
|
+
expect(() => schema.parse(testFile)).toThrow("File name must not match pattern")
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it("should handle multiple blacklist patterns", () => {
|
|
198
|
+
const schema = file({ nameBlacklist: ["virus", /malware/i, /\.(exe|bat)$/] })
|
|
199
|
+
const goodFile = createMockFile("document.pdf", 1024, "application/pdf")
|
|
200
|
+
const virusFile = createMockFile("virus.txt", 1024, "text/plain")
|
|
201
|
+
const malwareFile = createMockFile("MALWARE_test.pdf", 1024, "application/pdf")
|
|
202
|
+
const exeFile = createMockFile("program.exe", 1024, "application/x-executable")
|
|
203
|
+
|
|
204
|
+
expect(schema.parse(goodFile)).toBe(goodFile)
|
|
205
|
+
expect(() => schema.parse(virusFile)).toThrow("File name must not match pattern")
|
|
206
|
+
expect(() => schema.parse(malwareFile)).toThrow("File name must not match pattern")
|
|
207
|
+
expect(() => schema.parse(exeFile)).toThrow("File name must not match pattern")
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
describe("file category validation", () => {
|
|
212
|
+
it("should validate image files only", () => {
|
|
213
|
+
const schema = file({ imageOnly: true })
|
|
214
|
+
const imageFile = createMockFile("photo.jpg", 1024, "image/jpeg")
|
|
215
|
+
const textFile = createMockFile("document.txt", 1024, "text/plain")
|
|
216
|
+
|
|
217
|
+
expect(schema.parse(imageFile)).toBe(imageFile)
|
|
218
|
+
expect(() => schema.parse(textFile)).toThrow("Only image files are allowed")
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("should validate document files only", () => {
|
|
222
|
+
const schema = file({ documentOnly: true })
|
|
223
|
+
const pdfFile = createMockFile("document.pdf", 1024, "application/pdf")
|
|
224
|
+
const imageFile = createMockFile("photo.jpg", 1024, "image/jpeg")
|
|
225
|
+
|
|
226
|
+
expect(schema.parse(pdfFile)).toBe(pdfFile)
|
|
227
|
+
expect(() => schema.parse(imageFile)).toThrow("Only document files are allowed")
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it("should validate video files only", () => {
|
|
231
|
+
const schema = file({ videoOnly: true })
|
|
232
|
+
const videoFile = createMockFile("movie.mp4", 1024, "video/mp4")
|
|
233
|
+
const audioFile = createMockFile("song.mp3", 1024, "audio/mpeg")
|
|
234
|
+
|
|
235
|
+
expect(schema.parse(videoFile)).toBe(videoFile)
|
|
236
|
+
expect(() => schema.parse(audioFile)).toThrow("Only video files are allowed")
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("should validate audio files only", () => {
|
|
240
|
+
const schema = file({ audioOnly: true })
|
|
241
|
+
const audioFile = createMockFile("song.mp3", 1024, "audio/mpeg")
|
|
242
|
+
const videoFile = createMockFile("movie.mp4", 1024, "video/mp4")
|
|
243
|
+
|
|
244
|
+
expect(schema.parse(audioFile)).toBe(audioFile)
|
|
245
|
+
expect(() => schema.parse(videoFile)).toThrow("Only audio files are allowed")
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it("should validate archive files only", () => {
|
|
249
|
+
const schema = file({ archiveOnly: true })
|
|
250
|
+
const zipFile = createMockFile("archive.zip", 1024, "application/zip")
|
|
251
|
+
const textFile = createMockFile("document.txt", 1024, "text/plain")
|
|
252
|
+
|
|
253
|
+
expect(schema.parse(zipFile)).toBe(zipFile)
|
|
254
|
+
expect(() => schema.parse(textFile)).toThrow("Only archive files are allowed")
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe("transform functionality", () => {
|
|
259
|
+
it("should apply transform function", () => {
|
|
260
|
+
const schema = file({
|
|
261
|
+
transform: (file) => {
|
|
262
|
+
return new File([file], file.name.toLowerCase(), { type: file.type })
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
const originalFile = createMockFile("TEST.TXT", 1024, "text/plain")
|
|
266
|
+
const result = schema.parse(originalFile)
|
|
267
|
+
|
|
268
|
+
expect(result.name).toBe("test.txt")
|
|
269
|
+
expect(result.type).toBe("text/plain")
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe("default value functionality", () => {
|
|
274
|
+
it("should use defaultValue for empty input when required", () => {
|
|
275
|
+
const defaultFile = createMockFile("default.txt", 1024, "text/plain")
|
|
276
|
+
const schema = file({ defaultValue: defaultFile })
|
|
277
|
+
|
|
278
|
+
expect(schema.parse("")).toBe(defaultFile)
|
|
279
|
+
expect(schema.parse(null)).toBe(defaultFile)
|
|
280
|
+
expect(schema.parse(undefined)).toBe(defaultFile)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it("should use defaultValue for empty input when not required", () => {
|
|
284
|
+
const defaultFile = createMockFile("default.txt", 1024, "text/plain")
|
|
285
|
+
const schema = file({ required: false, defaultValue: defaultFile })
|
|
286
|
+
|
|
287
|
+
expect(schema.parse("")).toBe(defaultFile)
|
|
288
|
+
expect(schema.parse(null)).toBe(defaultFile)
|
|
289
|
+
expect(schema.parse(undefined)).toBe(defaultFile)
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
describe("custom i18n messages", () => {
|
|
294
|
+
it("should use custom messages when provided", () => {
|
|
295
|
+
const schema = file({
|
|
296
|
+
maxSize: 1024,
|
|
297
|
+
extension: [".pdf"],
|
|
298
|
+
imageOnly: true,
|
|
299
|
+
i18n: {
|
|
300
|
+
en: {
|
|
301
|
+
required: "Custom required message",
|
|
302
|
+
maxSize: "Custom size message: ${maxSize}",
|
|
303
|
+
extension: "Custom extension message: ${extension}",
|
|
304
|
+
imageOnly: "Custom image only message",
|
|
305
|
+
},
|
|
306
|
+
"zh-TW": {
|
|
307
|
+
required: "客製化必填訊息",
|
|
308
|
+
maxSize: "客製化大小訊息: ${maxSize}",
|
|
309
|
+
extension: "客製化副檔名訊息: ${extension}",
|
|
310
|
+
imageOnly: "客製化僅限圖片訊息",
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const largeFile = createMockFile("large.txt", 2048, "text/plain")
|
|
316
|
+
const wrongExtFile = createMockFile("test.txt", 512, "text/plain")
|
|
317
|
+
const nonImageFile = createMockFile("test.pdf", 512, "application/pdf")
|
|
318
|
+
|
|
319
|
+
expect(() => schema.parse("")).toThrow("Custom required message")
|
|
320
|
+
expect(() => schema.parse(largeFile)).toThrow("Custom size message: 1 KB")
|
|
321
|
+
expect(() => schema.parse(wrongExtFile)).toThrow("Custom extension message: .pdf")
|
|
322
|
+
expect(() => schema.parse(nonImageFile)).toThrow("Custom image only message")
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it("should fallback to default messages when custom not provided", () => {
|
|
326
|
+
const schema = file({
|
|
327
|
+
videoOnly: true,
|
|
328
|
+
i18n: {
|
|
329
|
+
en: {
|
|
330
|
+
required: "Custom required message",
|
|
331
|
+
},
|
|
332
|
+
"zh-TW": {
|
|
333
|
+
required: "客製化必填訊息",
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const nonVideoFile = createMockFile("document.pdf", 1024, "application/pdf")
|
|
339
|
+
|
|
340
|
+
expect(() => schema.parse("")).toThrow("Custom required message")
|
|
341
|
+
expect(() => schema.parse(nonVideoFile)).toThrow("Only video files are allowed")
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it("should use correct locale for custom messages", () => {
|
|
345
|
+
setLocale("en")
|
|
346
|
+
const schemaEn = file({
|
|
347
|
+
imageOnly: true,
|
|
348
|
+
i18n: {
|
|
349
|
+
en: {
|
|
350
|
+
imageOnly: "English image message",
|
|
351
|
+
},
|
|
352
|
+
"zh-TW": {
|
|
353
|
+
imageOnly: "繁體中文圖片訊息",
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
const nonImageFile = createMockFile("document.pdf", 1024, "application/pdf")
|
|
358
|
+
expect(() => schemaEn.parse(nonImageFile)).toThrow("English image message")
|
|
359
|
+
|
|
360
|
+
setLocale("zh-TW")
|
|
361
|
+
const schemaZh = file({
|
|
362
|
+
imageOnly: true,
|
|
363
|
+
i18n: {
|
|
364
|
+
en: {
|
|
365
|
+
imageOnly: "English image message",
|
|
366
|
+
},
|
|
367
|
+
"zh-TW": {
|
|
368
|
+
imageOnly: "繁體中文圖片訊息",
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
})
|
|
372
|
+
expect(() => schemaZh.parse(nonImageFile)).toThrow("繁體中文圖片訊息")
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
describe("complex scenarios", () => {
|
|
377
|
+
it("should work with multiple validations", () => {
|
|
378
|
+
const schema = file({
|
|
379
|
+
maxSize: 5 * 1024 * 1024, // 5MB
|
|
380
|
+
minSize: 1024, // 1KB
|
|
381
|
+
extension: [".jpg", ".png", ".pdf"],
|
|
382
|
+
namePattern: /^[a-zA-Z0-9_-]+\.(jpg|png|pdf)$/,
|
|
383
|
+
nameBlacklist: [/temp/i, /test/],
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
const validFile = createMockFile("document_2023.pdf", 2048, "application/pdf")
|
|
387
|
+
const tooSmallFile = createMockFile("tiny.jpg", 512, "image/jpeg")
|
|
388
|
+
const tooLargeFile = createMockFile("huge.png", 10 * 1024 * 1024, "image/png")
|
|
389
|
+
const wrongExtFile = createMockFile("document.txt", 2048, "text/plain")
|
|
390
|
+
const invalidNameFile = createMockFile("temp_file.pdf", 2048, "application/pdf")
|
|
391
|
+
|
|
392
|
+
expect(schema.parse(validFile)).toBe(validFile)
|
|
393
|
+
expect(() => schema.parse(tooSmallFile)).toThrow("File size must be at least 1 KB")
|
|
394
|
+
expect(() => schema.parse(tooLargeFile)).toThrow("File size must not exceed 5 MB")
|
|
395
|
+
expect(() => schema.parse(wrongExtFile)).toThrow("File extension must be one of: .jpg, .png, .pdf")
|
|
396
|
+
expect(() => schema.parse(invalidNameFile)).toThrow("File name must not match pattern")
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it("should handle null values correctly when not required", () => {
|
|
400
|
+
const schema = file({
|
|
401
|
+
required: false,
|
|
402
|
+
maxSize: 1024,
|
|
403
|
+
extension: [".txt"],
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
expect(schema.parse(null)).toBe(null)
|
|
407
|
+
expect(schema.parse("")).toBe(null)
|
|
408
|
+
expect(schema.parse(undefined)).toBe(null)
|
|
409
|
+
|
|
410
|
+
const validFile = createMockFile("test.txt", 512, "text/plain")
|
|
411
|
+
expect(schema.parse(validFile)).toBe(validFile)
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it("should work with transform and all validations together", () => {
|
|
415
|
+
const schema = file({
|
|
416
|
+
maxSize: 2048,
|
|
417
|
+
extension: [".txt"],
|
|
418
|
+
namePattern: /^[a-z0-9_-]+\.txt$/,
|
|
419
|
+
transform: (file) => {
|
|
420
|
+
return new File([file], file.name.toLowerCase(), { type: file.type })
|
|
421
|
+
},
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
const upperCaseFile = createMockFile("TEST_FILE.TXT", 1024, "text/plain")
|
|
425
|
+
const result = schema.parse(upperCaseFile)
|
|
426
|
+
|
|
427
|
+
expect(result.name).toBe("test_file.txt")
|
|
428
|
+
expect(result.type).toBe("text/plain")
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it("should validate predefined file categories with specific types", () => {
|
|
432
|
+
const imageSchema = file({ imageOnly: true, extension: [".jpg", ".png"] })
|
|
433
|
+
const jpegFile = createMockFile("photo.jpg", 1024, "image/jpeg")
|
|
434
|
+
const pngFile = createMockFile("image.png", 1024, "image/png")
|
|
435
|
+
const gifFile = createMockFile("animated.gif", 1024, "image/gif") // Valid image type but wrong extension
|
|
436
|
+
|
|
437
|
+
expect(imageSchema.parse(jpegFile)).toBe(jpegFile)
|
|
438
|
+
expect(imageSchema.parse(pngFile)).toBe(pngFile)
|
|
439
|
+
expect(() => imageSchema.parse(gifFile)).toThrow("File extension must be one of: .jpg, .png")
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
describe("edge cases", () => {
|
|
444
|
+
it("should handle files without extensions", () => {
|
|
445
|
+
const schema = file({ extension: [".txt"] })
|
|
446
|
+
const noExtFile = createMockFile("README", 1024, "text/plain")
|
|
447
|
+
|
|
448
|
+
expect(() => schema.parse(noExtFile)).toThrow("File extension must be one of: .txt")
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
it("should handle empty file names", () => {
|
|
452
|
+
const schema = file({ namePattern: /^.+$/ }) // Must have at least one character
|
|
453
|
+
const emptyNameFile = createMockFile("", 1024, "text/plain")
|
|
454
|
+
|
|
455
|
+
expect(() => schema.parse(emptyNameFile)).toThrow("File name must match pattern")
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
it("should handle zero-sized files", () => {
|
|
459
|
+
const schema = file({ minSize: 1 })
|
|
460
|
+
const emptyFile = createMockFile("empty.txt", 0, "text/plain")
|
|
461
|
+
|
|
462
|
+
expect(() => schema.parse(emptyFile)).toThrow("File size must be at least 1 B")
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it("should format file sizes correctly", () => {
|
|
466
|
+
const smallSchema = file({ maxSize: 1024 })
|
|
467
|
+
const mediumSchema = file({ maxSize: 1024 * 1024 })
|
|
468
|
+
const largeSchema = file({ maxSize: 1024 * 1024 * 1024 })
|
|
469
|
+
|
|
470
|
+
const tooLargeSmall = createMockFile("file.txt", 2048, "text/plain")
|
|
471
|
+
const tooLargeMedium = createMockFile("file.txt", 2 * 1024 * 1024, "text/plain")
|
|
472
|
+
const tooLargeLarge = createMockFile("file.txt", 2 * 1024 * 1024 * 1024, "text/plain")
|
|
473
|
+
|
|
474
|
+
expect(() => smallSchema.parse(tooLargeSmall)).toThrow("File size must not exceed 1 KB")
|
|
475
|
+
expect(() => mediumSchema.parse(tooLargeMedium)).toThrow("File size must not exceed 1 MB")
|
|
476
|
+
expect(() => largeSchema.parse(tooLargeLarge)).toThrow("File size must not exceed 1 GB")
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
})
|