From b8347ad55c8e5a9f9553a724add1d847b25f8f28 Mon Sep 17 00:00:00 2001 From: Carl Flottmann Date: Fri, 5 Sep 2025 11:24:36 +1000 Subject: [PATCH] chore: pypi inspector link generation now lives in a dataclass in the pypi registry code Signed-off-by: Carl Flottmann --- .../pypi_heuristics/metadata/wheel_absence.py | 88 +------- src/macaron/repo_finder/repo_finder_pypi.py | 10 +- .../package_registry/pypi_registry.py | 120 ++++++++++- tests/malware_analyzer/pypi/conftest.py | 5 +- .../pypi/test_wheel_absence.py | 204 +++++------------- 5 files changed, 192 insertions(+), 235 deletions(-) diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/wheel_absence.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/wheel_absence.py index 0198a932d..8d477e1a9 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/wheel_absence.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/wheel_absence.py @@ -6,11 +6,10 @@ import logging from macaron.errors import HeuristicAnalyzerValueError -from macaron.json_tools import JsonType, json_extract +from macaron.json_tools import JsonType from macaron.malware_analyzer.pypi_heuristics.base_analyzer import BaseHeuristicAnalyzer from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult, Heuristics from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIPackageJsonAsset -from macaron.util import send_head_http_raw logger: logging.Logger = logging.getLogger(__name__) @@ -23,13 +22,6 @@ class WheelAbsenceAnalyzer(BaseHeuristicAnalyzer): heuristic fails. """ - WHEEL: str = "bdist_wheel" - # as per https://github.com/pypi/inspector/blob/main/inspector/main.py line 125 - INSPECTOR_TEMPLATE = ( - "{inspector_url_scheme}://{inspector_url_netloc}/project/" - "{name}/{version}/packages/{first}/{second}/{rest}/{filename}" - ) - def __init__(self) -> None: super().__init__( name="wheel_absence_analyzer", @@ -53,83 +45,17 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes Raises ------ HeuristicAnalyzerValueError - If there is no release information, or has other missing package information. + If there is missing package information. """ - releases = pypi_package_json.get_releases() - if releases is None: # no release information - error_msg = "There is no information for any release of this package." - logger.debug(error_msg) - raise HeuristicAnalyzerValueError(error_msg) - - version = pypi_package_json.component_version - if version is None: # check latest release version - version = pypi_package_json.get_latest_version() - - if version is None: - error_msg = "There is no latest version of this package." - logger.debug(error_msg) - raise HeuristicAnalyzerValueError(error_msg) - - # Contains a boolean field identifying if the link is reachable by this Macaron instance or not. - inspector_links: dict[str, JsonType] = {} - wheel_present: bool = False - - release_distributions = json_extract(releases, [version], list) - if release_distributions is None: - error_msg = f"The version {version} is not available as a release." + if not pypi_package_json.get_inspector_links(): + error_msg = "Unable to retrieve PyPI inspector information about package" logger.debug(error_msg) raise HeuristicAnalyzerValueError(error_msg) - for distribution in release_distributions: - # validate data - package_type = json_extract(distribution, ["packagetype"], str) - if package_type is None: - error_msg = f"The version {version} has no 'package type' field in a distribution" - logger.debug(error_msg) - raise HeuristicAnalyzerValueError(error_msg) - - name = json_extract(pypi_package_json.package_json, ["info", "name"], str) - if name is None: - error_msg = f"The version {version} has no 'name' field in a distribution" - logger.debug(error_msg) - raise HeuristicAnalyzerValueError(error_msg) - - blake2b_256 = json_extract(distribution, ["digests", "blake2b_256"], str) - if blake2b_256 is None: - error_msg = f"The version {version} has no 'blake2b_256' field in a distribution" - logger.debug(error_msg) - raise HeuristicAnalyzerValueError(error_msg) - - filename = json_extract(distribution, ["filename"], str) - if filename is None: - error_msg = f"The version {version} has no 'filename' field in a distribution" - logger.debug(error_msg) - raise HeuristicAnalyzerValueError(error_msg) - - if package_type == self.WHEEL: - wheel_present = True - - inspector_link = self.INSPECTOR_TEMPLATE.format( - inspector_url_scheme=pypi_package_json.pypi_registry.inspector_url_scheme, - inspector_url_netloc=pypi_package_json.pypi_registry.inspector_url_netloc, - name=name, - version=version, - first=blake2b_256[0:2], - second=blake2b_256[2:4], - rest=blake2b_256[4:], - filename=filename, - ) - - # use a head request because we don't care about the response contents - inspector_links[inspector_link] = False - if send_head_http_raw(inspector_link): - inspector_links[inspector_link] = True # link was reachable - - detail_info: dict[str, JsonType] = { - "inspector_links": inspector_links, - } + detail_info: dict = {"inspector_links": pypi_package_json.inspector_asset.package_link_reachability} - if wheel_present: + # At least one wheel file exists + if len(pypi_package_json.inspector_asset.package_whl_links) > 0: return HeuristicResult.PASS, detail_info return HeuristicResult.FAIL, detail_info diff --git a/src/macaron/repo_finder/repo_finder_pypi.py b/src/macaron/repo_finder/repo_finder_pypi.py index c0c273154..42ec307f5 100644 --- a/src/macaron/repo_finder/repo_finder_pypi.py +++ b/src/macaron/repo_finder/repo_finder_pypi.py @@ -9,7 +9,11 @@ from macaron.repo_finder.repo_finder_enums import RepoFinderInfo from macaron.repo_finder.repo_validator import find_valid_repository_url from macaron.slsa_analyzer.package_registry import PACKAGE_REGISTRIES, PyPIRegistry -from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIPackageJsonAsset, find_or_create_pypi_asset +from macaron.slsa_analyzer.package_registry.pypi_registry import ( + PyPIInspectorAsset, + PyPIPackageJsonAsset, + find_or_create_pypi_asset, +) from macaron.slsa_analyzer.specs.package_registry_spec import PackageRegistryInfo logger: logging.Logger = logging.getLogger(__name__) @@ -58,7 +62,9 @@ def find_repo( pypi_registry = next((registry for registry in PACKAGE_REGISTRIES if isinstance(registry, PyPIRegistry)), None) if not pypi_registry: return "", RepoFinderInfo.PYPI_NO_REGISTRY - pypi_asset = PyPIPackageJsonAsset(purl.name, purl.version, False, pypi_registry, {}, "") + pypi_asset = PyPIPackageJsonAsset( + purl.name, purl.version, False, pypi_registry, {}, "", PyPIInspectorAsset("", [], {}) + ) if not pypi_asset: # This should be unreachable, as the pypi_registry has already been confirmed to be of type PyPIRegistry. diff --git a/src/macaron/slsa_analyzer/package_registry/pypi_registry.py b/src/macaron/slsa_analyzer/package_registry/pypi_registry.py index 4f91baa59..55623f8cb 100644 --- a/src/macaron/slsa_analyzer/package_registry/pypi_registry.py +++ b/src/macaron/slsa_analyzer/package_registry/pypi_registry.py @@ -31,6 +31,7 @@ download_file_with_size_limit, html_is_js_challenge, send_get_http_raw, + send_head_http_raw, stream_file_with_size_limit, ) @@ -472,6 +473,33 @@ def extract_attestation(attestation_data: dict) -> dict | None: return attestations[0] +# as per https://github.com/pypi/inspector/blob/main/inspector/main.py line 125 +INSPECTOR_TEMPLATE = ( + "{inspector_url_scheme}://{inspector_url_netloc}/project/" + "{name}/{version}/packages/{first}/{second}/{rest}/{filename}" +) + + +@dataclass +class PyPIInspectorAsset: + """The package PyPI inspector information.""" + + #: the pypi inspector link to the tarball + package_sdist_link: str + + #: the pypi inspector link(s) to the wheel(s) + package_whl_links: list[str] + + #: a mapping of inspector links to whether they are reachable + package_link_reachability: dict[str, bool] + + def __bool__(self) -> bool: + """Determine if this inspector object is empty.""" + if (self.package_sdist_link or self.package_whl_links) and self.package_link_reachability: + return True + return False + + @dataclass class PyPIPackageJsonAsset: """The package JSON hosted on the PyPI registry.""" @@ -494,6 +522,9 @@ class PyPIPackageJsonAsset: #: the source code temporary location name package_sourcecode_path: str + #: the pypi inspector information about this package + inspector_asset: PyPIInspectorAsset + #: The size of the asset (in bytes). This attribute is added to match the AssetLocator #: protocol and is not used because pypi API registry does not provide it. @property @@ -760,6 +791,91 @@ def get_sha256(self) -> str | None: logger.debug("Found sha256 hash: %s", artifact_hash) return artifact_hash + def get_inspector_links(self) -> bool: + """Generate PyPI inspector links for this package version's distributions and fill in the inspector asset. + + Returns + ------- + bool + True if the link generation was successful, False otherwise. + """ + if self.inspector_asset: + return True + + if not self.package_json and not self.download(""): + logger.warning("No package metadata available, cannot get links") + return False + + releases = self.get_releases() + if releases is None: + logger.warning("Package has no releases, cannot create inspector links.") + return False + + version = self.component_version + if self.component_version is None: + version = self.get_latest_version() + + if version is None: + logger.warning("No version set, and no latest version exists. cannot create inspector links.") + return False + + distributions = json_extract(releases, [version], list) + + if not distributions: + logger.warning( + "Package has no distributions for release version %s. Cannot create inspector links.", version + ) + return False + + for distribution in distributions: + package_type = json_extract(distribution, ["packagetype"], str) + if package_type is None: + logger.warning("The version %s has no 'package type' field in a distribution", version) + continue + + name = json_extract(self.package_json, ["info", "name"], str) + if name is None: + logger.warning("The version %s has no 'name' field in a distribution", version) + continue + + blake2b_256 = json_extract(distribution, ["digests", "blake2b_256"], str) + if blake2b_256 is None: + logger.warning("The version %s has no 'blake2b_256' field in a distribution", version) + continue + + filename = json_extract(distribution, ["filename"], str) + if filename is None: + logger.warning("The version %s has no 'filename' field in a distribution", version) + continue + + link = INSPECTOR_TEMPLATE.format( + inspector_url_scheme=self.pypi_registry.inspector_url_scheme, + inspector_url_netloc=self.pypi_registry.inspector_url_netloc, + name=name, + version=version, + first=blake2b_256[0:2], + second=blake2b_256[2:4], + rest=blake2b_256[4:], + filename=filename, + ) + + # use a head request because we don't care about the response contents + reachable = False + if send_head_http_raw(link): + reachable = True # link was reachable + + if package_type == "sdist": + self.inspector_asset.package_sdist_link = link + self.inspector_asset.package_link_reachability[link] = reachable + elif package_type == "bdist_wheel": + self.inspector_asset.package_whl_links.append(link) + self.inspector_asset.package_link_reachability[link] = reachable + else: # no other package types exist, so else statement should never occur + logger.debug("Unknown package distribution type: %s", package_type) + + # if all distributions were invalid and went along a 'continue' path + return bool(self.inspector_asset) + def find_or_create_pypi_asset( asset_name: str, asset_version: str | None, pypi_registry_info: PackageRegistryInfo @@ -797,6 +913,8 @@ def find_or_create_pypi_asset( logger.debug("Failed to create PyPIPackageJson asset.") return None - asset = PyPIPackageJsonAsset(asset_name, asset_version, False, package_registry, {}, "") + asset = PyPIPackageJsonAsset( + asset_name, asset_version, False, package_registry, {}, "", PyPIInspectorAsset("", [], {}) + ) pypi_registry_info.metadata.append(asset) return asset diff --git a/tests/malware_analyzer/pypi/conftest.py b/tests/malware_analyzer/pypi/conftest.py index 4a583fda3..dbcea9cbe 100644 --- a/tests/malware_analyzer/pypi/conftest.py +++ b/tests/malware_analyzer/pypi/conftest.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. """This module contains test configurations for malware analyzer.""" @@ -8,7 +8,7 @@ import pytest from macaron.database.table_definitions import Analysis, Component, RepoFinderMetadata -from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIPackageJsonAsset, PyPIRegistry +from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIInspectorAsset, PyPIPackageJsonAsset, PyPIRegistry @pytest.fixture(autouse=True) @@ -26,4 +26,5 @@ def pypi_package_json() -> MagicMock: pypi_package.component = Component( purl="pkg:pypi/package", analysis=Analysis(), repository=None, repo_finder_metadata=RepoFinderMetadata() ) + pypi_package.inspector_asset = MagicMock(spec=PyPIInspectorAsset) return pypi_package diff --git a/tests/malware_analyzer/pypi/test_wheel_absence.py b/tests/malware_analyzer/pypi/test_wheel_absence.py index 2c233428f..8949799a6 100644 --- a/tests/malware_analyzer/pypi/test_wheel_absence.py +++ b/tests/malware_analyzer/pypi/test_wheel_absence.py @@ -9,162 +9,66 @@ from macaron.errors import HeuristicAnalyzerValueError from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult from macaron.malware_analyzer.pypi_heuristics.metadata.wheel_absence import WheelAbsenceAnalyzer +from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIInspectorAsset, PyPIPackageJsonAsset -def test_analyze_no_information(pypi_package_json: MagicMock) -> None: - """Test for when there is no release information, so error""" +def test_no_information(pypi_package_json: MagicMock) -> None: + """Test for when inspector links cannot be created, so error""" analyzer = WheelAbsenceAnalyzer() - pypi_package_json.get_releases.return_value = None + pypi_package_json.get_inspector_links.return_value = False with pytest.raises(HeuristicAnalyzerValueError): - analyzer.analyze(pypi_package_json) + _ = analyzer.analyze(pypi_package_json) -# Note: to patch a function, the way it is imported matters. -# E.g. if it is imported like this: import os; os.listdir() then you patch os.listdir. -# If it is imported like this: from os import listdir; listdir() then you patch .listdir. -@patch("macaron.malware_analyzer.pypi_heuristics.metadata.wheel_absence.send_head_http_raw") -def test_analyze_tar_present(mock_send_head_http_raw: MagicMock, pypi_package_json: MagicMock) -> None: - """Test for when only .tar.gz is present, so failed""" +def test_no_wheel_links(pypi_package_json: MagicMock) -> None: + """Test for when no .whl files are present in the asset, so failed""" analyzer = WheelAbsenceAnalyzer() - version = "0.1.0" - filename = "ttttttttest_nester.py-0.1.0.tar.gz" - url = ( - "https://files.pythonhosted.org/packages/de/fa/" - + f"2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3/{filename}" - ) - inspector_link_expected = ( - "https://inspector.pypi.io/project/ttttttttest_nester/0.1.0/packages/" - + f"de/fa/2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3/{filename}" - ) - release = { - version: [ - { - "comment_text": "", - "digests": { - "blake2b_256": "defa2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3", - "md5": "9203bbb130f8ddb38269f4861c170d04", - "sha256": "168bcccbf5106132e90b85659297700194369b8f6b3e5a03769614f0d200e370", - }, - "downloads": -1, - "filename": filename, - "has_sig": False, - "md5_digest": "9203bbb130f8ddb38269f4861c170d04", - "packagetype": "sdist", - "python_version": "source", - "requires_python": None, - "size": 546, - "upload_time": "2016-10-13T05:42:27", - "upload_time_iso_8601": "2016-10-13T05:42:27.073842Z", - "url": url, - "yanked": False, - "yanked_reason": None, - } - ] - } - - pypi_package_json.get_releases.return_value = release - pypi_package_json.get_latest_version.return_value = version - pypi_package_json.component_version = None - pypi_package_json.package_json = {"info": {"name": "ttttttttest_nester"}} - pypi_package_json.pypi_registry.inspector_url_scheme = "https" - pypi_package_json.pypi_registry.inspector_url_netloc = "inspector.pypi.io" + pypi_package_json.get_inspector_links.return_value = True + pypi_package_json.inspector_asset.package_whl_links = [] + pypi_package_json.inspector_asset.package_link_reachability = {} - mock_send_head_http_raw.return_value = MagicMock() # Assume valid URL for testing purposes. - - expected_detail_info = { - "inspector_links": {inspector_link_expected: True}, - } - - expected_result: tuple[HeuristicResult, dict] = (HeuristicResult.FAIL, expected_detail_info) - - actual_result = analyzer.analyze(pypi_package_json) - - assert actual_result == expected_result + result, _ = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.FAIL -@patch("macaron.malware_analyzer.pypi_heuristics.metadata.wheel_absence.send_head_http_raw") -def test_analyze_whl_present(mock_send_head_http_raw: MagicMock, pypi_package_json: MagicMock) -> None: - """Test for when only .whl is present, so pass""" +def test_wheel_links(pypi_package_json: MagicMock) -> None: + """Test for when at least one .whl file is present in the asset, so pass""" analyzer = WheelAbsenceAnalyzer() - version = "0.1.0" - filename = "ttttttttest_nester.py-0.1.0.whl" - url = ( - "https://files.pythonhosted.org/packages/de/fa/" - + f"2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3/{filename}" - ) - inspector_link_expected = ( - "https://inspector.pypi.io/project/ttttttttest_nester/0.1.0/packages/" - + f"de/fa/2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3/{filename}" - ) - release = { - version: [ - { - "comment_text": "", - "digests": { - "blake2b_256": "defa2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3", - "md5": "9203bbb130f8ddb38269f4861c170d04", - "sha256": "168bcccbf5106132e90b85659297700194369b8f6b3e5a03769614f0d200e370", - }, - "downloads": -1, - "filename": filename, - "has_sig": False, - "md5_digest": "9203bbb130f8ddb38269f4861c170d04", - "packagetype": "bdist_wheel", - "python_version": "py2.py3", - "requires_python": None, - "size": 546, - "upload_time": "2016-10-13T05:42:27", - "upload_time_iso_8601": "2016-10-13T05:42:27.073842Z", - "url": url, - "yanked": False, - "yanked_reason": None, - } - ] - } - - pypi_package_json.get_releases.return_value = release - pypi_package_json.component_version = version - pypi_package_json.package_json = {"info": {"name": "ttttttttest_nester"}} - pypi_package_json.pypi_registry.inspector_url_scheme = "https" - pypi_package_json.pypi_registry.inspector_url_netloc = "inspector.pypi.io" - mock_send_head_http_raw.return_value = MagicMock() # Assume valid URL for testing purposes. - - expected_detail_info = { - "inspector_links": {inspector_link_expected: True}, - } + link = "https://files.pythonhosted.org/packages/de/fa/2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3/package.whl" + pypi_package_json.get_inspector_links.return_value = True + pypi_package_json.inspector_asset.package_whl_links = [link] + pypi_package_json.inspector_asset.package_link_reachability = {link: True} - expected_result: tuple[HeuristicResult, dict] = (HeuristicResult.PASS, expected_detail_info) + result, _ = analyzer.analyze(pypi_package_json) + assert result == HeuristicResult.PASS - actual_result = analyzer.analyze(pypi_package_json) - assert actual_result == expected_result - - -@patch("macaron.malware_analyzer.pypi_heuristics.metadata.wheel_absence.send_head_http_raw") -def test_analyze_both_present(mock_send_head_http_raw: MagicMock, pypi_package_json: MagicMock) -> None: - """Test for when both .tar.gz and .whl are present, so passed""" - analyzer = WheelAbsenceAnalyzer() +# Note: to patch a function, the way it is imported matters. +# E.g. if it is imported like this: import os; os.listdir() then you patch os.listdir. +# If it is imported like this: from os import listdir; listdir() then you patch .listdir. +@patch("macaron.slsa_analyzer.package_registry.pypi_registry.send_head_http_raw") +def test_get_inspector_links(mock_send_head_http_raw: MagicMock) -> None: + """Test to make sure the internal function used by this analyzer produces the correct output from JSON metadata""" version = "0.1.0" - file_prefix = "ttttttttest_nester.py-0.1.0" - wheel_url = ( - "https://files.pythonhosted.org/packages/de/fa/" - + f"2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3/{file_prefix}.whl" - ) - tar_url = ( - "https://files.pythonhosted.org/packages/de/fa/" - + f"2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3/{file_prefix}.tar.gz" - ) + package_name = "ttttttttest_nester" + file_prefix = package_name + "-" + version + b2b_first = "de" + b2b_second = "fa" + b2b_rest = "2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3" + blake2b_hash = b2b_first + b2b_second + b2b_rest + hash_link_prefix = f"{b2b_first}/{b2b_second}/{b2b_rest}/{file_prefix}" + + wheel_url = f"https://files.pythonhosted.org/packages/{hash_link_prefix}.whl" + tar_url = f"https://files.pythonhosted.org/packages/{hash_link_prefix}.tar.gz" wheel_link_expected = ( - "https://inspector.pypi.io/project/ttttttttest_nester/0.1.0/packages/" - + f"de/fa/2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3/{file_prefix}.whl" + f"https://inspector.pypi.io/project/{package_name}/{version}/packages/" + f"{hash_link_prefix}.whl" ) tar_link_expected = ( - "https://inspector.pypi.io/project/ttttttttest_nester/0.1.0/packages/" - + f"de/fa/2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3/{file_prefix}.tar.gz" + f"https://inspector.pypi.io/project/{package_name}/{version}/packages/" + f"{hash_link_prefix}.tar.gz" ) release = { @@ -172,7 +76,7 @@ def test_analyze_both_present(mock_send_head_http_raw: MagicMock, pypi_package_j { "comment_text": "", "digests": { - "blake2b_256": "defa2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3", + "blake2b_256": blake2b_hash, "md5": "9203bbb130f8ddb38269f4861c170d04", "sha256": "168bcccbf5106132e90b85659297700194369b8f6b3e5a03769614f0d200e370", }, @@ -193,7 +97,7 @@ def test_analyze_both_present(mock_send_head_http_raw: MagicMock, pypi_package_j { "comment_text": "", "digests": { - "blake2b_256": "defa2fbcebaeeb909511139ce28dac4a77ab2452ba72b49a22b12981b2f375b3", + "blake2b_256": blake2b_hash, "md5": "9203bbb130f8ddb38269f4861c170d04", "sha256": "168bcccbf5106132e90b85659297700194369b8f6b3e5a03769614f0d200e370", }, @@ -214,19 +118,21 @@ def test_analyze_both_present(mock_send_head_http_raw: MagicMock, pypi_package_j ] } - pypi_package_json.get_releases.return_value = release - pypi_package_json.component_version = version - pypi_package_json.package_json = {"info": {"name": "ttttttttest_nester"}} - pypi_package_json.pypi_registry.inspector_url_scheme = "https" - pypi_package_json.pypi_registry.inspector_url_netloc = "inspector.pypi.io" + package_json = {"info": {"name": package_name}, "releases": release} + pypi_registry = MagicMock() + pypi_registry.inspector_url_scheme = "https" + pypi_registry.inspector_url_netloc = "inspector.pypi.io" mock_send_head_http_raw.return_value = MagicMock() # assume valid URL for testing purposes - expected_detail_info = { - "inspector_links": {wheel_link_expected: True, tar_link_expected: True}, - } - - expected_result: tuple[HeuristicResult, dict] = (HeuristicResult.PASS, expected_detail_info) - - actual_result = analyzer.analyze(pypi_package_json) + pypi_package_json = PyPIPackageJsonAsset( + package_name, version, False, pypi_registry, package_json, "", PyPIInspectorAsset("", [], {}) + ) - assert actual_result == expected_result + assert pypi_package_json.get_inspector_links() is True + assert pypi_package_json.inspector_asset.package_sdist_link == tar_link_expected + assert len(pypi_package_json.inspector_asset.package_whl_links) == 1 + assert pypi_package_json.inspector_asset.package_whl_links[0] == wheel_link_expected + assert pypi_package_json.inspector_asset.package_link_reachability == { + tar_link_expected: True, + wheel_link_expected: True, + }