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
10 changes: 5 additions & 5 deletions src/drukbox_sdk/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -626,11 +626,11 @@ async def _request(
raise SandboxUnavailableError(json_response.get("detail", "service unavailable"))

if response.status_code == 422:
# FastAPI's 422 detail is a list of error dicts; join the messages
# so it reads as a line, not a raw list repr.
errors = json_response.get("detail") or []
detail = "; ".join(error.get("msg", "") for error in errors) or "validation error"
raise SandboxValidationError(detail)
# The detail is FastAPI's list of error dicts or the service's own sentence.
detail = json_response.get("detail")
if isinstance(detail, list):
detail = "; ".join(error.get("msg", "") for error in detail)
raise SandboxValidationError(detail or "validation error")

if response.status_code >= 400:
raise SandboxResponseError(json_response.get("detail", "error"))
Expand Down
14 changes: 14 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,20 @@ async def test_422_raises_sandbox_validation_error(api: SandboxAPI):
await api.create_host(env={"FOO BAR": "x"})


@respx.mock
async def test_422_string_detail_raises_sandbox_validation_error(api: SandboxAPI):
# Drukbox answers a refused secret with the issuer's reason as a plain string.
respx.post(f"{BASE_URL}/hosts").mock(
return_value=httpx.Response(
422,
json={"detail": "GitHub App not installed for acme", "error_code": "SECRET_REFUSED"},
),
)

with pytest.raises(SandboxValidationError, match=r"^GitHub App not installed for acme$"):
await api.create_host()


@respx.mock
async def test_500_raises_sandbox_response_error(api: SandboxAPI):
respx.get(f"{BASE_URL}/hosts").mock(
Expand Down
Loading