From f3e045b37705032eebb6a20317e51b85b213b761 Mon Sep 17 00:00:00 2001 From: Andreea Fulea Date: Wed, 27 May 2026 16:44:08 +0300 Subject: [PATCH 1/2] fix: remove fallback logic for Source Code Download URL --- capycli/bom/create_components.py | 70 ++- tests/test_bom_create_components.py | 1 - tests/test_bom_create_releases.py | 750 +++++++++++++++------------- 3 files changed, 453 insertions(+), 368 deletions(-) diff --git a/capycli/bom/create_components.py b/capycli/bom/create_components.py index e6afb74d..b18e353a 100644 --- a/capycli/bom/create_components.py +++ b/capycli/bom/create_components.py @@ -236,10 +236,13 @@ def add_licenses(self, cx_comp: Component, data: Dict[str, Any]) -> None: data["mainLicenseIds"] = licenses def prepare_release_data(self, cx_comp: Component) -> Dict[str, Any]: - """Create release data structure as expected by SW360 REST API + """Create the Release data structure as expected by SW360 REST API. - :param item: a single bill of materials item - a release - :type item: dictionary + If the Source Code Download URL is missing in the given Release, try to update the VCS/Repository URL + of the corresponding SW360 Component with the repository or website URL from the Release. + + :param cx_comp: a single bill of materials item - a Release + :type cx_comp: Component :return: the release :rtype: release (dictionary) """ @@ -251,6 +254,56 @@ def prepare_release_data(self, cx_comp: Component) -> Dict[str, Any]: src_url = str(CycloneDxSupport.get_ext_ref_source_url(cx_comp)) if src_url: data["sourceCodeDownloadurl"] = src_url + else: + print_red(" No Source Code Download URL found. Will try to update the VCS/Repository URL.") + + # Try to update the VCS/Repository URL of the corresponding SW360 Component + # with the repository or website URL from the Release + component_id = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_COMPONENT_ID) + if not component_id: + print_red( + f" Unable to get the SW360 Component ID from the BOM item: {cx_comp.name}. " + "Will not update the VCS/Repository URL." + ) + elif not self.client: + print_red(" No SW360 client available. Will not update the VCS/Repository URL.") + else: + component = self.client.get_component(component_id) + if component is None: + print_red( + f" Unable to get the SW360 Component with ID: {component_id}. " + "Will not update the VCS/Repository URL." + ) + else: + website = CycloneDxSupport.get_ext_ref_website(cx_comp) + repo = CycloneDxSupport.get_ext_ref_repository(cx_comp) + + current_vcs = component.get("vcs", "") + # Note: The VCS/Repository URL can only be updated after the rollout of Issue 1886 + # (https://code.siemens.com/sw360/sw360portal/-/issues/1886) - before that, + # the VCS/Repository URL is not available in the SW360 REST API and thus cannot be updated. + if repo: + print_yellow( + f" Not available until rollout of Issue 1886. " + f"Could update VCS/Repository URL from: {current_vcs if current_vcs else ''} " + f"with repository: {repo}..." + ) + # try: + # updated_component = self.client.update_component({"vcs": str(repo)}, component_id) + # print_green(f" Successfully updated VCS/Repository URL to: {updated_component}") + # except SW360Error as e: + # print_red(f" Failed to update VCS/Repository URL: {e}") + elif website: + print_yellow( + f" Not available until rollout of Issue 1886. " + f"Could update VCS/Repository URL from: {current_vcs if current_vcs else ''} " + f"with website: {website}..." + ) + # try: + # updated_component = self.client.update_component({"vcs": str(website)}, component_id) + # print_green(f" Successfully updated VCS/Repository URL to: {updated_component}") + # except SW360Error as e: + # print_red(f" Failed to update VCS/Repository URL: {e}") bin_url = str(CycloneDxSupport.get_ext_ref_binary_url(cx_comp)) if bin_url: @@ -266,17 +319,6 @@ def prepare_release_data(self, cx_comp: Component) -> Dict[str, Any]: data["additionalData"] = {} data["additionalData"]["createdWith"] = capycli.get_app_signature() - # use project site as fallback for source code download url - website = CycloneDxSupport.get_ext_ref_website(cx_comp) - repo = CycloneDxSupport.get_ext_ref_repository(cx_comp) - if not src_url: - if repo: - print(" Using repository for source code download URL...") - data["sourceCodeDownloadurl"] = str(repo) - elif website: - print(" Using website for source code download URL...") - data["sourceCodeDownloadurl"] = str(website) - language = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_LANGUAGE) if language: data["languages"] = [] diff --git a/tests/test_bom_create_components.py b/tests/test_bom_create_components.py index ceb1ae4b..f38b1204 100644 --- a/tests/test_bom_create_components.py +++ b/tests/test_bom_create_components.py @@ -61,7 +61,6 @@ def test_create_component(self) -> None: release_data = {"name": "activemodel", "version": "5.2.4.3", "mainlineState": "OPEN", "languages": ["Ruby"], - "sourceCodeDownloadurl": "http://test.org", "externalIds": {"package-url": "pkg:gem/activemodel@5.2.4.3"}, "additionalData": {"createdWith": capycli.get_app_signature()}} responses.add( diff --git a/tests/test_bom_create_releases.py b/tests/test_bom_create_releases.py index c0571864..54179425 100644 --- a/tests/test_bom_create_releases.py +++ b/tests/test_bom_create_releases.py @@ -7,6 +7,7 @@ # ------------------------------------------------------------------------------- """unit tests for bom/create_components.py in createreleases mode""" +import sys from typing import Any, Dict, Tuple import pytest @@ -15,6 +16,9 @@ from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component from packageurl import PackageURL +import logging +import io + import capycli.bom.create_components from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport @@ -58,6 +62,16 @@ def setUp(self) -> None: responses.add(responses.GET, SW360_BASE_URL, json={"status": "ok"}) self.app.login("sometoken", "https://my.server.com") + def capture_logs(self): + """Redirect logging output to capsys""" + logger = logging.getLogger("capycli.common.print") + logger.setLevel(logging.DEBUG) # Ensure all log levels are captured + handler = logging.StreamHandler(sys.stdout) + handler.setLevel(logging.DEBUG) + logger.addHandler(handler) + return logger, handler + + @responses.activate def test_create_items_existing_release_with_id(self) -> None: """Release exists and was identified in "bom map" @@ -376,6 +390,7 @@ def test_create_comp_release_component_id(self) -> None: self.app.create_component_and_release(item) assert CycloneDxSupport.get_property_value(item, CycloneDxSupport.CDX_PROP_SW360ID) == "06a6e7" + @pytest.mark.skip(reason="Fails after the Source Code Download URL is no longer updated with the VCS value.") @responses.activate def test_create_comp_release_component_id_update(self) -> None: """We have a componentId match. Update package-url if needed. @@ -442,25 +457,30 @@ def test_create_comp_release_component_id_update(self) -> None: def test_update_component_other_purl(self) -> None: """Existing component has different purl, so issue warning. """ - component_data: Dict[str, Any] = { - "name": "activemodel", - "externalIds": {"package-url": "pkg:deb/ubuntu/activemodel?arch=source"}, - "_links": {"self": { - "href": SW360_BASE_URL + "components/06a6e5"}}, - "_embedded": {"sw360:releases": [{ + logger, handler = self.capture_logs() + try: + component_data: Dict[str, Any] = { "name": "activemodel", - "version": "5.2.1", + "externalIds": {"package-url": "pkg:deb/ubuntu/activemodel?arch=source"}, "_links": {"self": { - "href": SW360_BASE_URL + "releases/06a6e6"}}}]}} - item = Component( - name="activemodel", - version="5.2.1", - purl=PackageURL.from_string("pkg:deb/debian/activemodel@5.2.1?arch=source") - ) - CycloneDxSupport.update_or_set_property(item, CycloneDxSupport.CDX_PROP_COMPONENT_ID, "06a6e5") - self.app.update_component(item, "123", component_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM id" in captured.out + "href": SW360_BASE_URL + "components/06a6e5"}}, + "_embedded": {"sw360:releases": [{ + "name": "activemodel", + "version": "5.2.1", + "_links": {"self": { + "href": SW360_BASE_URL + "releases/06a6e6"}}}]}} + item = Component( + name="activemodel", + version="5.2.1", + purl=PackageURL.from_string("pkg:deb/debian/activemodel@5.2.1?arch=source") + ) + CycloneDxSupport.update_or_set_property(item, CycloneDxSupport.CDX_PROP_COMPONENT_ID, "06a6e5") + self.app.update_component(item, "123", component_data) + + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM id" in captured.out + finally: + logger.removeHandler(handler) component_data["externalIds"]["package-url"] = ('[' '"pkg:deb/ubuntu/activemodel?arch=source",' @@ -691,27 +711,31 @@ def test_upload_file_local(self) -> None: def test_upload_file_prevent_git_source_upload(self) -> None: """Prevent uploading SOURCE, SOURCE_SELF file with .git file type """ - responses.add( - responses.GET, 'https://github.com/babel/babel.git', - body="content") - - self.app.download = True - item = Component( - name="activemodel", - version="5.2.1" - ) - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_URL_COMMENT, "https://github.com/babel/babel.git") - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_FILE_COMMENT, "babel.git") - - self.app.upload_file(item, {}, "06a6e7", "SOURCE", "") - captured = self.capsys.readouterr() # type: ignore - assert len(responses.calls) == 0 - assert "WARNING: resetting filename to prevent uploading .git file" in captured.out - assert captured.err == "" + logger, handler = self.capture_logs() + try: + responses.add( + responses.GET, 'https://github.com/babel/babel.git', + body="content") + + self.app.download = True + item = Component( + name="activemodel", + version="5.2.1" + ) + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_URL_COMMENT, "https://github.com/babel/babel.git") + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_FILE_COMMENT, "babel.git") + + self.app.upload_file(item, {}, "06a6e7", "SOURCE", "") + captured = self.capsys.readouterr() # type: ignore + assert len(responses.calls) == 0 + assert "WARNING: resetting filename to prevent uploading .git file" in captured.out + assert captured.err == "" + finally: + logger.removeHandler(handler) @responses.activate def test_upload_file_allow_git_binary_upload(self) -> None: @@ -824,303 +848,319 @@ def test_upload_file_source_dir(self) -> None: def test_update_release_SourceUrl(self) -> None: """Update SourceUrl in existing release """ - # no existing URL, no new URL - release_data: Dict[str, Any] = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - - item2 = Component(name="") - self.app.update_release(item2, release_data) - - # existing URL, no new URL - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, - "sourceCodeDownloadurl": "old_url" - } - item2 = Component(name="") - self.app.update_release(item2, release_data) - - # existing URL equals to new URL - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_URL_COMMENT, "old_url") - self.app.update_release(item2, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM URL" not in captured.out - - # existing URL differs from new URL - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_URL_COMMENT, "https://some.new/file.tar.gz") - self.app.update_release(item2, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM URL" in captured.out - assert len(responses.calls) == 0 # assure data in SW360 is not changed - - # no existing URL, set new URL - responses.add( - responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', - match=[responses.matchers.json_params_matcher({ - "sourceCodeDownloadurl": "new_url"})], - # server answer with created release data - json={ - "sourceCodeDownloadurl": "new_url", - "_links": {"self": { - "href": SW360_BASE_URL + "releases/06a6e7"}}}) - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_URL_COMMENT, "new_url") - self.app.update_release(item2, release_data) - assert len(responses.calls) == 1 - - # existing URL empty, set new URL - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, - "sourceCodeDownloadurl": "" - } - self.app.update_release(item2, release_data) - assert len(responses.calls) == 2 + logger, handler = self.capture_logs() + try: + # no existing URL, no new URL + release_data: Dict[str, Any] = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + + item2 = Component(name="") + self.app.update_release(item2, release_data) + + # existing URL, no new URL + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, + "sourceCodeDownloadurl": "old_url" + } + item2 = Component(name="") + self.app.update_release(item2, release_data) + + # existing URL equals to new URL + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_URL_COMMENT, "old_url") + self.app.update_release(item2, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM URL" not in captured.out + + # existing URL differs from new URL + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_URL_COMMENT, "https://some.new/file.tar.gz") + self.app.update_release(item2, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM URL" in captured.out + assert len(responses.calls) == 0 # assure data in SW360 is not changed + + # no existing URL, set new URL + responses.add( + responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', + match=[responses.matchers.json_params_matcher({ + "sourceCodeDownloadurl": "new_url"})], + # server answer with created release data + json={ + "sourceCodeDownloadurl": "new_url", + "_links": {"self": { + "href": SW360_BASE_URL + "releases/06a6e7"}}}) + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_URL_COMMENT, "new_url") + self.app.update_release(item2, release_data) + assert len(responses.calls) == 1 + + # existing URL empty, set new URL + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, + "sourceCodeDownloadurl": "" + } + self.app.update_release(item2, release_data) + assert len(responses.calls) == 2 + finally: + logger.removeHandler(handler) @responses.activate def test_update_release_BinaryUrl(self) -> None: """Update SourceUrl in existing release """ - # no existing URL, no new URL - release_data: Dict[str, Any] = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - - item2 = Component(name="") - self.app.update_release(item2, release_data) - - # existing URL, no new URL - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, - "binaryDownloadurl": "old_url" - } - item2 = Component(name="") - self.app.update_release(item2, release_data) - - # existing URL equals to new URL - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.BINARY_URL_COMMENT, "old_url") - self.app.update_release(item2, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM URL" not in captured.out - - # existing URL differs from new URL - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.BINARY_URL_COMMENT, "new_url") - self.app.update_release(item2, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM URL" in captured.out - - # no existing URL, set new URL - responses.add( - responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', - match=[responses.matchers.json_params_matcher({ - "binaryDownloadurl": "new_url"})], - # server answer with created release data - json={ - "binaryDownloadurl": "new_url", - "_links": {"self": { - "href": SW360_BASE_URL + "releases/06a6e7"}}}) - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.BINARY_URL_COMMENT, "new_url") - self.app.update_release(item2, release_data) - assert len(responses.calls) == 1 - - # existing URL empty, set new URL - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, - "binaryDownloadurl": "" - } - self.app.update_release(item2, release_data) - assert len(responses.calls) == 2 + logger, handler = self.capture_logs() + try: + # no existing URL, no new URL + release_data: Dict[str, Any] = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + + item2 = Component(name="") + self.app.update_release(item2, release_data) + + # existing URL, no new URL + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, + "binaryDownloadurl": "old_url" + } + item2 = Component(name="") + self.app.update_release(item2, release_data) + + # existing URL equals to new URL + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.BINARY_URL_COMMENT, "old_url") + self.app.update_release(item2, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM URL" not in captured.out + + # existing URL differs from new URL + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.BINARY_URL_COMMENT, "new_url") + self.app.update_release(item2, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM URL" in captured.out + + # no existing URL, set new URL + responses.add( + responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', + match=[responses.matchers.json_params_matcher({ + "binaryDownloadurl": "new_url"})], + # server answer with created release data + json={ + "binaryDownloadurl": "new_url", + "_links": {"self": { + "href": SW360_BASE_URL + "releases/06a6e7"}}}) + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.BINARY_URL_COMMENT, "new_url") + self.app.update_release(item2, release_data) + assert len(responses.calls) == 1 + + # existing URL empty, set new URL + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, + "binaryDownloadurl": "" + } + self.app.update_release(item2, release_data) + assert len(responses.calls) == 2 + finally: + logger.removeHandler(handler) @responses.activate def test_update_release_externalId(self) -> None: """Update externalId in existing release """ - # existing externalId, no new Id -> do nothing, "%7E" = "~" - release_data: Dict[str, Any] = { - "externalIds": {"package-url": "pkg:deb/debian/bash@1.0%7E1"}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - item = Component(name="") - self.app.update_release(item, release_data) - - # existing Id same as new Id - item = Component( - name="", - purl=PackageURL.from_string("pkg:deb/debian/bash@1.0%7E1") - ) - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM id" not in captured.out - - item.purl = PackageURL.from_string("pkg:deb/debian/bash@1.0~1") - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM id" not in captured.out - - # existing Id differs from new Id -> only warn - item.purl = PackageURL.from_string("pkg:deb/debian/bash@2.0") - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM id" in captured.out - assert item.purl.to_string() == "pkg:deb/debian/bash@2.0" - - # existing Id invalid - release_data["externalIds"]["package-url"] = "pkg:something" # invalid purl - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM id" in captured.out - assert item.purl.to_string() == "pkg:deb/debian/bash@2.0" - - # add new Id, no existing ID - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - responses.add( - responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', - match=[responses.matchers.json_params_matcher({ - "externalIds": { - "package-url": "pkg:deb/debian/bash@2.0"}})], - # server answer with created release data - json={ - "_links": {"self": { - "href": SW360_BASE_URL + "releases/06a6e7"}}}) - self.app.update_release(item, release_data) - assert len(responses.calls) == 1 - - release_data = { - "externalIds": {}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - self.app.update_release(item, release_data) - assert len(responses.calls) == 2 - - # add new Id to existing IDs -> assure we keep existing ones - release_data = { - "externalIds": {"some_id": "42"}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - responses.replace( - responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', - match=[responses.matchers.json_params_matcher({ - "externalIds": { - "some_id": "42", - "package-url": "pkg:deb/debian/bash@2.0"}})], - # server answer with created release data - json={ - "_links": {"self": { - "href": SW360_BASE_URL + "releases/06a6e7"}}}) - self.app.update_release(item, release_data) - assert len(responses.calls) == 3 + logger, handler = self.capture_logs() + try: + # existing externalId, no new Id -> do nothing, "%7E" = "~" + release_data: Dict[str, Any] = { + "externalIds": {"package-url": "pkg:deb/debian/bash@1.0%7E1"}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + item = Component(name="") + self.app.update_release(item, release_data) + + # existing Id same as new Id + item = Component( + name="", + purl=PackageURL.from_string("pkg:deb/debian/bash@1.0%7E1") + ) + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM id" not in captured.out + + item.purl = PackageURL.from_string("pkg:deb/debian/bash@1.0~1") + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM id" not in captured.out + + # existing Id differs from new Id -> only warn + item.purl = PackageURL.from_string("pkg:deb/debian/bash@2.0") + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM id" in captured.out + assert item.purl.to_string() == "pkg:deb/debian/bash@2.0" + + # existing Id invalid + release_data["externalIds"]["package-url"] = "pkg:something" # invalid purl + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM id" in captured.out + assert item.purl.to_string() == "pkg:deb/debian/bash@2.0" + + # add new Id, no existing ID + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + responses.add( + responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', + match=[responses.matchers.json_params_matcher({ + "externalIds": { + "package-url": "pkg:deb/debian/bash@2.0"}})], + # server answer with created release data + json={ + "_links": {"self": { + "href": SW360_BASE_URL + "releases/06a6e7"}}}) + self.app.update_release(item, release_data) + assert len(responses.calls) == 1 + + release_data = { + "externalIds": {}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + self.app.update_release(item, release_data) + assert len(responses.calls) == 2 + + # add new Id to existing IDs -> assure we keep existing ones + release_data = { + "externalIds": {"some_id": "42"}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + responses.replace( + responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', + match=[responses.matchers.json_params_matcher({ + "externalIds": { + "some_id": "42", + "package-url": "pkg:deb/debian/bash@2.0"}})], + # server answer with created release data + json={ + "_links": {"self": { + "href": SW360_BASE_URL + "releases/06a6e7"}}}) + self.app.update_release(item, release_data) + assert len(responses.calls) == 3 + finally: + logger.removeHandler(handler) @responses.activate def test_update_release_attachment(self) -> None: """Upload to existing release """ - responses.add(responses.GET, SW360_BASE_URL + "attachments/0123", - json={}) - - # existing source, no new source -> do nothing - release_data = { - '_embedded': {'sw360:attachments': [{ - 'filename': 'adduser-3.118.zip', - 'attachmentType': 'SOURCE'}]}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - item = Component(name="") - self.app.update_release(item, release_data) - - # existing source same as new source - release_data = { - '_embedded': {'sw360:attachments': [{ - 'filename': 'some.txt', - 'attachmentType': 'DOCUMENT'}, { - '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, - 'filename': 'adduser-3.118.zip', - 'attachmentType': 'SOURCE_SELF'}]}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - item = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_FILE_COMMENT, "adduser-3.118.zip") - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "different source attachment" not in captured.out - assert len(responses.calls) == 1 - - # existing source with different hash -> do nothing - release_data = { - '_embedded': {'sw360:attachments': [{ - '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, - 'filename': 'adduser-3.118.zip', - 'sha1': '123', - 'attachmentType': 'SOURCE'}]}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - item = Component(name="") - extref = ExternalReference( - type=ExternalReferenceType.DISTRIBUTION, - comment=CaPyCliBom.SOURCE_FILE_COMMENT, - url=XsUri("adduser-3.118.zip")) - extref.hashes.add(HashType( - alg=HashAlgorithm.SHA_1, - content="456")) - item.external_references.add(extref) - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "different hash for source attachment" in captured.out - assert len(responses.calls) == 2 - - # existing source, different source -> do nothing - item = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_FILE_COMMENT, "Readme.md") - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "different source attachment" in captured.out - assert len(responses.calls) == 3 - - # no attachment existing -> upload - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - item = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_FILE_COMMENT, "Readme.md") - responses.add( - responses.POST, SW360_BASE_URL + 'releases/06a6e7/attachments', - match=[upload_matcher("Readme.md")]) - self.app.update_release(item, release_data) - assert len(responses.calls) == 4 - - # only other attachments existing -> upload - release_data = { - '_embedded': {'sw360:attachments': [{ - 'filename': 'some.txt', - 'attachmentType': 'DOCUMENT'}]}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - self.app.update_release(item, release_data) - assert len(responses.calls) == 5 + logger, handler = self.capture_logs() + try: + responses.add(responses.GET, SW360_BASE_URL + "attachments/0123", + json={}) + + # existing source, no new source -> do nothing + release_data = { + '_embedded': {'sw360:attachments': [{ + 'filename': 'adduser-3.118.zip', + 'attachmentType': 'SOURCE'}]}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + item = Component(name="") + self.app.update_release(item, release_data) + + # existing source same as new source + release_data = { + '_embedded': {'sw360:attachments': [{ + 'filename': 'some.txt', + 'attachmentType': 'DOCUMENT'}, { + '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, + 'filename': 'adduser-3.118.zip', + 'attachmentType': 'SOURCE_SELF'}]}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + item = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_FILE_COMMENT, "adduser-3.118.zip") + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "different source attachment" not in captured.out + assert len(responses.calls) == 1 + + # existing source with different hash -> do nothing + release_data = { + '_embedded': {'sw360:attachments': [{ + '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, + 'filename': 'adduser-3.118.zip', + 'sha1': '123', + 'attachmentType': 'SOURCE'}]}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + item = Component(name="") + extref = ExternalReference( + type=ExternalReferenceType.DISTRIBUTION, + comment=CaPyCliBom.SOURCE_FILE_COMMENT, + url=XsUri("adduser-3.118.zip")) + extref.hashes.add(HashType( + alg=HashAlgorithm.SHA_1, + content="456")) + item.external_references.add(extref) + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "different hash for source attachment" in captured.out + assert len(responses.calls) == 2 + + # existing source, different source -> do nothing + item = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_FILE_COMMENT, "Readme.md") + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "different source attachment" in captured.out + assert len(responses.calls) == 3 + + # no attachment existing -> upload + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + item = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_FILE_COMMENT, "Readme.md") + responses.add( + responses.POST, SW360_BASE_URL + 'releases/06a6e7/attachments', + match=[upload_matcher("Readme.md")]) + self.app.update_release(item, release_data) + assert len(responses.calls) == 4 + + # only other attachments existing -> upload + release_data = { + '_embedded': {'sw360:attachments': [{ + 'filename': 'some.txt', + 'attachmentType': 'DOCUMENT'}]}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + self.app.update_release(item, release_data) + assert len(responses.calls) == 5 + finally: + logger.removeHandler(handler) @responses.activate def test_update_release_attachment_rejected(self) -> None: @@ -1152,36 +1192,40 @@ def test_update_release_attachment_rejected(self) -> None: def test_update_release_attachment_rename(self) -> None: """Upload to existing release with content-disposition rename """ - # attachment with target file name after rename exists -> don't upload - # (test for commit 2e403af032) - responses.add(responses.GET, SW360_BASE_URL + "attachments/0123", - json={}) - responses.add( - responses.GET, 'https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip', - headers={"content-disposition": 'attachment; filename=babel-7.16.0.zip'}, - body="content") - release_data = { - "sourceCodeDownloadurl": "https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip", - '_embedded': {'sw360:attachments': [{ - '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, - 'filename': 'babel-7.16.0.zip', - 'attachmentType': 'SOURCE'}]}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - self.app.download = True - item = Component( - name="babel", - version="7.16.0" - ) - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_URL_COMMENT, "https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip") - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "different source attachment" in captured.out - - # currently, upload_file() will do nothing if *any* source attachment exists, - # so there should be 0 calls. If this semantics changes and content-disposition - # handling avoids duplicated uploads, we will see 1 call, so accept 0 or 1 here. - assert len(responses.calls) <= 1 - assert "Error" not in captured.out - assert captured.err == "" + logger, handler = self.capture_logs() + try: + # attachment with target file name after rename exists -> don't upload + # (test for commit 2e403af032) + responses.add(responses.GET, SW360_BASE_URL + "attachments/0123", + json={}) + responses.add( + responses.GET, 'https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip', + headers={"content-disposition": 'attachment; filename=babel-7.16.0.zip'}, + body="content") + release_data = { + "sourceCodeDownloadurl": "https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip", + '_embedded': {'sw360:attachments': [{ + '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, + 'filename': 'babel-7.16.0.zip', + 'attachmentType': 'SOURCE'}]}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + self.app.download = True + item = Component( + name="babel", + version="7.16.0" + ) + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_URL_COMMENT, "https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip") + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "different source attachment" in captured.out + + # currently, upload_file() will do nothing if *any* source attachment exists, + # so there should be 0 calls. If this semantics changes and content-disposition + # handling avoids duplicated uploads, we will see 1 call, so accept 0 or 1 here. + assert len(responses.calls) <= 1 + assert "Error" not in captured.out + assert captured.err == "" + finally: + logger.removeHandler(handler) From 80f735f78569b3de334bf03c415dddb1ed1e9220 Mon Sep 17 00:00:00 2001 From: Andreea Fulea Date: Thu, 28 May 2026 14:59:11 +0300 Subject: [PATCH 2/2] fix: remove update with repo and website at component level and restore tests as previous for releases --- capycli/bom/create_components.py | 50 +- tests/test_bom_create_components.py | 2 + tests/test_bom_create_releases.py | 750 +++++++++++++--------------- 3 files changed, 356 insertions(+), 446 deletions(-) diff --git a/capycli/bom/create_components.py b/capycli/bom/create_components.py index b18e353a..7a831d6f 100644 --- a/capycli/bom/create_components.py +++ b/capycli/bom/create_components.py @@ -255,55 +255,7 @@ def prepare_release_data(self, cx_comp: Component) -> Dict[str, Any]: if src_url: data["sourceCodeDownloadurl"] = src_url else: - print_red(" No Source Code Download URL found. Will try to update the VCS/Repository URL.") - - # Try to update the VCS/Repository URL of the corresponding SW360 Component - # with the repository or website URL from the Release - component_id = CycloneDxSupport.get_property_value(cx_comp, CycloneDxSupport.CDX_PROP_COMPONENT_ID) - if not component_id: - print_red( - f" Unable to get the SW360 Component ID from the BOM item: {cx_comp.name}. " - "Will not update the VCS/Repository URL." - ) - elif not self.client: - print_red(" No SW360 client available. Will not update the VCS/Repository URL.") - else: - component = self.client.get_component(component_id) - if component is None: - print_red( - f" Unable to get the SW360 Component with ID: {component_id}. " - "Will not update the VCS/Repository URL." - ) - else: - website = CycloneDxSupport.get_ext_ref_website(cx_comp) - repo = CycloneDxSupport.get_ext_ref_repository(cx_comp) - - current_vcs = component.get("vcs", "") - # Note: The VCS/Repository URL can only be updated after the rollout of Issue 1886 - # (https://code.siemens.com/sw360/sw360portal/-/issues/1886) - before that, - # the VCS/Repository URL is not available in the SW360 REST API and thus cannot be updated. - if repo: - print_yellow( - f" Not available until rollout of Issue 1886. " - f"Could update VCS/Repository URL from: {current_vcs if current_vcs else ''} " - f"with repository: {repo}..." - ) - # try: - # updated_component = self.client.update_component({"vcs": str(repo)}, component_id) - # print_green(f" Successfully updated VCS/Repository URL to: {updated_component}") - # except SW360Error as e: - # print_red(f" Failed to update VCS/Repository URL: {e}") - elif website: - print_yellow( - f" Not available until rollout of Issue 1886. " - f"Could update VCS/Repository URL from: {current_vcs if current_vcs else ''} " - f"with website: {website}..." - ) - # try: - # updated_component = self.client.update_component({"vcs": str(website)}, component_id) - # print_green(f" Successfully updated VCS/Repository URL to: {updated_component}") - # except SW360Error as e: - # print_red(f" Failed to update VCS/Repository URL: {e}") + print_red(" No Source Code Download URL found.") bin_url = str(CycloneDxSupport.get_ext_ref_binary_url(cx_comp)) if bin_url: diff --git a/tests/test_bom_create_components.py b/tests/test_bom_create_components.py index f38b1204..9755ba13 100644 --- a/tests/test_bom_create_components.py +++ b/tests/test_bom_create_components.py @@ -61,6 +61,7 @@ def test_create_component(self) -> None: release_data = {"name": "activemodel", "version": "5.2.4.3", "mainlineState": "OPEN", "languages": ["Ruby"], + "sourceCodeDownloadurl": "http://test.org/activemodel@5.2.4.3.zip", "externalIds": {"package-url": "pkg:gem/activemodel@5.2.4.3"}, "additionalData": {"createdWith": capycli.get_app_signature()}} responses.add( @@ -84,6 +85,7 @@ def test_create_component(self) -> None: CycloneDxSupport.update_or_set_property(item, CycloneDxSupport.CDX_PROP_LANGUAGE, "Ruby") CycloneDxSupport.update_or_set_property(item, CycloneDxSupport.CDX_PROP_CATEGORIES, "devel") CycloneDxSupport.update_or_set_ext_ref(item, ExternalReferenceType.WEBSITE, "", "http://test.org") + CycloneDxSupport.update_or_set_ext_ref(item, ExternalReferenceType.SOURCE_DISTRIBUTION, "", "http://test.org/activemodel@5.2.4.3.zip") self.app.create_component_and_release(item) assert len(responses.calls) == 3 diff --git a/tests/test_bom_create_releases.py b/tests/test_bom_create_releases.py index 54179425..c0571864 100644 --- a/tests/test_bom_create_releases.py +++ b/tests/test_bom_create_releases.py @@ -7,7 +7,6 @@ # ------------------------------------------------------------------------------- """unit tests for bom/create_components.py in createreleases mode""" -import sys from typing import Any, Dict, Tuple import pytest @@ -16,9 +15,6 @@ from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component from packageurl import PackageURL -import logging -import io - import capycli.bom.create_components from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport @@ -62,16 +58,6 @@ def setUp(self) -> None: responses.add(responses.GET, SW360_BASE_URL, json={"status": "ok"}) self.app.login("sometoken", "https://my.server.com") - def capture_logs(self): - """Redirect logging output to capsys""" - logger = logging.getLogger("capycli.common.print") - logger.setLevel(logging.DEBUG) # Ensure all log levels are captured - handler = logging.StreamHandler(sys.stdout) - handler.setLevel(logging.DEBUG) - logger.addHandler(handler) - return logger, handler - - @responses.activate def test_create_items_existing_release_with_id(self) -> None: """Release exists and was identified in "bom map" @@ -390,7 +376,6 @@ def test_create_comp_release_component_id(self) -> None: self.app.create_component_and_release(item) assert CycloneDxSupport.get_property_value(item, CycloneDxSupport.CDX_PROP_SW360ID) == "06a6e7" - @pytest.mark.skip(reason="Fails after the Source Code Download URL is no longer updated with the VCS value.") @responses.activate def test_create_comp_release_component_id_update(self) -> None: """We have a componentId match. Update package-url if needed. @@ -457,30 +442,25 @@ def test_create_comp_release_component_id_update(self) -> None: def test_update_component_other_purl(self) -> None: """Existing component has different purl, so issue warning. """ - logger, handler = self.capture_logs() - try: - component_data: Dict[str, Any] = { + component_data: Dict[str, Any] = { + "name": "activemodel", + "externalIds": {"package-url": "pkg:deb/ubuntu/activemodel?arch=source"}, + "_links": {"self": { + "href": SW360_BASE_URL + "components/06a6e5"}}, + "_embedded": {"sw360:releases": [{ "name": "activemodel", - "externalIds": {"package-url": "pkg:deb/ubuntu/activemodel?arch=source"}, + "version": "5.2.1", "_links": {"self": { - "href": SW360_BASE_URL + "components/06a6e5"}}, - "_embedded": {"sw360:releases": [{ - "name": "activemodel", - "version": "5.2.1", - "_links": {"self": { - "href": SW360_BASE_URL + "releases/06a6e6"}}}]}} - item = Component( - name="activemodel", - version="5.2.1", - purl=PackageURL.from_string("pkg:deb/debian/activemodel@5.2.1?arch=source") - ) - CycloneDxSupport.update_or_set_property(item, CycloneDxSupport.CDX_PROP_COMPONENT_ID, "06a6e5") - self.app.update_component(item, "123", component_data) - - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM id" in captured.out - finally: - logger.removeHandler(handler) + "href": SW360_BASE_URL + "releases/06a6e6"}}}]}} + item = Component( + name="activemodel", + version="5.2.1", + purl=PackageURL.from_string("pkg:deb/debian/activemodel@5.2.1?arch=source") + ) + CycloneDxSupport.update_or_set_property(item, CycloneDxSupport.CDX_PROP_COMPONENT_ID, "06a6e5") + self.app.update_component(item, "123", component_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM id" in captured.out component_data["externalIds"]["package-url"] = ('[' '"pkg:deb/ubuntu/activemodel?arch=source",' @@ -711,31 +691,27 @@ def test_upload_file_local(self) -> None: def test_upload_file_prevent_git_source_upload(self) -> None: """Prevent uploading SOURCE, SOURCE_SELF file with .git file type """ - logger, handler = self.capture_logs() - try: - responses.add( - responses.GET, 'https://github.com/babel/babel.git', - body="content") - - self.app.download = True - item = Component( - name="activemodel", - version="5.2.1" - ) - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_URL_COMMENT, "https://github.com/babel/babel.git") - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_FILE_COMMENT, "babel.git") - - self.app.upload_file(item, {}, "06a6e7", "SOURCE", "") - captured = self.capsys.readouterr() # type: ignore - assert len(responses.calls) == 0 - assert "WARNING: resetting filename to prevent uploading .git file" in captured.out - assert captured.err == "" - finally: - logger.removeHandler(handler) + responses.add( + responses.GET, 'https://github.com/babel/babel.git', + body="content") + + self.app.download = True + item = Component( + name="activemodel", + version="5.2.1" + ) + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_URL_COMMENT, "https://github.com/babel/babel.git") + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_FILE_COMMENT, "babel.git") + + self.app.upload_file(item, {}, "06a6e7", "SOURCE", "") + captured = self.capsys.readouterr() # type: ignore + assert len(responses.calls) == 0 + assert "WARNING: resetting filename to prevent uploading .git file" in captured.out + assert captured.err == "" @responses.activate def test_upload_file_allow_git_binary_upload(self) -> None: @@ -848,319 +824,303 @@ def test_upload_file_source_dir(self) -> None: def test_update_release_SourceUrl(self) -> None: """Update SourceUrl in existing release """ - logger, handler = self.capture_logs() - try: - # no existing URL, no new URL - release_data: Dict[str, Any] = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - - item2 = Component(name="") - self.app.update_release(item2, release_data) - - # existing URL, no new URL - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, - "sourceCodeDownloadurl": "old_url" - } - item2 = Component(name="") - self.app.update_release(item2, release_data) - - # existing URL equals to new URL - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_URL_COMMENT, "old_url") - self.app.update_release(item2, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM URL" not in captured.out - - # existing URL differs from new URL - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_URL_COMMENT, "https://some.new/file.tar.gz") - self.app.update_release(item2, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM URL" in captured.out - assert len(responses.calls) == 0 # assure data in SW360 is not changed - - # no existing URL, set new URL - responses.add( - responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', - match=[responses.matchers.json_params_matcher({ - "sourceCodeDownloadurl": "new_url"})], - # server answer with created release data - json={ - "sourceCodeDownloadurl": "new_url", - "_links": {"self": { - "href": SW360_BASE_URL + "releases/06a6e7"}}}) - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_URL_COMMENT, "new_url") - self.app.update_release(item2, release_data) - assert len(responses.calls) == 1 - - # existing URL empty, set new URL - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, - "sourceCodeDownloadurl": "" - } - self.app.update_release(item2, release_data) - assert len(responses.calls) == 2 - finally: - logger.removeHandler(handler) + # no existing URL, no new URL + release_data: Dict[str, Any] = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + + item2 = Component(name="") + self.app.update_release(item2, release_data) + + # existing URL, no new URL + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, + "sourceCodeDownloadurl": "old_url" + } + item2 = Component(name="") + self.app.update_release(item2, release_data) + + # existing URL equals to new URL + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_URL_COMMENT, "old_url") + self.app.update_release(item2, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM URL" not in captured.out + + # existing URL differs from new URL + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_URL_COMMENT, "https://some.new/file.tar.gz") + self.app.update_release(item2, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM URL" in captured.out + assert len(responses.calls) == 0 # assure data in SW360 is not changed + + # no existing URL, set new URL + responses.add( + responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', + match=[responses.matchers.json_params_matcher({ + "sourceCodeDownloadurl": "new_url"})], + # server answer with created release data + json={ + "sourceCodeDownloadurl": "new_url", + "_links": {"self": { + "href": SW360_BASE_URL + "releases/06a6e7"}}}) + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_URL_COMMENT, "new_url") + self.app.update_release(item2, release_data) + assert len(responses.calls) == 1 + + # existing URL empty, set new URL + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, + "sourceCodeDownloadurl": "" + } + self.app.update_release(item2, release_data) + assert len(responses.calls) == 2 @responses.activate def test_update_release_BinaryUrl(self) -> None: """Update SourceUrl in existing release """ - logger, handler = self.capture_logs() - try: - # no existing URL, no new URL - release_data: Dict[str, Any] = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - - item2 = Component(name="") - self.app.update_release(item2, release_data) - - # existing URL, no new URL - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, - "binaryDownloadurl": "old_url" - } - item2 = Component(name="") - self.app.update_release(item2, release_data) - - # existing URL equals to new URL - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.BINARY_URL_COMMENT, "old_url") - self.app.update_release(item2, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM URL" not in captured.out - - # existing URL differs from new URL - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.BINARY_URL_COMMENT, "new_url") - self.app.update_release(item2, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM URL" in captured.out - - # no existing URL, set new URL - responses.add( - responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', - match=[responses.matchers.json_params_matcher({ - "binaryDownloadurl": "new_url"})], - # server answer with created release data - json={ - "binaryDownloadurl": "new_url", - "_links": {"self": { - "href": SW360_BASE_URL + "releases/06a6e7"}}}) - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - item2 = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item2, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.BINARY_URL_COMMENT, "new_url") - self.app.update_release(item2, release_data) - assert len(responses.calls) == 1 - - # existing URL empty, set new URL - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, - "binaryDownloadurl": "" - } - self.app.update_release(item2, release_data) - assert len(responses.calls) == 2 - finally: - logger.removeHandler(handler) + # no existing URL, no new URL + release_data: Dict[str, Any] = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + + item2 = Component(name="") + self.app.update_release(item2, release_data) + + # existing URL, no new URL + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, + "binaryDownloadurl": "old_url" + } + item2 = Component(name="") + self.app.update_release(item2, release_data) + + # existing URL equals to new URL + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.BINARY_URL_COMMENT, "old_url") + self.app.update_release(item2, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM URL" not in captured.out + + # existing URL differs from new URL + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.BINARY_URL_COMMENT, "new_url") + self.app.update_release(item2, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM URL" in captured.out + + # no existing URL, set new URL + responses.add( + responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', + match=[responses.matchers.json_params_matcher({ + "binaryDownloadurl": "new_url"})], + # server answer with created release data + json={ + "binaryDownloadurl": "new_url", + "_links": {"self": { + "href": SW360_BASE_URL + "releases/06a6e7"}}}) + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + item2 = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item2, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.BINARY_URL_COMMENT, "new_url") + self.app.update_release(item2, release_data) + assert len(responses.calls) == 1 + + # existing URL empty, set new URL + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}, + "binaryDownloadurl": "" + } + self.app.update_release(item2, release_data) + assert len(responses.calls) == 2 @responses.activate def test_update_release_externalId(self) -> None: """Update externalId in existing release """ - logger, handler = self.capture_logs() - try: - # existing externalId, no new Id -> do nothing, "%7E" = "~" - release_data: Dict[str, Any] = { - "externalIds": {"package-url": "pkg:deb/debian/bash@1.0%7E1"}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - item = Component(name="") - self.app.update_release(item, release_data) - - # existing Id same as new Id - item = Component( - name="", - purl=PackageURL.from_string("pkg:deb/debian/bash@1.0%7E1") - ) - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM id" not in captured.out - - item.purl = PackageURL.from_string("pkg:deb/debian/bash@1.0~1") - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM id" not in captured.out - - # existing Id differs from new Id -> only warn - item.purl = PackageURL.from_string("pkg:deb/debian/bash@2.0") - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM id" in captured.out - assert item.purl.to_string() == "pkg:deb/debian/bash@2.0" - - # existing Id invalid - release_data["externalIds"]["package-url"] = "pkg:something" # invalid purl - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "differs from BOM id" in captured.out - assert item.purl.to_string() == "pkg:deb/debian/bash@2.0" - - # add new Id, no existing ID - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - responses.add( - responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', - match=[responses.matchers.json_params_matcher({ - "externalIds": { - "package-url": "pkg:deb/debian/bash@2.0"}})], - # server answer with created release data - json={ - "_links": {"self": { - "href": SW360_BASE_URL + "releases/06a6e7"}}}) - self.app.update_release(item, release_data) - assert len(responses.calls) == 1 - - release_data = { - "externalIds": {}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - self.app.update_release(item, release_data) - assert len(responses.calls) == 2 - - # add new Id to existing IDs -> assure we keep existing ones - release_data = { - "externalIds": {"some_id": "42"}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} - } - responses.replace( - responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', - match=[responses.matchers.json_params_matcher({ - "externalIds": { - "some_id": "42", - "package-url": "pkg:deb/debian/bash@2.0"}})], - # server answer with created release data - json={ - "_links": {"self": { - "href": SW360_BASE_URL + "releases/06a6e7"}}}) - self.app.update_release(item, release_data) - assert len(responses.calls) == 3 - finally: - logger.removeHandler(handler) + # existing externalId, no new Id -> do nothing, "%7E" = "~" + release_data: Dict[str, Any] = { + "externalIds": {"package-url": "pkg:deb/debian/bash@1.0%7E1"}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + item = Component(name="") + self.app.update_release(item, release_data) + + # existing Id same as new Id + item = Component( + name="", + purl=PackageURL.from_string("pkg:deb/debian/bash@1.0%7E1") + ) + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM id" not in captured.out + + item.purl = PackageURL.from_string("pkg:deb/debian/bash@1.0~1") + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM id" not in captured.out + + # existing Id differs from new Id -> only warn + item.purl = PackageURL.from_string("pkg:deb/debian/bash@2.0") + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM id" in captured.out + assert item.purl.to_string() == "pkg:deb/debian/bash@2.0" + + # existing Id invalid + release_data["externalIds"]["package-url"] = "pkg:something" # invalid purl + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "differs from BOM id" in captured.out + assert item.purl.to_string() == "pkg:deb/debian/bash@2.0" + + # add new Id, no existing ID + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + responses.add( + responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', + match=[responses.matchers.json_params_matcher({ + "externalIds": { + "package-url": "pkg:deb/debian/bash@2.0"}})], + # server answer with created release data + json={ + "_links": {"self": { + "href": SW360_BASE_URL + "releases/06a6e7"}}}) + self.app.update_release(item, release_data) + assert len(responses.calls) == 1 + + release_data = { + "externalIds": {}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + self.app.update_release(item, release_data) + assert len(responses.calls) == 2 + + # add new Id to existing IDs -> assure we keep existing ones + release_data = { + "externalIds": {"some_id": "42"}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}} + } + responses.replace( + responses.PATCH, SW360_BASE_URL + 'releases/06a6e7', + match=[responses.matchers.json_params_matcher({ + "externalIds": { + "some_id": "42", + "package-url": "pkg:deb/debian/bash@2.0"}})], + # server answer with created release data + json={ + "_links": {"self": { + "href": SW360_BASE_URL + "releases/06a6e7"}}}) + self.app.update_release(item, release_data) + assert len(responses.calls) == 3 @responses.activate def test_update_release_attachment(self) -> None: """Upload to existing release """ - logger, handler = self.capture_logs() - try: - responses.add(responses.GET, SW360_BASE_URL + "attachments/0123", - json={}) - - # existing source, no new source -> do nothing - release_data = { - '_embedded': {'sw360:attachments': [{ - 'filename': 'adduser-3.118.zip', - 'attachmentType': 'SOURCE'}]}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - item = Component(name="") - self.app.update_release(item, release_data) - - # existing source same as new source - release_data = { - '_embedded': {'sw360:attachments': [{ - 'filename': 'some.txt', - 'attachmentType': 'DOCUMENT'}, { - '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, - 'filename': 'adduser-3.118.zip', - 'attachmentType': 'SOURCE_SELF'}]}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - item = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_FILE_COMMENT, "adduser-3.118.zip") - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "different source attachment" not in captured.out - assert len(responses.calls) == 1 - - # existing source with different hash -> do nothing - release_data = { - '_embedded': {'sw360:attachments': [{ - '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, - 'filename': 'adduser-3.118.zip', - 'sha1': '123', - 'attachmentType': 'SOURCE'}]}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - item = Component(name="") - extref = ExternalReference( - type=ExternalReferenceType.DISTRIBUTION, - comment=CaPyCliBom.SOURCE_FILE_COMMENT, - url=XsUri("adduser-3.118.zip")) - extref.hashes.add(HashType( - alg=HashAlgorithm.SHA_1, - content="456")) - item.external_references.add(extref) - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "different hash for source attachment" in captured.out - assert len(responses.calls) == 2 - - # existing source, different source -> do nothing - item = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_FILE_COMMENT, "Readme.md") - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "different source attachment" in captured.out - assert len(responses.calls) == 3 - - # no attachment existing -> upload - release_data = { - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - item = Component(name="") - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_FILE_COMMENT, "Readme.md") - responses.add( - responses.POST, SW360_BASE_URL + 'releases/06a6e7/attachments', - match=[upload_matcher("Readme.md")]) - self.app.update_release(item, release_data) - assert len(responses.calls) == 4 - - # only other attachments existing -> upload - release_data = { - '_embedded': {'sw360:attachments': [{ - 'filename': 'some.txt', - 'attachmentType': 'DOCUMENT'}]}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - self.app.update_release(item, release_data) - assert len(responses.calls) == 5 - finally: - logger.removeHandler(handler) + responses.add(responses.GET, SW360_BASE_URL + "attachments/0123", + json={}) + + # existing source, no new source -> do nothing + release_data = { + '_embedded': {'sw360:attachments': [{ + 'filename': 'adduser-3.118.zip', + 'attachmentType': 'SOURCE'}]}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + item = Component(name="") + self.app.update_release(item, release_data) + + # existing source same as new source + release_data = { + '_embedded': {'sw360:attachments': [{ + 'filename': 'some.txt', + 'attachmentType': 'DOCUMENT'}, { + '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, + 'filename': 'adduser-3.118.zip', + 'attachmentType': 'SOURCE_SELF'}]}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + item = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_FILE_COMMENT, "adduser-3.118.zip") + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "different source attachment" not in captured.out + assert len(responses.calls) == 1 + + # existing source with different hash -> do nothing + release_data = { + '_embedded': {'sw360:attachments': [{ + '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, + 'filename': 'adduser-3.118.zip', + 'sha1': '123', + 'attachmentType': 'SOURCE'}]}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + item = Component(name="") + extref = ExternalReference( + type=ExternalReferenceType.DISTRIBUTION, + comment=CaPyCliBom.SOURCE_FILE_COMMENT, + url=XsUri("adduser-3.118.zip")) + extref.hashes.add(HashType( + alg=HashAlgorithm.SHA_1, + content="456")) + item.external_references.add(extref) + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "different hash for source attachment" in captured.out + assert len(responses.calls) == 2 + + # existing source, different source -> do nothing + item = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_FILE_COMMENT, "Readme.md") + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "different source attachment" in captured.out + assert len(responses.calls) == 3 + + # no attachment existing -> upload + release_data = { + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + item = Component(name="") + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_FILE_COMMENT, "Readme.md") + responses.add( + responses.POST, SW360_BASE_URL + 'releases/06a6e7/attachments', + match=[upload_matcher("Readme.md")]) + self.app.update_release(item, release_data) + assert len(responses.calls) == 4 + + # only other attachments existing -> upload + release_data = { + '_embedded': {'sw360:attachments': [{ + 'filename': 'some.txt', + 'attachmentType': 'DOCUMENT'}]}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + self.app.update_release(item, release_data) + assert len(responses.calls) == 5 @responses.activate def test_update_release_attachment_rejected(self) -> None: @@ -1192,40 +1152,36 @@ def test_update_release_attachment_rejected(self) -> None: def test_update_release_attachment_rename(self) -> None: """Upload to existing release with content-disposition rename """ - logger, handler = self.capture_logs() - try: - # attachment with target file name after rename exists -> don't upload - # (test for commit 2e403af032) - responses.add(responses.GET, SW360_BASE_URL + "attachments/0123", - json={}) - responses.add( - responses.GET, 'https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip', - headers={"content-disposition": 'attachment; filename=babel-7.16.0.zip'}, - body="content") - release_data = { - "sourceCodeDownloadurl": "https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip", - '_embedded': {'sw360:attachments': [{ - '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, - 'filename': 'babel-7.16.0.zip', - 'attachmentType': 'SOURCE'}]}, - "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} - self.app.download = True - item = Component( - name="babel", - version="7.16.0" - ) - CycloneDxSupport.update_or_set_ext_ref( - item, ExternalReferenceType.DISTRIBUTION, - CaPyCliBom.SOURCE_URL_COMMENT, "https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip") - self.app.update_release(item, release_data) - captured = self.capsys.readouterr() # type: ignore - assert "different source attachment" in captured.out - - # currently, upload_file() will do nothing if *any* source attachment exists, - # so there should be 0 calls. If this semantics changes and content-disposition - # handling avoids duplicated uploads, we will see 1 call, so accept 0 or 1 here. - assert len(responses.calls) <= 1 - assert "Error" not in captured.out - assert captured.err == "" - finally: - logger.removeHandler(handler) + # attachment with target file name after rename exists -> don't upload + # (test for commit 2e403af032) + responses.add(responses.GET, SW360_BASE_URL + "attachments/0123", + json={}) + responses.add( + responses.GET, 'https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip', + headers={"content-disposition": 'attachment; filename=babel-7.16.0.zip'}, + body="content") + release_data = { + "sourceCodeDownloadurl": "https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip", + '_embedded': {'sw360:attachments': [{ + '_links': {'self': {'href': SW360_BASE_URL + 'attachments/0123'}}, + 'filename': 'babel-7.16.0.zip', + 'attachmentType': 'SOURCE'}]}, + "_links": {"self": {"href": SW360_BASE_URL + "releases/06a6e7"}}} + self.app.download = True + item = Component( + name="babel", + version="7.16.0" + ) + CycloneDxSupport.update_or_set_ext_ref( + item, ExternalReferenceType.DISTRIBUTION, + CaPyCliBom.SOURCE_URL_COMMENT, "https://github.com/babel/babel/archive/refs/tags/v7.16.0.zip") + self.app.update_release(item, release_data) + captured = self.capsys.readouterr() # type: ignore + assert "different source attachment" in captured.out + + # currently, upload_file() will do nothing if *any* source attachment exists, + # so there should be 0 calls. If this semantics changes and content-disposition + # handling avoids duplicated uploads, we will see 1 call, so accept 0 or 1 here. + assert len(responses.calls) <= 1 + assert "Error" not in captured.out + assert captured.err == ""