Skip to content

Commit d27d524

Browse files
authored
Fix npm purl lookup in info (#217)
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 27c4fc9 commit d27d524

2 files changed

Lines changed: 113 additions & 19 deletions

File tree

‎src/fetchcode/package.py‎

Lines changed: 36 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,37 @@ 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 = f"{base_path}/{registry_name}/-/{name}-{version}.tgz" if version else None
148+
vcs_url = get_npm_field_url(response.get("repository"))
149+
bug_tracking_url = get_npm_field_url(response.get("bugs"))
133150
license = response.get("license")
134151
homepage_url = response.get("homepage")
135152

136-
versions = response.get("versions", [])
153+
versions = response.get("versions") or {}
137154
for num in versions:
138155
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 {}
156+
if not isinstance(version, dict):
157+
continue
158+
version_purl = PackageURL(
159+
type=purl.type,
160+
namespace=namespace,
161+
name=name,
162+
version=version.get("version"),
163+
)
142164
dist = version.get("dist") or {}
143-
vcs_url = repository.get("url")
165+
if not isinstance(dist, dict):
166+
dist = {}
167+
vcs_url = get_npm_field_url(version.get("repository"))
144168
download_url = dist.get("tarball")
145-
bug_tracking_url = bugs.get("url")
169+
bug_tracking_url = get_npm_field_url(version.get("bugs") or response.get("bugs"))
146170
declared_license = license
147171

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

‎tests/test_package.py‎

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,85 @@ def test_cargo_packages(mock_get):
5959
check_packages(packages, expected_data)
6060

6161

62+
SCOPED_NPM_REGISTRY = {
63+
"name": "@angular/core",
64+
"repository": {"url": "git+https://github.com/angular/angular.git"},
65+
"bugs": {"url": "https://github.com/angular/angular/issues"},
66+
"license": "MIT",
67+
"homepage": "https://angular.io",
68+
"versions": {
69+
"1.0.0": {
70+
"version": "1.0.0",
71+
"repository": {"type": "git", "url": "git+https://github.com/angular/angular.git"},
72+
"dist": {"tarball": "https://registry.npmjs.org/@angular/core/-/core-1.0.0.tgz"},
73+
},
74+
"1.0.1": {
75+
"version": "1.0.1",
76+
"repository": {"type": "git", "url": "git+https://github.com/angular/angular.git"},
77+
"dist": {"tarball": "https://registry.npmjs.org/@angular/core/-/core-1.0.1.tgz"},
78+
},
79+
},
80+
}
81+
82+
6283
@mock.patch("fetchcode.package.get_response")
63-
def test_npm_packages(mock_get):
64-
side_effect = [load_json("tests/data/npm_mock_data.json")]
65-
purl = "pkg:npm/express"
66-
expected_data = "tests/data/npm.json"
67-
mock_get.side_effect = side_effect
68-
packages = list(info(purl))
84+
def test_npm_scoped_packages(mock_get):
85+
mock_get.return_value = SCOPED_NPM_REGISTRY
86+
packages = list(info("pkg:npm/%40angular/core"))
87+
88+
mock_get.assert_called_once_with("http://registry.npmjs.org/@angular/core")
89+
assert [p.version for p in packages] == ["1.0.0", "1.0.1"]
90+
assert all(p.namespace == "@angular" and p.name == "core" for p in packages)
91+
assert packages[0].purl == "pkg:npm/%40angular/core@1.0.0"
92+
assert packages[0].api_url == "http://registry.npmjs.org/@angular/core"
93+
assert packages[0].download_url == "https://registry.npmjs.org/@angular/core/-/core-1.0.0.tgz"
94+
assert packages[0].homepage_url == "https://angular.io"
95+
assert packages[0].bug_tracking_url == "https://github.com/angular/angular/issues"
96+
assert packages[0].vcs_url == "git+https://github.com/angular/angular.git"
97+
assert packages[0].declared_license == "MIT"
6998

70-
check_packages(packages, expected_data)
99+
100+
@mock.patch("fetchcode.package.get_response")
101+
def test_npm_scoped_package_with_version(mock_get):
102+
mock_get.return_value = SCOPED_NPM_REGISTRY
103+
packages = list(info("pkg:npm/%40angular/core@1.0.1"))
104+
105+
mock_get.assert_called_once_with("http://registry.npmjs.org/@angular/core")
106+
assert len(packages) == 1
107+
package = packages[0]
108+
assert package.namespace == "@angular"
109+
assert package.name == "core"
110+
assert package.version == "1.0.1"
111+
assert package.purl == "pkg:npm/%40angular/core@1.0.1"
112+
assert package.download_url == "https://registry.npmjs.org/@angular/core/-/core-1.0.1.tgz"
113+
114+
115+
@mock.patch("fetchcode.package.get_response")
116+
def test_npm_string_repository_and_bugs(mock_get):
117+
mock_get.return_value = {
118+
"name": "@babel/core",
119+
"repository": "https://github.com/babel/babel.git",
120+
"bugs": "https://github.com/babel/babel/issues",
121+
"license": "MIT",
122+
"homepage": "https://babeljs.io",
123+
"versions": {
124+
"7.0.0": {
125+
"version": "7.0.0",
126+
"repository": "https://github.com/babel/babel.git",
127+
"bugs": "https://github.com/babel/babel/issues",
128+
"dist": {"tarball": "https://registry.npmjs.org/@babel/core/-/core-7.0.0.tgz"},
129+
}
130+
},
131+
}
132+
packages = list(info("pkg:npm/%40babel/core@7.0.0"))
133+
134+
assert len(packages) == 1
135+
package = packages[0]
136+
assert package.namespace == "@babel"
137+
assert package.name == "core"
138+
assert package.vcs_url == "https://github.com/babel/babel.git"
139+
assert package.bug_tracking_url == "https://github.com/babel/babel/issues"
140+
assert package.download_url == "https://registry.npmjs.org/@babel/core/-/core-7.0.0.tgz"
71141

72142

73143
@mock.patch("fetchcode.package.get_response")

0 commit comments

Comments
 (0)