from __future__ import annotations import httpx import pytest from iti.service_client import ( ServiceClient, ServiceConfig, ServiceHTTPError, ServiceUnavailableError, ) from iti.service_client.config import CircuitBreakerConfig, RetryConfig def _json_response(status_code: int, payload: dict) -> httpx.Response: return httpx.Response(status_code, json=payload) def test_service_client_sends_json_headers_token_trace_and_path(): seen: dict[str, object] = {} def handler(request: httpx.Request) -> httpx.Response: seen["url"] = str(request.url) seen["authorization"] = request.headers.get("Authorization") seen["trace_id"] = request.headers.get("X-Trace-Id") return _json_response(200, {"ok": True}) client = ServiceClient( ServiceConfig(name="erp", base_url="http://erp.local", token="token-a"), transport=httpx.MockTransport(handler), ) result = client.get("/users/{id}", path={"id": 12}, params={"active": "1"}) assert result == {"ok": True} assert seen["url"] == "http://erp.local/users/12?active=1" assert seen["authorization"] == "Bearer token-a" assert isinstance(seen["trace_id"], str) assert seen["trace_id"] def test_service_client_retries_idempotent_statuses(): calls = {"count": 0} def handler(request: httpx.Request) -> httpx.Response: calls["count"] += 1 if calls["count"] == 1: return _json_response(503, {"error": "busy"}) return _json_response(200, {"ok": True}) client = ServiceClient( ServiceConfig( name="erp", base_url="http://erp.local", retry=RetryConfig(attempts=2, backoff=0), ), transport=httpx.MockTransport(handler), ) assert client.get("/health") == {"ok": True} assert calls["count"] == 2 def test_service_client_does_not_retry_post_by_default(): calls = {"count": 0} def handler(request: httpx.Request) -> httpx.Response: calls["count"] += 1 return _json_response(503, {"error": "busy"}) client = ServiceClient( ServiceConfig( name="erp", base_url="http://erp.local", retry=RetryConfig(attempts=3, backoff=0), ), transport=httpx.MockTransport(handler), ) with pytest.raises(ServiceHTTPError) as exc_info: client.post("/jobs", json={"kind": "users"}) assert exc_info.value.status_code == 503 assert calls["count"] == 1 def test_service_client_opens_circuit_breaker_after_failures(): def handler(request: httpx.Request) -> httpx.Response: return _json_response(500, {"error": "failed"}) client = ServiceClient( ServiceConfig( name="erp", base_url="http://erp.local", circuit_breaker=CircuitBreakerConfig( enabled=True, fail_max=1, reset_timeout=30, ), ), transport=httpx.MockTransport(handler), ) with pytest.raises(ServiceHTTPError): client.get("/health") with pytest.raises(ServiceUnavailableError, match="circuit breaker is open"): client.get("/health")