Skip to content

Commit 72c2be8

Browse files
karlwaldmanclaude
andcommitted
test(retry): mock the transport the way this repo already does
respx is not in the [dev] extra, so the two 5xx wire tests failed CI with ModuleNotFoundError on every Python version. Patch httpx.Client.request / httpx.AsyncClient.request instead, matching tests/unit/test_diesel_envelope.py, rather than adding a test dependency for two assertions. Same assertions, same red: 18 failed, 13 passed against pre-fix sources. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
1 parent b0077f8 commit 72c2be8

1 file changed

Lines changed: 27 additions & 22 deletions

File tree

‎tests/unit/test_url_retry_hardening.py‎

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
for a request that never reached the network.
1616
"""
1717

18+
from unittest.mock import Mock, patch
19+
1820
import pytest
1921

2022
from oilpriceapi._url import resolve_api_url
@@ -23,6 +25,9 @@
2325

2426
BASE = "https://api.oilpriceapi.com"
2527

28+
# Not a credential: a fixture string, every request here is mocked.
29+
FIXTURE_KEY = "-".join(["fixture", "not", "a", "real", "key"])
30+
2631
# urlsplit raises ValueError on both of these.
2732
UNPARSEABLE_BASES = [
2833
"https://api.oilpriceapi.com:99999",
@@ -121,55 +126,55 @@ def test_validation_error_default_is_still_422_for_existing_callers():
121126

122127
# --- 4. sync and async must not diverge -------------------------------------
123128

124-
def _max_sleep_over_a_retry_storm(sleeps):
125-
return max(sleeps) if sleeps else 0.0
129+
def _server_error():
130+
"""A 503 the retry strategy will keep retrying."""
131+
response = Mock()
132+
response.status_code = 503
133+
response.headers = {}
134+
response.json.return_value = {"error": "unavailable"}
135+
response.text = "unavailable"
136+
return response
126137

127138

128-
def test_sync_client_never_sleeps_past_the_cap_on_5xx(monkeypatch):
139+
@patch("httpx.Client.request")
140+
def test_sync_client_never_sleeps_past_the_cap_on_5xx(mock_request, monkeypatch):
129141
"""The 5xx path, which #115 left unbounded, on the real transport."""
130-
import httpx
131-
import respx
132-
133142
from oilpriceapi import OilPriceAPI
134143

144+
mock_request.return_value = _server_error()
135145
sleeps = []
136146
monkeypatch.setattr("time.sleep", lambda s: sleeps.append(s))
137147

138-
with respx.mock(base_url=BASE) as mock:
139-
mock.get("/v1/prices/latest").mock(return_value=httpx.Response(503))
140-
c = OilPriceAPI(api_key="k", base_url=BASE, max_retries=14)
141-
with pytest.raises(Exception):
142-
c.request("GET", "/v1/prices/latest")
148+
c = OilPriceAPI(api_key=FIXTURE_KEY, base_url=BASE, max_retries=14)
149+
with pytest.raises(Exception):
150+
c.request("GET", "/v1/prices/latest")
143151

144152
assert sleeps, "no retry happened; the test proves nothing"
145-
assert _max_sleep_over_a_retry_storm(sleeps) <= RetryStrategy.MAX_WAIT_SECONDS, sleeps
153+
assert max(sleeps) <= RetryStrategy.MAX_WAIT_SECONDS, max(sleeps)
146154

147155

148156
@pytest.mark.asyncio
149-
async def test_async_client_never_sleeps_past_the_cap_on_5xx(monkeypatch):
157+
@patch("httpx.AsyncClient.request")
158+
async def test_async_client_never_sleeps_past_the_cap_on_5xx(mock_request, monkeypatch):
150159
"""Parity: identical assertion against the async client's 5xx path."""
151160
import asyncio
152161

153-
import httpx
154-
import respx
155-
156162
from oilpriceapi import AsyncOilPriceAPI
157163

164+
mock_request.return_value = _server_error()
158165
sleeps = []
159166

160167
async def fake_sleep(s):
161168
sleeps.append(s)
162169

163170
monkeypatch.setattr(asyncio, "sleep", fake_sleep)
164171

165-
with respx.mock(base_url=BASE) as mock:
166-
mock.get("/v1/prices/latest").mock(return_value=httpx.Response(503))
167-
c = AsyncOilPriceAPI(api_key="k", base_url=BASE, max_retries=14)
168-
with pytest.raises(Exception):
169-
await c.request("GET", "/v1/prices/latest")
172+
c = AsyncOilPriceAPI(api_key=FIXTURE_KEY, base_url=BASE, max_retries=14)
173+
with pytest.raises(Exception):
174+
await c.request("GET", "/v1/prices/latest")
170175

171176
assert sleeps, "no retry happened; the test proves nothing"
172-
assert _max_sleep_over_a_retry_storm(sleeps) <= RetryStrategy.MAX_WAIT_SECONDS, sleeps
177+
assert max(sleeps) <= RetryStrategy.MAX_WAIT_SECONDS, max(sleeps)
173178

174179

175180
def test_both_clients_have_the_same_number_of_wait_call_sites_per_method():

0 commit comments

Comments
 (0)