From 93f06003069d52e0946f4650766d7c89e749b53b Mon Sep 17 00:00:00 2001 From: Gernot Hillier Date: Wed, 29 Jul 2026 11:08:01 +0200 Subject: [PATCH 1/3] test(check_bom): fix and re-enable tests for error cases The initial tests used HTTP 500 errors which would lead to multiple urllib3 retries and long timeouts, so use 403 (forbidden) instead. Also fix some responses so we don't run into unrelated errors and remove two unneeded responses. --- tests/test_check_bom.py | 76 ++++++++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/tests/test_check_bom.py b/tests/test_check_bom.py index bfd1f343..56ee7b09 100644 --- a/tests/test_check_bom.py +++ b/tests/test_check_bom.py @@ -323,7 +323,7 @@ def test_simple_bom_without_id(self) -> None: self.assertTrue("wheel, 0.38.4" in out) @responses.activate - def xx_test_simple_bom_with_errors(self) -> None: + def test_simple_bom_with_errors(self) -> None: sut = CheckBom() # create argparse command line argument object @@ -361,7 +361,7 @@ def xx_test_simple_bom_with_errors(self) -> None: responses.GET, url=self.MYURL + "resource/api/releases/05c30bf89a512463260b57e84d99b38f", body='{"name": "python", "version": "3.8"}', - status=500, # internal server error + status=403, # forbidden (don't use 500 as this leads to multiple retries by urllib3) content_type="application/json", adding_headers={"Authorization": "Token " + self.MYTOKEN}, ) @@ -370,7 +370,17 @@ def xx_test_simple_bom_with_errors(self) -> None: responses.add( responses.GET, url=self.MYURL + "resource/api/releases/fa0d21eb17574ba9ae17e5c9b432558e", - body='{"name": "tomli", "version": "2.0.1"}', + body=''' + { + "name": "tomli", + "version": "2.0.1", + "_links" : { + "self" : { + "href" : "https://my.server.com/resource/api/releases/fa0d21eb17574ba9ae17e5c9b432558e" + } + } + } + ''', status=200, content_type="application/json", adding_headers={"Authorization": "Token " + self.MYTOKEN}, @@ -380,7 +390,17 @@ def xx_test_simple_bom_with_errors(self) -> None: responses.add( responses.GET, url=self.MYURL + "resource/api/releases/e0995819173d4ac8b1a4da3548935976", - body='{"name": "wheel", "version": "0.38.4"}', + body=''' + { + "name": "wheel", + "version": "0.38.4", + "_links" : { + "self" : { + "href" : "https://my.server.com/resource/api/releases/e0995819173d4ac8b1a4da3548935976" + } + } + } + ''', status=200, content_type="application/json", adding_headers={"Authorization": "Token " + self.MYTOKEN}, @@ -393,7 +413,7 @@ def xx_test_simple_bom_with_errors(self) -> None: self.assertTrue("wheel, 0.38.4" in out) @responses.activate - def xx_test_simple_bom_without_id_with_errors(self) -> None: + def test_simple_bom_without_id_with_errors(self) -> None: sut = CheckBom() # create argparse command line argument object @@ -419,7 +439,17 @@ def xx_test_simple_bom_without_id_with_errors(self) -> None: responses.add( responses.GET, url=self.MYURL + "resource/api/releases/9a2373710bd44769a2560dd31280901d", - body='{"name": "colorama", "version": "0.4.6"}', + body=''' + { + "name": "colorama", + "version": "0.4.6", + "_links" : { + "self" : { + "href" : "https://my.server.com/resource/api/releases/9a2373710bd44769a2560dd31280901d" + } + } + } + ''', status=200, content_type="application/json", adding_headers={"Authorization": "Token " + self.MYTOKEN}, @@ -429,7 +459,17 @@ def xx_test_simple_bom_without_id_with_errors(self) -> None: responses.add( responses.GET, url=self.MYURL + "resource/api/releases/05c30bf89a512463260b57e84d99b38f", - body='{"name": "python", "version": "3.8"}', + body=''' + { + "name": "python", + "version": "3.8", + "_links" : { + "self" : { + "href" : "https://my.server.com/resource/api/releases/05c30bf89a512463260b57e84d99b38f" + } + } + } + ''', status=200, content_type="application/json", adding_headers={"Authorization": "Token " + self.MYTOKEN}, @@ -455,16 +495,6 @@ def xx_test_simple_bom_without_id_with_errors(self) -> None: adding_headers={"Authorization": "Token " + self.MYTOKEN}, ) - # for tomli (2) - responses.add( - responses.GET, - url=self.MYURL + "resource/api/releases/fa0d21eb17574ba9ae17e5c9b432558e", - body='{"name": "tomli", "version": "2.0.1"}', - status=200, - content_type="application/json", - adding_headers={"Authorization": "Token " + self.MYTOKEN}, - ) - # for wheel (1) responses.add( responses.GET, @@ -481,17 +511,7 @@ def xx_test_simple_bom_without_id_with_errors(self) -> None: } } ] }}''', - status=500, - content_type="application/json", - adding_headers={"Authorization": "Token " + self.MYTOKEN}, - ) - - # for wheel (2) - responses.add( - responses.GET, - url=self.MYURL + "resource/api/releases/e0995819173d4ac8b1a4da3548935976", - body='{"name": "wheel", "version": "0.38.4"}', - status=200, + status=403, # forbidden (don't use 500 as this leads to multiple retries by urllib3) content_type="application/json", adding_headers={"Authorization": "Token " + self.MYTOKEN}, ) From 543bd2d80d2a9d1f04a5309e9bb09ee8d27a5cd5 Mon Sep 17 00:00:00 2001 From: Gernot Hillier Date: Wed, 29 Jul 2026 12:15:51 +0200 Subject: [PATCH 2/3] fix(bom check): inform user about HTTP errors when getting release The current code tries to assure HTTP response objects are valid before trying to print them, but due to the HTTP requests API, error responses are considered False so they were not printed. Thanks to Raul Fuentes for reporting. Fixes #227 --- capycli/bom/check_bom.py | 4 ++-- tests/test_check_bom.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/capycli/bom/check_bom.py b/capycli/bom/check_bom.py index 6af5b206..de715548 100644 --- a/capycli/bom/check_bom.py +++ b/capycli/bom/check_bom.py @@ -66,7 +66,7 @@ def _find_by_id(self, component: Component) -> Optional[Dict[str, Any]]: print( " " + component.name + ", " + version + ", " + sw360id) - if swex.response: + if swex.response is not None: print(" Status Code: " + str(swex.response.status_code)) if swex.message: print(" Message: " + swex.message) @@ -105,7 +105,7 @@ def _find_by_name(self, component: Component) -> Optional[Dict[str, Any]]: print(Fore.LIGHTRED_EX + " Error retrieving release data: ") print( " " + component.name + ", " + version) - if swex.response: + if swex.response is not None: print(" Status Code: " + str(swex.response.status_code)) if swex.message: print(" Message: " + swex.message) diff --git a/tests/test_check_bom.py b/tests/test_check_bom.py index 56ee7b09..da9b8e2f 100644 --- a/tests/test_check_bom.py +++ b/tests/test_check_bom.py @@ -411,6 +411,7 @@ def test_simple_bom_with_errors(self) -> None: self.assertTrue("python, 3.8" in out) self.assertTrue("tomli, 2.0.1" in out) self.assertTrue("wheel, 0.38.4" in out) + self.assertTrue("Status Code: 403" in out) @responses.activate def test_simple_bom_without_id_with_errors(self) -> None: @@ -521,6 +522,7 @@ def test_simple_bom_without_id_with_errors(self) -> None: self.assertTrue("python, 3.8" in out) self.assertTrue("tomli, 2.0.1" in out) self.assertTrue("wheel, 0.38.4" in out) + self.assertIn("Status Code: 403", out) @responses.activate def xxx_test_simple_bom_show_all(self) -> None: From f08d63778af8c1319a1c21cc8bd436f364b28670 Mon Sep 17 00:00:00 2001 From: Gernot Hillier Date: Wed, 29 Jul 2026 12:31:23 +0200 Subject: [PATCH 3/3] fix(project create): inform user about HTTP errors when updating project Same as in 543bd2d8, requests.Response objects are considered "False" for HTTP errors, so we must not check for True. And there's no need for the other else paths as the first "if" already tells us that we have swex.response so there can't be another error case. --- capycli/project/create_project.py | 9 +-------- tests/test_update_project.py | 12 +++++++----- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/capycli/project/create_project.py b/capycli/project/create_project.py index 61d45ce6..be9deadd 100644 --- a/capycli/project/create_project.py +++ b/capycli/project/create_project.py @@ -146,14 +146,7 @@ def update_project(self, project_id: str, project: Optional[Dict[str, Any]], if swex.response.status_code == requests.codes["forbidden"]: print_red(" You are not authorized - do you have a valid write token?") sys.exit(ResultCode.RESULT_AUTH_ERROR) - if swex.response: - print_red(" " + str(swex.response.status_code) + ": " + swex.response.text) - sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360) - if swex.details: - print_red(" " + swex.details.get("error", "") + ": " + swex.details.get("message", "")) - sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360) - - print_red(" Unknown error updating project: " + repr(swex)) + print_red(" " + str(swex.response.status_code) + ": " + swex.response.text) sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360) def update_project_version(self, project_id: str, project: Dict[str, Any], new_version: str) -> None: diff --git a/tests/test_update_project.py b/tests/test_update_project.py index 28877316..99f0b2fc 100644 --- a/tests/test_update_project.py +++ b/tests/test_update_project.py @@ -4,6 +4,7 @@ from cyclonedx.model.bom import Bom from pytest import fixture, raises +from requests import Response from sw360 import SW360Error from capycli.main.result_codes import ResultCode @@ -51,12 +52,13 @@ class DummyResp: @fixture -def dummy_response() -> Callable[[int, str], Callable[[int, str], MagicMock]]: - """Fixture to create a dummy response object.""" - def _dummy_response(status_code: int, text: str) -> Callable[[int, str], MagicMock]: - result = MagicMock() +def dummy_response() -> Callable[[int, str], Response]: + """Fixture to create a dummy response object with real requests.Response + so that bool(response) correctly returns False for non-2xx status codes.""" + def _dummy_response(status_code: int, text: str) -> Response: + result = Response() result.status_code = status_code - result.text = text + result._content = text.encode("utf-8") return result return _dummy_response