Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/473.fixed.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion changelog/535.fixed.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Fix branch handling in `_run_transform` and `execute_graphql_query` functions in Infrahubctl to use environment variables for branch management.
Fix branch handling in `_run_transform` and `execute_graphql_query` functions in Infrahubctl to use environment variables for branch management.
1 change: 1 addition & 0 deletions changelog/549.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Allow the ability to clear optional attributes by setting them to None if they have been mutated by the user.
2 changes: 2 additions & 0 deletions infrahub_sdk/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
2 changes: 2 additions & 0 deletions infrahub_sdk/node/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ def _generate_input_data(self) -> dict | None:
variables: dict[str, Any] = {}

if self.value is None:
if self._schema.optional and self.value_has_been_mutated:
data["value"] = None
return data

if isinstance(self.value, str):
Expand Down
42 changes: 42 additions & 0 deletions tests/unit/sdk/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,48 @@ async def location_schema() -> NodeSchemaAPI:
return NodeSchema(**data).convert_api() # type: ignore


@pytest.fixture
async def location_schema_with_dropdown() -> NodeSchemaAPI:
data = {
"name": "Location",
"namespace": "Builtin",
"default_filter": "name__value",
"attributes": [
{"name": "name", "kind": "String", "unique": True},
{"name": "description", "kind": "String", "optional": True},
{"name": "type", "kind": "String"},
{
"name": "status",
"kind": "Dropdown",
"optional": True,
"choices": [{"name": "active", "label": "Active"}, {"name": "planning", "label": "Planning"}],
},
],
"relationships": [
{
"name": "tags",
"peer": "BuiltinTag",
"optional": True,
"cardinality": "many",
},
{
"name": "primary_tag",
"peer": "BuiltinTag",
"optional": True,
"cardinality": "one",
},
{
"name": "member_of_groups",
"peer": "CoreGroup",
"optional": True,
"cardinality": "many",
"kind": "Group",
},
],
}
return NodeSchema(**data).convert_api() # type: ignore


@pytest.fixture
async def schema_with_hfid() -> dict[str, NodeSchemaAPI]:
data = {
Expand Down
28 changes: 28 additions & 0 deletions tests/unit/sdk/test_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -1370,6 +1370,34 @@ async def test_create_input_data(client, location_schema: NodeSchemaAPI, client_
}


@pytest.mark.parametrize("client_type", client_types)
async def test_create_input_data_with_dropdown(client, location_schema_with_dropdown, client_type) -> None:
"""Validate input data including dropdown field"""
data = {
"name": {"value": "JFK1"},
"description": {"value": "JFK Airport"},
"type": {"value": "SITE"},
"status": {"value": "active"},
}

if client_type == "standard":
node = InfrahubNode(client=client, schema=location_schema_with_dropdown, data=data)
else:
node = InfrahubNodeSync(client=client, schema=location_schema_with_dropdown, data=data)

assert node.status.value == "active"
node.status = None
assert node._generate_input_data()["data"] == {
"data": {
"name": {"value": "JFK1"},
"description": {"value": "JFK Airport"},
"type": {"value": "SITE"},
"status": {"value": None},
"primary_tag": None,
}
}


@pytest.mark.parametrize("client_type", client_types)
async def test_create_input_data__with_relationships_02(client, location_schema, client_type) -> None:
"""Validate input data with variables that needs replacements"""
Expand Down
54 changes: 54 additions & 0 deletions tests/unit/sdk/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
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,
base36decode,
base36encode,
calculate_time_diff,
compare_lists,
decode_json,
deep_merge_dict,
dict_hash,
duplicates,
Expand Down Expand Up @@ -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"