From b78db0c1c263db61ff97279ab40337f404463b23 Mon Sep 17 00:00:00 2001 From: RAJVEER42 Date: Mon, 18 May 2026 22:08:50 +0530 Subject: [PATCH] fix(report): raise FossologyApiError when report ID cannot be parsed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The regex `[0-9]*$` (zero-or-more) always matches — even on a message with no trailing digits — so `re.search` returned an empty-string match and the caller got `""` with no error. Switch to `[0-9]+$` and raise `FossologyApiError` when no ID can be parsed. Wrap the parse block (JSON decode, message lookup, regex search) in `try/except (ValueError, KeyError, TypeError)` so malformed JSON, missing keys, and non-string `message` values surface as `FossologyApiError` consistently rather than leaking raw exceptions. Coerce the returned report ID to `int` to match the docstring `:rtype: int` and the `download_report(report_id: int, ...)` parameter type. Tests cover the five paths: int return on happy path, missing trailing digits, malformed JSON body, missing "message" key, non-string "message" value. Closes #176 Signed-off-by: RAJVEER42 --- fossology/report.py | 19 ++++++++-- tests/test_report.py | 82 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/fossology/report.py b/fossology/report.py index 10f55d9..3217d51 100644 --- a/fossology/report.py +++ b/fossology/report.py @@ -53,8 +53,23 @@ def generate_report( response = self.session.get(f"{self.api}/report", headers=headers) if response.status_code == 201: - report_id = re.search("[0-9]*$", response.json()["message"]) - return report_id[0] # type: ignore + message = None + try: + message = response.json()["message"] + match = re.search(r"[0-9]+$", message) + except (ValueError, KeyError, TypeError) as exc: + description = ( + f"Report generation for upload {upload.uploadname} succeeded " + f"but response could not be parsed: {exc}" + ) + raise FossologyApiError(description, response) from exc + if match is None: + description = ( + f"Report generation for upload {upload.uploadname} succeeded " + f"but report ID could not be parsed from response message: {message!r}" + ) + raise FossologyApiError(description, response) + return int(match[0]) elif response.status_code == 403: description = f"Report generation for upload {upload.id} not authorized" diff --git a/tests/test_report.py b/tests/test_report.py index b3a25ad..97e21d3 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -47,6 +47,88 @@ def test_generate_report(foss: Fossology, upload: Upload): Path(report_path / report_name).unlink() +@responses.activate +def test_generate_report_unparseable_message( + foss_server: str, foss: Fossology, upload: Upload +): + responses.add( + responses.GET, + f"{foss_server}/api/v1/report", + status=201, + json={"code": 201, "message": "Report has been queued."}, + ) + with pytest.raises(FossologyApiError) as excinfo: + foss.generate_report(upload) + assert "report ID could not be parsed" in str(excinfo.value) + + +@responses.activate +def test_generate_report_malformed_response_body( + foss_server: str, foss: Fossology, upload: Upload +): + responses.add( + responses.GET, + f"{foss_server}/api/v1/report", + status=201, + body="Internal Server Error", + content_type="text/html", + ) + with pytest.raises(FossologyApiError) as excinfo: + foss.generate_report(upload) + assert "response could not be parsed" in str(excinfo.value) + + +@responses.activate +def test_generate_report_missing_message_key( + foss_server: str, foss: Fossology, upload: Upload +): + responses.add( + responses.GET, + f"{foss_server}/api/v1/report", + status=201, + json={"code": 201, "type": "INFO"}, + ) + with pytest.raises(FossologyApiError) as excinfo: + foss.generate_report(upload) + assert "response could not be parsed" in str(excinfo.value) + + +@responses.activate +def test_generate_report_non_string_message( + foss_server: str, foss: Fossology, upload: Upload +): + responses.add( + responses.GET, + f"{foss_server}/api/v1/report", + status=201, + json={"code": 201, "message": 42}, + ) + with pytest.raises(FossologyApiError) as excinfo: + foss.generate_report(upload) + assert "response could not be parsed" in str(excinfo.value) + + +@responses.activate +def test_generate_report_returns_int( + foss_server: str, foss: Fossology, upload: Upload +): + responses.add( + responses.GET, + f"{foss_server}/api/v1/report", + status=201, + json={ + "code": 201, + "message": ( + "Report will be generated in the back ground, " + "report id is 42" + ), + }, + ) + report_id = foss.generate_report(upload) + assert report_id == 42 + assert isinstance(report_id, int) + + @responses.activate def test_report_error(foss_server: str, foss: Fossology, upload: Upload): responses.add(