Skip to content

Commit 4845de9

Browse files
authored
Merge pull request #62 from sonukapoor/codex/issue-61-parent-upgrade-tests
test: add parent upgrade remediation tests
2 parents 5dcd805 + dbc8992 commit 4845de9

1 file changed

Lines changed: 198 additions & 0 deletions

File tree

tests/parent-upgrade.test.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { jest } from "@jest/globals";
2+
import type { Finding, PackageRef } from "../src/types.js";
3+
4+
const fetchMock = jest.fn();
5+
global.fetch = fetchMock as unknown as typeof fetch;
6+
7+
function createPackages(): PackageRef[] {
8+
return [
9+
{
10+
name: "app",
11+
version: "1.0.0",
12+
ecosystem: "npm",
13+
paths: [["project", "app"]],
14+
},
15+
{
16+
name: "mid",
17+
version: "2.0.0",
18+
ecosystem: "npm",
19+
paths: [["project", "app", "mid"]],
20+
},
21+
{
22+
name: "lodash",
23+
version: "4.17.20",
24+
ecosystem: "npm",
25+
paths: [
26+
["project", "app", "lodash"],
27+
["project", "app", "mid", "lodash"],
28+
],
29+
},
30+
];
31+
}
32+
33+
function createFinding(overrides?: Partial<Finding>): Finding {
34+
return {
35+
pkg: {
36+
name: "lodash",
37+
version: "4.17.20",
38+
ecosystem: "npm",
39+
paths: [["project", "app", "lodash"]],
40+
},
41+
vulnerabilities: [{ id: "OSV-123" }],
42+
severity: "high",
43+
cveAliases: [],
44+
dependencyPaths: [["project", "app", "lodash"]],
45+
relationship: "transitive",
46+
firstFixedVersion: "4.17.21",
47+
recommendedParentUpgrade: undefined,
48+
...overrides,
49+
};
50+
}
51+
52+
function mockPackument(data: unknown, ok = true) {
53+
fetchMock.mockResolvedValue({
54+
ok,
55+
json: async () => data,
56+
});
57+
}
58+
59+
async function loadResolver() {
60+
const module = await import(`../src/remediation/parent-upgrade.js?test=${Date.now()}-${Math.random()}`);
61+
return module.resolveRecommendedParentUpgrade;
62+
}
63+
64+
describe("resolveRecommendedParentUpgrade", () => {
65+
beforeEach(() => {
66+
fetchMock.mockReset();
67+
});
68+
69+
it("returns null for non-transitive findings or missing usable paths", async () => {
70+
const resolveRecommendedParentUpgrade = await loadResolver();
71+
const packages = createPackages();
72+
73+
await expect(
74+
resolveRecommendedParentUpgrade(
75+
createFinding({ relationship: "direct" }),
76+
packages,
77+
),
78+
).resolves.toBeNull();
79+
80+
await expect(
81+
resolveRecommendedParentUpgrade(
82+
createFinding({ dependencyPaths: [], pkg: { name: "lodash", version: "4.17.20", ecosystem: "npm" } }),
83+
packages,
84+
),
85+
).resolves.toBeNull();
86+
87+
expect(fetchMock).not.toHaveBeenCalled();
88+
});
89+
90+
it("returns null when the direct parent cannot be found in the package list", async () => {
91+
const resolveRecommendedParentUpgrade = await loadResolver();
92+
const finding = createFinding({
93+
dependencyPaths: [["project", "missing-parent", "lodash"]],
94+
});
95+
96+
await expect(resolveRecommendedParentUpgrade(finding, createPackages())).resolves.toBeNull();
97+
expect(fetchMock).not.toHaveBeenCalled();
98+
});
99+
100+
it("recommends an exact direct-child parent upgrade when a newer parent stops allowing the vulnerable version", async () => {
101+
const resolveRecommendedParentUpgrade = await loadResolver();
102+
mockPackument({
103+
versions: {
104+
"1.0.0": { dependencies: { lodash: "^4.17.20" } },
105+
"1.0.5": { dependencies: { lodash: "^4.17.20" } },
106+
"1.1.0": { dependencies: { lodash: "^4.17.21" } },
107+
},
108+
});
109+
110+
const result = await resolveRecommendedParentUpgrade(createFinding(), createPackages());
111+
112+
expect(result).toMatchObject({
113+
package: "app",
114+
currentVersion: "1.0.0",
115+
targetVersion: "1.1.0",
116+
vulnerablePackage: "lodash",
117+
confidence: "exact-direct-child",
118+
});
119+
expect(result?.reason).toContain("no longer allows lodash@4.17.20");
120+
expect(result?.reason).toContain("allows 4.17.21+");
121+
});
122+
123+
it("recommends a best-effort upgrade for deeper paths when the direct parent stops allowing the current intermediate version", async () => {
124+
const resolveRecommendedParentUpgrade = await loadResolver();
125+
mockPackument({
126+
versions: {
127+
"1.0.0": { dependencies: { mid: "^2.0.0" } },
128+
"1.1.0": { dependencies: { mid: "^2.0.0" } },
129+
"2.0.0": { dependencies: { mid: "^3.0.0" } },
130+
},
131+
});
132+
133+
const finding = createFinding({
134+
dependencyPaths: [["project", "app", "mid", "lodash"]],
135+
pkg: {
136+
name: "lodash",
137+
version: "4.17.20",
138+
ecosystem: "npm",
139+
paths: [["project", "app", "mid", "lodash"]],
140+
},
141+
});
142+
143+
const result = await resolveRecommendedParentUpgrade(finding, createPackages());
144+
145+
expect(result).toMatchObject({
146+
package: "app",
147+
currentVersion: "1.0.0",
148+
targetVersion: "2.0.0",
149+
vulnerablePackage: "lodash",
150+
confidence: "best-effort",
151+
});
152+
expect(result?.reason).toContain("no longer allows mid@2.0.0");
153+
});
154+
155+
it("returns null when the immediate parent version is missing or invalid in deeper paths", async () => {
156+
const resolveRecommendedParentUpgrade = await loadResolver();
157+
mockPackument({
158+
versions: {
159+
"1.1.0": { dependencies: { mid: "^3.0.0" } },
160+
},
161+
});
162+
163+
const packages: PackageRef[] = [
164+
{
165+
name: "app",
166+
version: "1.0.0",
167+
ecosystem: "npm",
168+
paths: [["project", "app"]],
169+
},
170+
{
171+
name: "lodash",
172+
version: "4.17.20",
173+
ecosystem: "npm",
174+
paths: [["project", "app", "mid", "lodash"]],
175+
},
176+
];
177+
178+
const finding = createFinding({
179+
dependencyPaths: [["project", "app", "mid", "lodash"]],
180+
pkg: {
181+
name: "lodash",
182+
version: "4.17.20",
183+
ecosystem: "npm",
184+
paths: [["project", "app", "mid", "lodash"]],
185+
},
186+
});
187+
188+
await expect(resolveRecommendedParentUpgrade(finding, packages)).resolves.toBeNull();
189+
});
190+
191+
it("returns null when the registry packument cannot be fetched successfully", async () => {
192+
const resolveRecommendedParentUpgrade = await loadResolver();
193+
mockPackument({}, false);
194+
195+
await expect(resolveRecommendedParentUpgrade(createFinding(), createPackages())).resolves.toBeNull();
196+
expect(fetchMock).toHaveBeenCalledTimes(1);
197+
});
198+
});

0 commit comments

Comments
 (0)