Skip to content

Commit 6aff8a4

Browse files
committed
fix: add comprehensive parameter validation for patterns array
- Add validatePatterns utility to validate array and string content - Prevent TypeError when undefined/null passed as patterns parameter - Validate that all pattern elements are non-empty strings - Maintain backward compatibility for empty arrays - Add validation to all API entry points (zip, zipToFile methods and functions) - Include comprehensive test coverage with 20 new validation tests - Provide specific error messages for different validation failures
1 parent 9e90d5d commit 6aff8a4

File tree

5 files changed

+250
-1
lines changed

5 files changed

+250
-1
lines changed

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import type {
77
ZipJsonData,
88
ZipOptions,
99
} from "./core/types.js"
10+
import { validatePatterns } from "./utils/validation.js"
1011

1112
export class ZipJson {
1213
private archiver = new Archiver()
1314
private extractor = new Extractor()
1415

1516
async zip(patterns: string[], options?: ZipOptions): Promise<ZipJsonData> {
17+
validatePatterns(patterns)
1618
return this.archiver.archive(patterns, options)
1719
}
1820

src/utils/glob.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { resolve } from "node:path"
22
import { glob } from "glob"
33
import type { FileEntry } from "../core/types.js"
44
import { getFileStats, makeRelativePath } from "./file.js"
5+
import { validatePatterns } from "./validation.js"
56

67
export interface GlobOptions {
78
baseDir?: string
@@ -12,11 +13,18 @@ export async function collectFiles(
1213
patterns: string[],
1314
options: GlobOptions = {},
1415
): Promise<FileEntry[]> {
16+
validatePatterns(patterns)
17+
1518
const { baseDir = process.cwd(), ignore = [] } = options
1619

1720
const resolvedBaseDir = resolve(baseDir)
1821
const allFiles = new Set<string>()
1922

23+
// Return empty array if no patterns provided (allows empty archives)
24+
if (patterns.length === 0) {
25+
return []
26+
}
27+
2028
for (const pattern of patterns) {
2129
const files = await glob(pattern, {
2230
cwd: resolvedBaseDir,

src/utils/validation.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function validatePatterns(patterns: string[]): void {
2+
if (!patterns || !Array.isArray(patterns)) {
3+
throw new Error("patterns must be an array of strings")
4+
}
5+
6+
if (patterns.length > 0) {
7+
for (let i = 0; i < patterns.length; i++) {
8+
const pattern = patterns[i]
9+
if (typeof pattern !== "string") {
10+
throw new Error(
11+
`patterns[${i}] must be a string, got ${typeof pattern}`,
12+
)
13+
}
14+
if (pattern.trim() === "") {
15+
throw new Error(`patterns[${i}] must be a non-empty string`)
16+
}
17+
}
18+
}
19+
}

tests/unit/index.test.ts

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
22
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
33
import { join } from "node:path"
4-
import { ZipJson, listFromFile, unzipFromFile } from "../../src/index.js"
4+
import {
5+
ZipJson,
6+
listFromFile,
7+
unzipFromFile,
8+
zip,
9+
zipToFile,
10+
} from "../../src/index.js"
511

612
describe("Index API Tests", () => {
713
const testDir = join(process.cwd(), "test-index")
@@ -78,3 +84,138 @@ describe("Index API Tests", () => {
7884
expect(files[0].isDirectory).toBe(false)
7985
})
8086
})
87+
88+
describe("Pattern Validation Tests", () => {
89+
const testDir = join(process.cwd(), "test-validation")
90+
const zipJson = new ZipJson()
91+
92+
beforeEach(() => {
93+
mkdirSync(testDir, { recursive: true })
94+
writeFileSync(join(testDir, "test.txt"), "test content")
95+
})
96+
97+
afterEach(() => {
98+
if (existsSync(testDir)) {
99+
rmSync(testDir, { recursive: true, force: true })
100+
}
101+
})
102+
103+
describe("ZipJson class methods", () => {
104+
test("zip() validates patterns parameter", async () => {
105+
// Valid cases
106+
await expect(zipJson.zip([])).resolves.toBeDefined()
107+
await expect(zipJson.zip(["*.txt"])).resolves.toBeDefined()
108+
109+
// Invalid cases
110+
await expect(zipJson.zip(null as any)).rejects.toThrow(
111+
"patterns must be an array of strings",
112+
)
113+
await expect(zipJson.zip(undefined as any)).rejects.toThrow(
114+
"patterns must be an array of strings",
115+
)
116+
await expect(zipJson.zip("*.txt" as any)).rejects.toThrow(
117+
"patterns must be an array of strings",
118+
)
119+
await expect(zipJson.zip([123] as any)).rejects.toThrow(
120+
"patterns[0] must be a string, got number",
121+
)
122+
await expect(zipJson.zip([""])).rejects.toThrow(
123+
"patterns[0] must be a non-empty string",
124+
)
125+
await expect(zipJson.zip([" "])).rejects.toThrow(
126+
"patterns[0] must be a non-empty string",
127+
)
128+
})
129+
130+
test("zipToFile() validates patterns parameter", async () => {
131+
const outputPath = join(testDir, "test.json")
132+
133+
// Valid cases
134+
await expect(zipJson.zipToFile([], outputPath)).resolves.toBeUndefined()
135+
await expect(
136+
zipJson.zipToFile(["*.txt"], outputPath),
137+
).resolves.toBeUndefined()
138+
139+
// Invalid cases
140+
await expect(zipJson.zipToFile(null as any, outputPath)).rejects.toThrow(
141+
"patterns must be an array of strings",
142+
)
143+
await expect(
144+
zipJson.zipToFile(undefined as any, outputPath),
145+
).rejects.toThrow("patterns must be an array of strings")
146+
await expect(
147+
zipJson.zipToFile("*.txt" as any, outputPath),
148+
).rejects.toThrow("patterns must be an array of strings")
149+
await expect(
150+
zipJson.zipToFile([null] as any, outputPath),
151+
).rejects.toThrow("patterns[0] must be a string, got object")
152+
await expect(zipJson.zipToFile([""], outputPath)).rejects.toThrow(
153+
"patterns[0] must be a non-empty string",
154+
)
155+
})
156+
})
157+
158+
describe("Standalone functions", () => {
159+
test("zip() function validates patterns parameter", async () => {
160+
// Valid cases
161+
await expect(zip([])).resolves.toBeDefined()
162+
await expect(zip(["*.txt"])).resolves.toBeDefined()
163+
164+
// Invalid cases
165+
await expect(zip(null as any)).rejects.toThrow(
166+
"patterns must be an array of strings",
167+
)
168+
await expect(zip(undefined as any)).rejects.toThrow(
169+
"patterns must be an array of strings",
170+
)
171+
await expect(zip({} as any)).rejects.toThrow(
172+
"patterns must be an array of strings",
173+
)
174+
await expect(zip([{}] as any)).rejects.toThrow(
175+
"patterns[0] must be a string, got object",
176+
)
177+
await expect(zip(["\t\n"])).rejects.toThrow(
178+
"patterns[0] must be a non-empty string",
179+
)
180+
})
181+
182+
test("zipToFile() function validates patterns parameter", async () => {
183+
const outputPath = join(testDir, "standalone.json")
184+
185+
// Valid cases
186+
await expect(zipToFile([], outputPath)).resolves.toBeUndefined()
187+
await expect(zipToFile(["*.txt"], outputPath)).resolves.toBeUndefined()
188+
189+
// Invalid cases
190+
await expect(zipToFile(123 as any, outputPath)).rejects.toThrow(
191+
"patterns must be an array of strings",
192+
)
193+
await expect(
194+
zipToFile(["valid", 456] as any, outputPath),
195+
).rejects.toThrow("patterns[1] must be a string, got number")
196+
await expect(zipToFile(["valid", ""], outputPath)).rejects.toThrow(
197+
"patterns[1] must be a non-empty string",
198+
)
199+
})
200+
})
201+
202+
describe("Mixed valid and invalid patterns", () => {
203+
test("identifies first invalid pattern in mixed array", async () => {
204+
await expect(zip(["*.js", "", "*.ts"])).rejects.toThrow(
205+
"patterns[1] must be a non-empty string",
206+
)
207+
await expect(zip(["*.js", 123, "*.ts"] as any)).rejects.toThrow(
208+
"patterns[1] must be a string, got number",
209+
)
210+
await expect(zip(["*.js", "*.ts", null] as any)).rejects.toThrow(
211+
"patterns[2] must be a string, got object",
212+
)
213+
})
214+
215+
test("passes for all valid patterns", async () => {
216+
await expect(
217+
zip(["*.js", "src/**/*.ts", "!node_modules/**", "*.{json,yml}"]),
218+
).resolves.toBeDefined()
219+
})
220+
})
221+
})

tests/unit/validation.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { describe, expect, test } from "bun:test"
2+
import { validatePatterns } from "../../src/utils/validation.js"
3+
4+
describe("Validation utilities", () => {
5+
describe("validatePatterns", () => {
6+
test("passes for valid string array", () => {
7+
expect(() => validatePatterns(["*.js", "*.ts"])).not.toThrow()
8+
})
9+
10+
test("passes for empty array", () => {
11+
expect(() => validatePatterns([])).not.toThrow()
12+
})
13+
14+
test("throws for null/undefined", () => {
15+
expect(() => validatePatterns(null as any)).toThrow(
16+
"patterns must be an array of strings",
17+
)
18+
expect(() => validatePatterns(undefined as any)).toThrow(
19+
"patterns must be an array of strings",
20+
)
21+
})
22+
23+
test("throws for non-array types", () => {
24+
expect(() => validatePatterns("*.js" as any)).toThrow(
25+
"patterns must be an array of strings",
26+
)
27+
expect(() => validatePatterns(123 as any)).toThrow(
28+
"patterns must be an array of strings",
29+
)
30+
expect(() => validatePatterns({} as any)).toThrow(
31+
"patterns must be an array of strings",
32+
)
33+
})
34+
35+
test("throws for array with non-string elements", () => {
36+
expect(() => validatePatterns(["*.js", 123] as any)).toThrow(
37+
"patterns[1] must be a string, got number",
38+
)
39+
expect(() => validatePatterns([null, "*.js"] as any)).toThrow(
40+
"patterns[0] must be a string, got object",
41+
)
42+
expect(() => validatePatterns(["*.js", {}] as any)).toThrow(
43+
"patterns[1] must be a string, got object",
44+
)
45+
})
46+
47+
test("throws for array with empty string elements", () => {
48+
expect(() => validatePatterns(["*.js", ""])).toThrow(
49+
"patterns[1] must be a non-empty string",
50+
)
51+
expect(() => validatePatterns(["", "*.js"])).toThrow(
52+
"patterns[0] must be a non-empty string",
53+
)
54+
expect(() => validatePatterns(["*.js", " "])).toThrow(
55+
"patterns[1] must be a non-empty string",
56+
)
57+
})
58+
59+
test("throws for array with whitespace-only string elements", () => {
60+
expect(() => validatePatterns(["\t\n ", "*.js"])).toThrow(
61+
"patterns[0] must be a non-empty string",
62+
)
63+
expect(() => validatePatterns(["*.js", "\t"])).toThrow(
64+
"patterns[1] must be a non-empty string",
65+
)
66+
})
67+
68+
test("passes for array with valid string patterns", () => {
69+
expect(() =>
70+
validatePatterns([
71+
"*.js",
72+
"src/**/*.ts",
73+
"!node_modules/**",
74+
"*.{json,yaml}",
75+
]),
76+
).not.toThrow()
77+
})
78+
})
79+
})

0 commit comments

Comments
 (0)