Skip to content

Commit 4fa5cd8

Browse files
authored
Merge pull request #58 from sonukapoor/codex/issue-57-helper-tests
test: add helper unit tests
2 parents 42ed18f + 368b9e1 commit 4fa5cd8

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

tests/helpers.test.ts

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import fs from "node:fs";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { parseArgs } from "../src/cli/args.js";
5+
import { maxSeverity, inferSeverity, normalizeSeverity } from "../src/osv/severity.js";
6+
import {
7+
chooseBestLockfile,
8+
findFiles,
9+
findNearestPackageJson,
10+
relativeOrName,
11+
safeReadText,
12+
} from "../src/utils/file.js";
13+
import {
14+
compareVersions,
15+
looksLikeVersion,
16+
normalizeRawVersion,
17+
parseExactManifestVersion,
18+
} from "../src/utils/version.js";
19+
20+
function createTempDir(): string {
21+
return fs.mkdtempSync(path.join(os.tmpdir(), "cve-lite-helper-test-"));
22+
}
23+
24+
function removeDir(dirPath: string) {
25+
fs.rmSync(dirPath, { recursive: true, force: true });
26+
}
27+
28+
describe("parseArgs", () => {
29+
it("returns default options when no arguments are provided", () => {
30+
const result = parseArgs([]);
31+
32+
expect(result).toEqual({
33+
options: {
34+
failOn: "critical",
35+
batchSize: "100",
36+
searchDepth: "4",
37+
minSeverity: "medium",
38+
},
39+
});
40+
});
41+
42+
it("parses flags, inline values, and a project path together", () => {
43+
const result = parseArgs([
44+
"--json",
45+
"--verbose",
46+
"--prod-only",
47+
"--offline",
48+
"--all",
49+
"--fail-on=high",
50+
"--batch-size",
51+
"25",
52+
"--cache-dir=.cache/test",
53+
"--osv-url",
54+
"https://example.com/osv",
55+
"--search-depth=7",
56+
"--min-severity",
57+
"low",
58+
"./fixture",
59+
]);
60+
61+
expect(result).toEqual({
62+
options: {
63+
json: true,
64+
verbose: true,
65+
prodOnly: true,
66+
offline: true,
67+
all: true,
68+
failOn: "high",
69+
batchSize: "25",
70+
cacheDir: ".cache/test",
71+
osvUrl: "https://example.com/osv",
72+
searchDepth: "7",
73+
minSeverity: "low",
74+
},
75+
projectArg: "./fixture",
76+
});
77+
});
78+
79+
it("throws on unknown options and unexpected extra arguments", () => {
80+
expect(() => parseArgs(["--wat"])).toThrow("Unknown option: --wat");
81+
expect(() => parseArgs(["project-a", "project-b"])).toThrow("Unexpected argument: project-b");
82+
});
83+
});
84+
85+
describe("severity helpers", () => {
86+
it("infers severity from score ranges and database fallback", () => {
87+
expect(inferSeverity({ id: "1", severity: [{ score: "9.8" }] })).toBe("critical");
88+
expect(inferSeverity({ id: "2", severity: [{ score: "7.5" }] })).toBe("high");
89+
expect(inferSeverity({ id: "3", severity: [{ score: "5.6" }] })).toBe("medium");
90+
expect(inferSeverity({ id: "4", severity: [{ score: "2.1" }] })).toBe("low");
91+
expect(inferSeverity({ id: "5", severity: [{ score: "0.0" }] })).toBe("none");
92+
expect(inferSeverity({ id: "6", database_specific: { severity: "HIGH" } })).toBe("high");
93+
expect(inferSeverity({ id: "7" })).toBe("unknown");
94+
});
95+
96+
it("returns the highest severity across multiple vulnerabilities", () => {
97+
expect(
98+
maxSeverity([
99+
{ id: "1", severity: [{ score: "3.0" }] },
100+
{ id: "2", severity: [{ score: "9.1" }] },
101+
{ id: "3", database_specific: { severity: "medium" } },
102+
]),
103+
).toBe("critical");
104+
});
105+
106+
it("normalizes valid labels and falls back invalid ones to critical", () => {
107+
expect(normalizeSeverity("HIGH")).toBe("high");
108+
expect(normalizeSeverity("unknown")).toBe("unknown");
109+
expect(normalizeSeverity("not-a-level")).toBe("critical");
110+
});
111+
});
112+
113+
describe("version helpers", () => {
114+
it("recognizes supported exact versions", () => {
115+
expect(looksLikeVersion("1.2.3")).toBe(true);
116+
expect(looksLikeVersion("1.2.3-beta")).toBe(true);
117+
expect(looksLikeVersion("1.2")).toBe(false);
118+
expect(looksLikeVersion("^1.2.3")).toBe(false);
119+
});
120+
121+
it("compares versions numerically and with suffix segments", () => {
122+
expect(compareVersions("1.2.3", "1.2.4")).toBeLessThan(0);
123+
expect(compareVersions("2.0.0", "1.9.9")).toBeGreaterThan(0);
124+
expect(compareVersions("1.2.3", "1.2.3")).toBe(0);
125+
expect(compareVersions("1.2.3-beta", "1.2.3-alpha")).toBeGreaterThan(0);
126+
});
127+
128+
it("parses exact manifest versions and normalizes raw versions", () => {
129+
expect(parseExactManifestVersion("1.2.3")).toBe("1.2.3");
130+
expect(parseExactManifestVersion(" npm:1.2.3 ")).toBe("1.2.3");
131+
expect(parseExactManifestVersion("^1.2.3")).toBeNull();
132+
133+
expect(normalizeRawVersion("workspace:1.2.3")).toBe("1.2.3");
134+
expect(normalizeRawVersion("npm:4.5.6")).toBe("4.5.6");
135+
expect(normalizeRawVersion("../local-package")).toBeNull();
136+
expect(normalizeRawVersion(42)).toBeNull();
137+
});
138+
});
139+
140+
describe("file helpers", () => {
141+
it("safely reads files and returns an empty string for missing paths", () => {
142+
const tempDir = createTempDir();
143+
const filePath = path.join(tempDir, "note.txt");
144+
fs.writeFileSync(filePath, "hello", "utf8");
145+
146+
try {
147+
expect(safeReadText(filePath)).toBe("hello");
148+
expect(safeReadText(path.join(tempDir, "missing.txt"))).toBe("");
149+
} finally {
150+
removeDir(tempDir);
151+
}
152+
});
153+
154+
it("returns relative paths when possible and falls back to the file name", () => {
155+
const rootDir = "/tmp/project";
156+
expect(relativeOrName(rootDir, "/tmp/project/src/index.ts")).toBe(path.join("src", "index.ts"));
157+
expect(relativeOrName(rootDir, rootDir)).toBe("project");
158+
});
159+
160+
it("finds matching files by depth while skipping excluded directories", () => {
161+
const tempDir = createTempDir();
162+
const nestedDir = path.join(tempDir, "packages", "app");
163+
const gitDir = path.join(tempDir, ".git", "hooks");
164+
const nodeModulesDir = path.join(tempDir, "node_modules", "left-pad");
165+
166+
fs.mkdirSync(nestedDir, { recursive: true });
167+
fs.mkdirSync(gitDir, { recursive: true });
168+
fs.mkdirSync(nodeModulesDir, { recursive: true });
169+
170+
const rootLock = path.join(tempDir, "package-lock.json");
171+
const nestedLock = path.join(nestedDir, "yarn.lock");
172+
const ignoredLock = path.join(nodeModulesDir, "package-lock.json");
173+
174+
fs.writeFileSync(rootLock, "{}", "utf8");
175+
fs.writeFileSync(nestedLock, "content", "utf8");
176+
fs.writeFileSync(ignoredLock, "{}", "utf8");
177+
178+
try {
179+
const files = findFiles(tempDir, ["package-lock.json", "yarn.lock"], 3);
180+
expect(files).toEqual([rootLock, nestedLock]);
181+
} finally {
182+
removeDir(tempDir);
183+
}
184+
});
185+
186+
it("finds the nearest package.json and chooses the preferred lockfile", () => {
187+
const tempDir = createTempDir();
188+
const nestedDir = path.join(tempDir, "packages", "app");
189+
fs.mkdirSync(nestedDir, { recursive: true });
190+
191+
try {
192+
expect(findNearestPackageJson(tempDir, 3)).toBeNull();
193+
194+
const nestedPackageJson = path.join(nestedDir, "package.json");
195+
fs.writeFileSync(nestedPackageJson, "{}", "utf8");
196+
expect(findNearestPackageJson(tempDir, 3)).toBe(nestedPackageJson);
197+
198+
const rootPackageJson = path.join(tempDir, "package.json");
199+
fs.writeFileSync(rootPackageJson, "{}", "utf8");
200+
expect(findNearestPackageJson(tempDir, 3)).toBe(rootPackageJson);
201+
202+
expect(
203+
chooseBestLockfile([
204+
path.join(tempDir, "packages", "app", "yarn.lock"),
205+
path.join(tempDir, "pnpm-lock.yaml"),
206+
path.join(tempDir, "package-lock.json"),
207+
]),
208+
).toBe(path.join(tempDir, "package-lock.json"));
209+
} finally {
210+
removeDir(tempDir);
211+
}
212+
});
213+
});

0 commit comments

Comments
 (0)