Skip to content

Commit 525f8f2

Browse files
committed
Fix npm purl lookup in info
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 27c4fc9 commit 525f8f2

2 files changed

Lines changed: 120 additions & 12 deletions

File tree

‎src/fetchcode/package.py‎

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ def get_pypi_codeview_url(project_urls):
7171
return code_view_url
7272

7373

74+
def get_npm_field_url(value):
75+
"""
76+
Return a URL from an npm ``repository`` or ``bugs`` field.
77+
78+
The registry stores these as either a URL string or an object with a ``url``.
79+
"""
80+
if isinstance(value, str):
81+
return value or None
82+
if isinstance(value, dict):
83+
url = value.get("url")
84+
if isinstance(url, str):
85+
return url or None
86+
return None
87+
88+
7489
@router.route("pkg:cargo/.*")
7590
def get_cargo_data_from_purl(purl):
7691
"""
@@ -121,28 +136,39 @@ def get_npm_data_from_purl(purl):
121136
purl = PackageURL.from_string(purl)
122137
base_path = "http://registry.npmjs.org"
123138
name = purl.name
139+
namespace = purl.namespace
140+
# Scoped packages are published as @scope/name. Keep namespace and name
141+
# separate so the emitted Package PURL stays pkg:npm/%40scope/name.
142+
registry_name = f"{namespace}/{name}" if namespace else name
124143
version = purl.version
125-
api_url = f"{base_path}/{name}"
144+
api_url = f"{base_path}/{registry_name}"
126145

127146
response = get_response(api_url)
128-
vcs_data = response.get("repository") or {}
129-
bugs = response.get("bugs") or {}
130-
download_url = f"{base_path}/{name}/-/{name}-{version}.tgz" if version else None
131-
vcs_url = vcs_data.get("url")
132-
bug_tracking_url = bugs.get("url")
147+
download_url = (
148+
f"{base_path}/{registry_name}/-/{name}-{version}.tgz" if version else None
149+
)
150+
vcs_url = get_npm_field_url(response.get("repository"))
151+
bug_tracking_url = get_npm_field_url(response.get("bugs"))
133152
license = response.get("license")
134153
homepage_url = response.get("homepage")
135154

136-
versions = response.get("versions", [])
155+
versions = response.get("versions") or {}
137156
for num in versions:
138157
version = versions[num]
139-
version_purl = PackageURL(type=purl.type, name=name, version=version.get("version"))
140-
repository = version.get("repository") or {}
141-
bugs = response.get("bugs") or {}
158+
if not isinstance(version, dict):
159+
continue
160+
version_purl = PackageURL(
161+
type=purl.type,
162+
namespace=namespace,
163+
name=name,
164+
version=version.get("version"),
165+
)
142166
dist = version.get("dist") or {}
143-
vcs_url = repository.get("url")
167+
if not isinstance(dist, dict):
168+
dist = {}
169+
vcs_url = get_npm_field_url(version.get("repository"))
144170
download_url = dist.get("tarball")
145-
bug_tracking_url = bugs.get("url")
171+
bug_tracking_url = get_npm_field_url(version.get("bugs") or response.get("bugs"))
146172
declared_license = license
147173

148174
if purl.version and version_purl.version != purl.version:

‎tests/test_package.py‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,91 @@ def test_npm_packages(mock_get):
6767
mock_get.side_effect = side_effect
6868
packages = list(info(purl))
6969

70+
mock_get.assert_called_once_with("http://registry.npmjs.org/express")
7071
check_packages(packages, expected_data)
7172

7273

74+
SCOPED_NPM_REGISTRY = {
75+
"name": "@angular/core",
76+
"repository": {"url": "git+https://github.com/angular/angular.git"},
77+
"bugs": {"url": "https://github.com/angular/angular/issues"},
78+
"license": "MIT",
79+
"homepage": "https://angular.io",
80+
"versions": {
81+
"1.0.0": {
82+
"version": "1.0.0",
83+
"repository": {"type": "git", "url": "git+https://github.com/angular/angular.git"},
84+
"dist": {"tarball": "https://registry.npmjs.org/@angular/core/-/core-1.0.0.tgz"},
85+
},
86+
"1.0.1": {
87+
"version": "1.0.1",
88+
"repository": {"type": "git", "url": "git+https://github.com/angular/angular.git"},
89+
"dist": {"tarball": "https://registry.npmjs.org/@angular/core/-/core-1.0.1.tgz"},
90+
},
91+
},
92+
}
93+
94+
95+
@mock.patch("fetchcode.package.get_response")
96+
def test_npm_scoped_packages(mock_get):
97+
mock_get.return_value = SCOPED_NPM_REGISTRY
98+
packages = list(info("pkg:npm/%40angular/core"))
99+
100+
mock_get.assert_called_once_with("http://registry.npmjs.org/@angular/core")
101+
assert [p.version for p in packages] == ["1.0.0", "1.0.1"]
102+
assert all(p.namespace == "@angular" and p.name == "core" for p in packages)
103+
assert packages[0].purl == "pkg:npm/%40angular/core@1.0.0"
104+
assert packages[0].api_url == "http://registry.npmjs.org/@angular/core"
105+
assert packages[0].download_url == "https://registry.npmjs.org/@angular/core/-/core-1.0.0.tgz"
106+
assert packages[0].homepage_url == "https://angular.io"
107+
assert packages[0].bug_tracking_url == "https://github.com/angular/angular/issues"
108+
assert packages[0].vcs_url == "git+https://github.com/angular/angular.git"
109+
assert packages[0].declared_license == "MIT"
110+
111+
112+
@mock.patch("fetchcode.package.get_response")
113+
def test_npm_scoped_package_with_version(mock_get):
114+
mock_get.return_value = SCOPED_NPM_REGISTRY
115+
packages = list(info("pkg:npm/%40angular/core@1.0.1"))
116+
117+
mock_get.assert_called_once_with("http://registry.npmjs.org/@angular/core")
118+
assert len(packages) == 1
119+
package = packages[0]
120+
assert package.namespace == "@angular"
121+
assert package.name == "core"
122+
assert package.version == "1.0.1"
123+
assert package.purl == "pkg:npm/%40angular/core@1.0.1"
124+
assert package.download_url == "https://registry.npmjs.org/@angular/core/-/core-1.0.1.tgz"
125+
126+
127+
@mock.patch("fetchcode.package.get_response")
128+
def test_npm_string_repository_and_bugs(mock_get):
129+
mock_get.return_value = {
130+
"name": "@babel/core",
131+
"repository": "https://github.com/babel/babel.git",
132+
"bugs": "https://github.com/babel/babel/issues",
133+
"license": "MIT",
134+
"homepage": "https://babeljs.io",
135+
"versions": {
136+
"7.0.0": {
137+
"version": "7.0.0",
138+
"repository": "https://github.com/babel/babel.git",
139+
"bugs": "https://github.com/babel/babel/issues",
140+
"dist": {"tarball": "https://registry.npmjs.org/@babel/core/-/core-7.0.0.tgz"},
141+
}
142+
},
143+
}
144+
packages = list(info("pkg:npm/%40babel/core@7.0.0"))
145+
146+
assert len(packages) == 1
147+
package = packages[0]
148+
assert package.namespace == "@babel"
149+
assert package.name == "core"
150+
assert package.vcs_url == "https://github.com/babel/babel.git"
151+
assert package.bug_tracking_url == "https://github.com/babel/babel/issues"
152+
assert package.download_url == "https://registry.npmjs.org/@babel/core/-/core-7.0.0.tgz"
153+
154+
73155
@mock.patch("fetchcode.package.get_response")
74156
def test_pypi_packages(mock_get):
75157
side_effect = [load_json("tests/data/pypi_mock_data.json")]

0 commit comments

Comments
 (0)