|
15 | 15 | for a request that never reached the network. |
16 | 16 | """ |
17 | 17 |
|
| 18 | +from unittest.mock import Mock, patch |
| 19 | + |
18 | 20 | import pytest |
19 | 21 |
|
20 | 22 | from oilpriceapi._url import resolve_api_url |
|
23 | 25 |
|
24 | 26 | BASE = "https://api.oilpriceapi.com" |
25 | 27 |
|
| 28 | +# Not a credential: a fixture string, every request here is mocked. |
| 29 | +FIXTURE_KEY = "-".join(["fixture", "not", "a", "real", "key"]) |
| 30 | + |
26 | 31 | # urlsplit raises ValueError on both of these. |
27 | 32 | UNPARSEABLE_BASES = [ |
28 | 33 | "https://api.oilpriceapi.com:99999", |
@@ -121,55 +126,55 @@ def test_validation_error_default_is_still_422_for_existing_callers(): |
121 | 126 |
|
122 | 127 | # --- 4. sync and async must not diverge ------------------------------------- |
123 | 128 |
|
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 |
126 | 137 |
|
127 | 138 |
|
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): |
129 | 141 | """The 5xx path, which #115 left unbounded, on the real transport.""" |
130 | | - import httpx |
131 | | - import respx |
132 | | - |
133 | 142 | from oilpriceapi import OilPriceAPI |
134 | 143 |
|
| 144 | + mock_request.return_value = _server_error() |
135 | 145 | sleeps = [] |
136 | 146 | monkeypatch.setattr("time.sleep", lambda s: sleeps.append(s)) |
137 | 147 |
|
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") |
143 | 151 |
|
144 | 152 | 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) |
146 | 154 |
|
147 | 155 |
|
148 | 156 | @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): |
150 | 159 | """Parity: identical assertion against the async client's 5xx path.""" |
151 | 160 | import asyncio |
152 | 161 |
|
153 | | - import httpx |
154 | | - import respx |
155 | | - |
156 | 162 | from oilpriceapi import AsyncOilPriceAPI |
157 | 163 |
|
| 164 | + mock_request.return_value = _server_error() |
158 | 165 | sleeps = [] |
159 | 166 |
|
160 | 167 | async def fake_sleep(s): |
161 | 168 | sleeps.append(s) |
162 | 169 |
|
163 | 170 | monkeypatch.setattr(asyncio, "sleep", fake_sleep) |
164 | 171 |
|
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") |
170 | 175 |
|
171 | 176 | 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) |
173 | 178 |
|
174 | 179 |
|
175 | 180 | def test_both_clients_have_the_same_number_of_wait_call_sites_per_method(): |
|
0 commit comments