diff --git a/changelog/473.fixed.md b/changelog/473.fixed.md new file mode 100644 index 00000000..22c5a5af --- /dev/null +++ b/changelog/473.fixed.md @@ -0,0 +1 @@ +JsonDecodeError now includes server response content in error message when JSON decoding fails, providing better debugging information for non-JSON server responses. \ No newline at end of file diff --git a/infrahub_sdk/exceptions.py b/infrahub_sdk/exceptions.py index a8b1ef9b..d8982d8e 100644 --- a/infrahub_sdk/exceptions.py +++ b/infrahub_sdk/exceptions.py @@ -17,6 +17,8 @@ def __init__(self, message: str | None = None, content: str | None = None, url: self.url = url if not self.message and self.url: self.message = f"Unable to decode response as JSON data from {self.url}" + if self.content: + self.message += f". Server response: {self.content}" super().__init__(self.message) diff --git a/tests/unit/sdk/test_utils.py b/tests/unit/sdk/test_utils.py index 99ef7e29..bc56bf98 100644 --- a/tests/unit/sdk/test_utils.py +++ b/tests/unit/sdk/test_utils.py @@ -1,11 +1,14 @@ +import json import tempfile import uuid from pathlib import Path +from unittest.mock import Mock import pytest from graphql import parse from whenever import Instant +from infrahub_sdk.exceptions import JsonDecodeError from infrahub_sdk.utils import ( base16decode, base16encode, @@ -13,6 +16,7 @@ base36encode, calculate_time_diff, compare_lists, + decode_json, deep_merge_dict, dict_hash, duplicates, @@ -227,3 +231,53 @@ def test_calculate_time_diff() -> None: time5 = Instant.now().subtract(hours=77, minutes=12, seconds=34).format_common_iso() assert calculate_time_diff(time5) == "3d and 5h ago" + + +def test_decode_json_success() -> None: + """Test decode_json with valid JSON response.""" + mock_response = Mock() + mock_response.json.return_value = {"status": "ok", "data": {"key": "value"}} + + result = decode_json(mock_response) + assert result == {"status": "ok", "data": {"key": "value"}} + + +def test_decode_json_failure_with_content() -> None: + """Test decode_json with invalid JSON response includes server content in error message.""" + mock_response = Mock() + mock_response.json.side_effect = json.decoder.JSONDecodeError("Invalid JSON", "document", 0) + mock_response.text = "Internal Server Error: Database connection failed" + mock_response.url = "https://example.com/api/graphql" + + with pytest.raises(JsonDecodeError) as exc_info: + decode_json(mock_response) + + error_message = str(exc_info.value) + assert "Unable to decode response as JSON data from https://example.com/api/graphql" in error_message + assert "Server response: Internal Server Error: Database connection failed" in error_message + + +def test_decode_json_failure_without_content() -> None: + """Test decode_json with invalid JSON response and no content.""" + mock_response = Mock() + mock_response.json.side_effect = json.decoder.JSONDecodeError("Invalid JSON", "document", 0) + mock_response.text = "" + mock_response.url = "https://example.com/api/graphql" + + with pytest.raises(JsonDecodeError) as exc_info: + decode_json(mock_response) + + error_message = str(exc_info.value) + assert "Unable to decode response as JSON data from https://example.com/api/graphql" in error_message + # Should not include server response part when content is empty + assert "Server response:" not in error_message + + +def test_json_decode_error_custom_message() -> None: + """Test JsonDecodeError with custom message does not override custom message.""" + custom_message = "Custom error message" + error = JsonDecodeError(message=custom_message, content="server error", url="https://example.com") + + assert str(error) == custom_message + assert error.content == "server error" + assert error.url == "https://example.com"