Merge pull request #114 from AikidoSec/handle-package-without-version

Fix crash when a package does not contain a version (retracted packages)
This commit is contained in:
bitterpanda 2025-10-21 15:57:11 +02:00 committed by GitHub
commit 6a69eec342
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 215 additions and 0 deletions

View file

@ -25,6 +25,10 @@ export async function resolvePackageVersion(packageName, versionRange) {
return distTags[versionRange]; return distTags[versionRange];
} }
if (!packageInfo.versions) {
return null;
}
// If the version range is not a dist-tag, we need to resolve the highest version matching the range. // If the version range is not a dist-tag, we need to resolve the highest version matching the range.
// This is useful for ranges like "^1.0.0" or "~2.3.4". // This is useful for ranges like "^1.0.0" or "~2.3.4".
const availableVersions = Object.keys(packageInfo.versions); const availableVersions = Object.keys(packageInfo.versions);

View file

@ -0,0 +1,211 @@
import { describe, it, mock } from "node:test";
import assert from "node:assert";
describe("resolvePackageVersion", async () => {
const mockNpmFetchJson = mock.fn();
mock.module("npm-registry-fetch", {
namedExports: {
json: mockNpmFetchJson,
},
});
const { resolvePackageVersion } = await import("./npmApi.js");
it("should return the version if it is already a fixed version", async () => {
const result = await resolvePackageVersion("express", "4.17.1");
assert.strictEqual(result, "4.17.1");
});
it("should use 'latest' as default version range when not provided", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
"dist-tags": {
latest: "4.18.2",
},
versions: {
"4.18.2": {},
},
}));
const result = await resolvePackageVersion("express");
assert.strictEqual(result, "4.18.2");
});
it("should resolve dist-tag versions", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
"dist-tags": {
latest: "4.18.2",
next: "5.0.0-beta.1",
},
versions: {
"4.18.2": {},
"5.0.0-beta.1": {},
},
}));
const result = await resolvePackageVersion("express", "next");
assert.strictEqual(result, "5.0.0-beta.1");
});
it("should resolve version ranges using semver", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
"dist-tags": {
latest: "4.18.2",
},
versions: {
"4.16.0": {},
"4.17.0": {},
"4.17.1": {},
"4.18.0": {},
"4.18.2": {},
},
}));
const result = await resolvePackageVersion("express", "^4.17.0");
assert.strictEqual(result, "4.18.2");
});
it("should resolve tilde ranges correctly", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
"dist-tags": {
latest: "4.18.2",
},
versions: {
"4.17.0": {},
"4.17.1": {},
"4.17.3": {},
"4.18.0": {},
},
}));
const result = await resolvePackageVersion("express", "~4.17.0");
assert.strictEqual(result, "4.17.3");
});
it("should return null if package info cannot be fetched", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => {
throw new Error("Package not found");
});
const result = await resolvePackageVersion("non-existent-package", "latest");
assert.strictEqual(result, null);
});
it("should return null if no versions match the range", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
"dist-tags": {
latest: "1.0.0",
},
versions: {
"1.0.0": {},
"1.1.0": {},
},
}));
const result = await resolvePackageVersion("express", "^5.0.0");
assert.strictEqual(result, null);
});
it("should return null if dist-tag does not exist", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
"dist-tags": {
latest: "4.18.2",
},
versions: {
"4.18.2": {},
},
}));
const result = await resolvePackageVersion("express", "nonexistent-tag");
assert.strictEqual(result, null);
});
it("should return null if package info has no versions property (retracted package)", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
_id: "zenn",
name: "zenn",
time: {
modified: "2021-04-20T16:20:56.084Z",
created: "2017-07-10T19:48:07.891Z",
unpublished: {
time: "2021-04-20T16:20:56.084Z",
versions: [
"0.9.0",
"0.9.1",
"0.9.2",
"0.9.3",
"0.9.4",
"0.9.5",
"0.9.6",
"0.9.8",
"0.9.9",
"0.9.10",
"0.9.11",
"0.9.12",
"0.9.13",
"0.9.14",
],
},
},
}));
const result = await resolvePackageVersion("zenn", "^0.9.0");
assert.strictEqual(result, null);
});
it("should return dist-tag version even if versions property is missing", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
"dist-tags": {
latest: "4.18.2",
},
}));
const result = await resolvePackageVersion("express", "latest");
assert.strictEqual(result, "4.18.2");
});
it("should handle scoped packages", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
"dist-tags": {
latest: "1.2.3",
},
versions: {
"1.2.3": {},
},
}));
const result = await resolvePackageVersion("@scope/package", "latest");
assert.strictEqual(result, "1.2.3");
});
it("should handle complex version ranges", async () => {
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
"dist-tags": {
latest: "2.5.0",
},
versions: {
"1.0.0": {},
"2.0.0": {},
"2.3.0": {},
"2.4.0": {},
"2.5.0": {},
"3.0.0": {},
},
}));
const result = await resolvePackageVersion("express", ">=2.0.0 <3.0.0");
assert.strictEqual(result, "2.5.0");
});
});