Skip to content

Commit 4d01147

Browse files
committed
test(retry): add sync and async retry unit test suites
1 parent 5815d12 commit 4d01147

2 files changed

Lines changed: 310 additions & 0 deletions

File tree

tests/test_async_retry.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import unittest
2+
from unittest.mock import AsyncMock, MagicMock, patch
3+
4+
import httpx
5+
6+
from openapi_python_sdk.client import AsyncClient, AsyncOauthClient
7+
8+
9+
class TestAsyncRetry(unittest.IsolatedAsyncioTestCase):
10+
11+
@patch("openapi_python_sdk.client.httpx.AsyncClient")
12+
@patch("asyncio.sleep")
13+
async def test_default_no_retries(self, mock_sleep, mock_httpx):
14+
"""Verify that max_retries defaults to 0 and does not retry on network error."""
15+
# Setup mock to raise a request error
16+
mock_httpx.return_value.request = AsyncMock(side_effect=httpx.RequestError("Network error"))
17+
mock_httpx.return_value.aclose = AsyncMock()
18+
19+
async with AsyncClient(token="test_token") as client:
20+
self.assertEqual(client.max_retries, 0)
21+
with self.assertRaises(httpx.RequestError):
22+
await client.request(method="GET", url="https://test.example.com")
23+
24+
# Request should only be called once
25+
self.assertEqual(mock_httpx.return_value.request.call_count, 1)
26+
mock_sleep.assert_not_called()
27+
28+
@patch("openapi_python_sdk.client.httpx.AsyncClient")
29+
@patch("asyncio.sleep")
30+
async def test_successful_request_no_retries(self, mock_sleep, mock_httpx):
31+
"""Verify that a successful request is only called once even if max_retries > 0."""
32+
mock_resp = MagicMock()
33+
mock_resp.status_code = 200
34+
mock_resp.json.return_value = {"status": "ok"}
35+
mock_httpx.return_value.request = AsyncMock(return_value=mock_resp)
36+
mock_httpx.return_value.aclose = AsyncMock()
37+
38+
async with AsyncClient(token="test_token", max_retries=3, backoff_factor=0.001) as client:
39+
resp = await client.request(method="GET", url="https://test.example.com")
40+
41+
self.assertEqual(resp, {"status": "ok"})
42+
self.assertEqual(mock_httpx.return_value.request.call_count, 1)
43+
mock_sleep.assert_not_called()
44+
45+
@patch("openapi_python_sdk.client.httpx.AsyncClient")
46+
@patch("asyncio.sleep")
47+
async def test_retry_on_network_error_then_success(self, mock_sleep, mock_httpx):
48+
"""Verify client retries on network error and succeeds on subsequent try."""
49+
mock_resp = MagicMock()
50+
mock_resp.status_code = 200
51+
mock_resp.json.return_value = {"status": "ok"}
52+
mock_httpx.return_value.aclose = AsyncMock()
53+
54+
# First two requests fail, third succeeds
55+
mock_httpx.return_value.request = AsyncMock(side_effect=[
56+
httpx.RequestError("Error 1"),
57+
httpx.RequestError("Error 2"),
58+
mock_resp,
59+
])
60+
61+
async with AsyncClient(token="test_token", max_retries=3, backoff_factor=0.1) as client:
62+
resp = await client.request(method="GET", url="https://test.example.com")
63+
64+
self.assertEqual(resp, {"status": "ok"})
65+
self.assertEqual(mock_httpx.return_value.request.call_count, 3)
66+
self.assertEqual(mock_sleep.call_count, 2)
67+
68+
@patch("openapi_python_sdk.client.httpx.AsyncClient")
69+
@patch("asyncio.sleep")
70+
async def test_retry_limit_reached(self, mock_sleep, mock_httpx):
71+
"""Verify that client fails after reaching max_retries limit."""
72+
mock_httpx.return_value.request = AsyncMock(side_effect=httpx.RequestError("Error"))
73+
mock_httpx.return_value.aclose = AsyncMock()
74+
75+
async with AsyncClient(token="test_token", max_retries=2, backoff_factor=0.1) as client:
76+
with self.assertRaises(httpx.RequestError):
77+
await client.request(method="GET", url="https://test.example.com")
78+
79+
# Initial call + 2 retries = 3 calls
80+
self.assertEqual(mock_httpx.return_value.request.call_count, 3)
81+
self.assertEqual(mock_sleep.call_count, 2)
82+
83+
@patch("openapi_python_sdk.client.httpx.AsyncClient")
84+
@patch("asyncio.sleep")
85+
async def test_retry_on_status_codes(self, mock_sleep, mock_httpx):
86+
"""Verify client retries on configured retry status codes (e.g. 503)."""
87+
mock_resp_fail = MagicMock()
88+
mock_resp_fail.status_code = 503
89+
90+
mock_resp_success = MagicMock()
91+
mock_resp_success.status_code = 200
92+
mock_resp_success.json.return_value = {"status": "ok"}
93+
mock_httpx.return_value.aclose = AsyncMock()
94+
95+
mock_httpx.return_value.request = AsyncMock(side_effect=[
96+
mock_resp_fail,
97+
mock_resp_success,
98+
])
99+
100+
async with AsyncClient(token="test_token", max_retries=3, backoff_factor=0.1) as client:
101+
resp = await client.request(method="GET", url="https://test.example.com")
102+
103+
self.assertEqual(resp, {"status": "ok"})
104+
self.assertEqual(mock_httpx.return_value.request.call_count, 2)
105+
mock_sleep.assert_called_once()
106+
107+
@patch("openapi_python_sdk.client.httpx.AsyncClient")
108+
@patch("asyncio.sleep")
109+
async def test_respects_retry_after_header(self, mock_sleep, mock_httpx):
110+
"""Verify client respects Retry-After header on 429 Too Many Requests."""
111+
mock_resp_429 = MagicMock()
112+
mock_resp_429.status_code = 429
113+
mock_resp_429.headers = {"Retry-After": "5"}
114+
115+
mock_resp_success = MagicMock()
116+
mock_resp_success.status_code = 200
117+
mock_resp_success.json.return_value = {"status": "ok"}
118+
mock_httpx.return_value.aclose = AsyncMock()
119+
120+
mock_httpx.return_value.request = AsyncMock(side_effect=[
121+
mock_resp_429,
122+
mock_resp_success,
123+
])
124+
125+
async with AsyncClient(token="test_token", max_retries=3, backoff_factor=1.0) as client:
126+
resp = await client.request(method="GET", url="https://test.example.com")
127+
128+
self.assertEqual(resp, {"status": "ok"})
129+
# Should sleep for exactly 5.0 seconds as specified by Retry-After
130+
mock_sleep.assert_called_once_with(5.0)
131+
132+
@patch("openapi_python_sdk.client.httpx.AsyncClient")
133+
@patch("asyncio.sleep")
134+
async def test_oauth_client_retry(self, mock_sleep, mock_httpx):
135+
"""Verify that AsyncOauthClient also supports retries."""
136+
mock_resp_fail = MagicMock()
137+
mock_resp_fail.status_code = 502
138+
139+
mock_resp_success = MagicMock()
140+
mock_resp_success.status_code = 200
141+
mock_resp_success.json.return_value = {"token": "abc123"}
142+
mock_httpx.return_value.aclose = AsyncMock()
143+
144+
mock_httpx.return_value.post = AsyncMock(side_effect=[
145+
mock_resp_fail,
146+
mock_resp_success,
147+
])
148+
149+
async with AsyncOauthClient(username="user", apikey="key", max_retries=2, backoff_factor=0.1) as oauth:
150+
resp = await oauth.create_token(scopes=["test"])
151+
152+
self.assertEqual(resp["token"], "abc123")
153+
self.assertEqual(mock_httpx.return_value.post.call_count, 2)
154+
mock_sleep.assert_called_once()
155+
156+
157+
if __name__ == "__main__":
158+
unittest.main()

