1+ from collections .abc import AsyncGenerator
2+
13import httpx
4+ import pytest
5+ import respx
6+ import tenacity
7+
8+ from bubble_data_api_client import configure , http_client
9+ from bubble_data_api_client .pool import close_clients
10+ from bubble_data_api_client .transport import Transport
11+
212
3- from bubble_data_api_client import http_client
13+ @pytest .fixture
14+ async def clean_client_pool () -> AsyncGenerator [None ]:
15+ """Ensure client pool is clean before and after each test."""
16+ await close_clients ()
17+ yield
18+ await close_clients ()
419
520
621def test_httpx_client_factory (test_url : str , test_api_key : str ) -> None :
@@ -13,3 +28,72 @@ def test_httpx_client_factory(test_url: str, test_api_key: str) -> None:
1328 assert client .base_url == test_url
1429 assert client .headers ["Authorization" ] == f"Bearer { test_api_key } "
1530 assert client .headers ["User-Agent" ] == http_client .DEFAULT_USER_AGENT
31+
32+
33+ @respx .mock
34+ async def test_transport_no_retry_fails_immediately (clean_client_pool : None ) -> None :
35+ """Test that request fails immediately when no retry is configured."""
36+ configure (
37+ data_api_root_url = "https://test.example.com" ,
38+ api_key = "test-key" ,
39+ retry = None ,
40+ )
41+
42+ route = respx .get ("https://test.example.com/test" ).mock (return_value = httpx .Response (500 ))
43+
44+ async with Transport () as transport :
45+ with pytest .raises (httpx .HTTPStatusError ) as exc_info :
46+ await transport .get ("/test" )
47+ assert exc_info .value .response .status_code == 500
48+
49+ assert route .call_count == 1
50+
51+
52+ @respx .mock
53+ async def test_transport_retry_succeeds_after_failures (clean_client_pool : None ) -> None :
54+ """Test that request retries and succeeds after transient failures."""
55+ configure (
56+ data_api_root_url = "https://test.example.com" ,
57+ api_key = "test-key" ,
58+ retry = tenacity .AsyncRetrying (
59+ stop = tenacity .stop_after_attempt (3 ),
60+ wait = tenacity .wait_none (),
61+ retry = tenacity .retry_if_exception_type (httpx .HTTPStatusError ),
62+ ),
63+ )
64+
65+ route = respx .get ("https://test.example.com/test" ).mock (
66+ side_effect = [
67+ httpx .Response (500 ),
68+ httpx .Response (500 ),
69+ httpx .Response (200 , json = {"result" : "ok" }),
70+ ]
71+ )
72+
73+ async with Transport () as transport :
74+ response = await transport .get ("/test" )
75+ assert response .status_code == 200
76+
77+ assert route .call_count == 3
78+
79+
80+ @respx .mock
81+ async def test_transport_retry_exhausted (clean_client_pool : None ) -> None :
82+ """Test that RetryError is raised when all retry attempts fail."""
83+ configure (
84+ data_api_root_url = "https://test.example.com" ,
85+ api_key = "test-key" ,
86+ retry = tenacity .AsyncRetrying (
87+ stop = tenacity .stop_after_attempt (3 ),
88+ wait = tenacity .wait_none (),
89+ retry = tenacity .retry_if_exception_type (httpx .HTTPStatusError ),
90+ ),
91+ )
92+
93+ route = respx .get ("https://test.example.com/test" ).mock (return_value = httpx .Response (500 ))
94+
95+ async with Transport () as transport :
96+ with pytest .raises (tenacity .RetryError ):
97+ await transport .get ("/test" )
98+
99+ assert route .call_count == 3
0 commit comments