You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
109 lines
3.1 KiB
Python
109 lines
3.1 KiB
Python
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")
|