tests/test_retry.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import unittest
2+
from unittest.mock import MagicMock, patch
3+
4+
import httpx
5+
6+
from openapi_python_sdk.client import Client, OauthClient
7+
8+
9+
class TestSyncRetry(unittest.TestCase):
10+
11+
@patch("openapi_python_sdk.client.httpx.Client")
12+
@patch("time.sleep")
13+
def test_default_no_retries(self, mock_sleep, mock_httpx):
14+
"""Verify that max_retries defaults to 0 and does not retry on network error."""
15+
# Setup mock to raise a request error
16+
mock_httpx.return_value.request.side_effect = httpx.RequestError("Network error")
17+
18+
client = Client(token="test_token")
19+
self.assertEqual(client.max_retries, 0)
20+
21+
with self.assertRaises(httpx.RequestError):
22+
client.request(method="GET", url="https://test.example.com")
23+
24+
# Request should only be called once
25+
self.assertEqual(mock_httpx.return_value.request.call_count, 1)
26+
mock_sleep.assert_not_called()
27+
28+
@patch("openapi_python_sdk.client.httpx.Client")
29+
@patch("time.sleep")
30+
def test_successful_request_no_retries(self, mock_sleep, mock_httpx):
31+
"""Verify that a successful request is only called once even if max_retries > 0."""
32+
mock_resp = MagicMock()
33+
mock_resp.status_code = 200
34+
mock_resp.json.return_value = {"status": "ok"}
35+
mock_httpx.return_value.request.return_value = mock_resp
36+
37+
client = Client(token="test_token", max_retries=3, backoff_factor=0.001)
38+
resp = client.request(method="GET", url="https://test.example.com")
39+
40+
self.assertEqual(resp, {"status": "ok"})
41+
self.assertEqual(mock_httpx.return_value.request.call_count, 1)
42+
mock_sleep.assert_not_called()
43+
44+
@patch("openapi_python_sdk.client.httpx.Client")
45+
@patch("time.sleep")
46+
def test_retry_on_network_error_then_success(self, mock_sleep, mock_httpx):
47+
"""Verify client retries on network error and succeeds on subsequent try."""
48+
mock_resp = MagicMock()
49+
mock_resp.status_code = 200
50+
mock_resp.json.return_value = {"status": "ok"}
51+
52+
# First two requests fail, third succeeds
53+
mock_httpx.return_value.request.side_effect = [
54+
httpx.RequestError("Error 1"),
55+
httpx.RequestError("Error 2"),
56+
mock_resp,
57+
]
58+
59+
client = Client(token="test_token", max_retries=3, backoff_factor=0.1)
60+
resp = client.request(method="GET", url="https://test.example.com")
61+
62+
self.assertEqual(resp, {"status": "ok"})
63+
self.assertEqual(mock_httpx.return_value.request.call_count, 3)
64+
self.assertEqual(mock_sleep.call_count, 2)
65+
66+
@patch("openapi_python_sdk.client.httpx.Client")
67+
@patch("time.sleep")
68+
def test_retry_limit_reached(self, mock_sleep, mock_httpx):
69+
"""Verify that client fails after reaching max_retries limit."""
70+
mock_httpx.return_value.request.side_effect = httpx.RequestError("Error")
71+
72+
client = Client(token="test_token", max_retries=2, backoff_factor=0.1)
73+
with self.assertRaises(httpx.RequestError):
74+
client.request(method="GET", url="https://test.example.com")
75+
76+
# Initial call + 2 retries = 3 calls
77+
self.assertEqual(mock_httpx.return_value.request.call_count, 3)
78+
self.assertEqual(mock_sleep.call_count, 2)
79+
80+
@patch("openapi_python_sdk.client.httpx.Client")
81+
@patch("time.sleep")
82+
def test_retry_on_status_codes(self, mock_sleep, mock_httpx):
83+
"""Verify client retries on configured retry status codes (e.g. 503)."""
84+
mock_resp_fail = MagicMock()
85+
mock_resp_fail.status_code = 503
86+
87+
mock_resp_success = MagicMock()
88+
mock_resp_success.status_code = 200
89+
mock_resp_success.json.return_value = {"status": "ok"}
90+
91+
mock_httpx.return_value.request.side_effect = [
92+
mock_resp_fail,
93+
mock_resp_success,
94+
]
95+
96+
client = Client(token="test_token", max_retries=3, backoff_factor=0.1)
97+
resp = client.request(method="GET", url="https://test.example.com")
98+
99+
self.assertEqual(resp, {"status": "ok"})
100+
self.assertEqual(mock_httpx.return_value.request.call_count, 2)
101+
mock_sleep.assert_called_once()
102+
103+
@patch("openapi_python_sdk.client.httpx.Client")
104+
@patch("time.sleep")
105+
def test_respects_retry_after_header(self, mock_sleep, mock_httpx):
106+
"""Verify client respects Retry-After header on 429 Too Many Requests."""
107+
mock_resp_429 = MagicMock()
108+
mock_resp_429.status_code = 429
109+
mock_resp_429.headers = {"Retry-After": "5"}
110+
111+
mock_resp_success = MagicMock()
112+
mock_resp_success.status_code = 200
113+
mock_resp_success.json.return_value = {"status": "ok"}
114+
115+
mock_httpx.return_value.request.side_effect = [
116+
mock_resp_429,
117+
mock_resp_success,
118+
]
119+
120+
client = Client(token="test_token", max_retries=3, backoff_factor=1.0)
121+
resp = client.request(method="GET", url="https://test.example.com")
122+
123+
self.assertEqual(resp, {"status": "ok"})
124+
# Should sleep for exactly 5.0 seconds as specified by Retry-After
125+
mock_sleep.assert_called_once_with(5.0)
126+
127+
@patch("openapi_python_sdk.client.httpx.Client")
128+
@patch("time.sleep")
129+
def test_oauth_client_retry(self, mock_sleep, mock_httpx):
130+
"""Verify that OauthClient also supports retries."""
131+
mock_resp_fail = MagicMock()
132+
mock_resp_fail.status_code = 502
133+
134+
mock_resp_success = MagicMock()
135+
mock_resp_success.status_code = 200
136+
mock_resp_success.json.return_value = {"token": "abc123"}
137+
138+
mock_httpx.return_value.post.side_effect = [
139+
mock_resp_fail,
140+
mock_resp_success,
141+
]
142+
143+
oauth = OauthClient(username="user", apikey="key", max_retries=2, backoff_factor=0.1)
144+
resp = oauth.create_token(scopes=["test"])
145+
146+
self.assertEqual(resp["token"], "abc123")
147+
self.assertEqual(mock_httpx.return_value.post.call_count, 2)
148+
mock_sleep.assert_called_once()
149+
150+
151+
if __name__ == "__main__":
152+
unittest.main()

0 commit comments

Comments
 (0)