Skip to content

Commit fc884c8

Browse files
namedgraphclaude
andcommitted
Add unit tests for RetryAfterHandler
Covers numeric and HTTP-date Retry-After parsing, past-date clamping, retry count tracking, and HTTPError raise after max_retries exhausted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent fdd4011 commit fc884c8

1 file changed

Lines changed: 124 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""Tests for RetryAfterHandler — HTTP 429 retry logic (src/web_algebra/client.py).
2+
3+
Not in formal-semantics.md (client infrastructure, not an operation).
4+
Behaviour spec: on 429, sleep Retry-After seconds then retry; raise HTTPError
5+
after max_retries exhausted.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import urllib.error
11+
from datetime import datetime, timedelta, timezone
12+
from email.utils import format_datetime
13+
from unittest.mock import MagicMock, patch
14+
15+
import pytest
16+
17+
from web_algebra.client import RetryAfterHandler
18+
19+
20+
def _req(url: str = "http://example.org/resource") -> MagicMock:
21+
req = MagicMock()
22+
req.full_url = url
23+
return req
24+
25+
26+
class TestRetryAfterHandlerNumericDelay:
27+
def test_sleeps_numeric_retry_after(self):
28+
handler = RetryAfterHandler()
29+
handler.parent = MagicMock()
30+
handler.parent.open.return_value = "ok"
31+
32+
with patch("web_algebra.client.time.sleep") as mock_sleep:
33+
result = handler.http_error_429(
34+
_req(), None, 429, "Too Many Requests", {"Retry-After": "2"}
35+
)
36+
37+
mock_sleep.assert_called_once_with(2.0)
38+
assert result == "ok"
39+
40+
def test_defaults_to_1s_when_header_absent(self):
41+
handler = RetryAfterHandler()
42+
handler.parent = MagicMock()
43+
handler.parent.open.return_value = "ok"
44+
45+
with patch("web_algebra.client.time.sleep") as mock_sleep:
46+
handler.http_error_429(_req(), None, 429, "Too Many Requests", {})
47+
48+
mock_sleep.assert_called_once_with(1.0)
49+
50+
51+
class TestRetryAfterHandlerDateDelay:
52+
def test_sleeps_computed_seconds_for_future_http_date(self):
53+
handler = RetryAfterHandler()
54+
handler.parent = MagicMock()
55+
handler.parent.open.return_value = "ok"
56+
57+
future = datetime.now(tz=timezone.utc) + timedelta(seconds=5)
58+
hdrs = {"Retry-After": format_datetime(future)}
59+
60+
with patch("web_algebra.client.time.sleep") as mock_sleep:
61+
handler.http_error_429(_req(), None, 429, "Too Many Requests", hdrs)
62+
63+
delay = mock_sleep.call_args[0][0]
64+
assert 4.0 <= delay <= 6.0
65+
66+
def test_clamps_past_http_date_to_zero(self):
67+
handler = RetryAfterHandler()
68+
handler.parent = MagicMock()
69+
handler.parent.open.return_value = "ok"
70+
71+
past = datetime.now(tz=timezone.utc) - timedelta(seconds=10)
72+
hdrs = {"Retry-After": format_datetime(past)}
73+
74+
with patch("web_algebra.client.time.sleep") as mock_sleep:
75+
handler.http_error_429(_req(), None, 429, "Too Many Requests", hdrs)
76+
77+
mock_sleep.assert_called_once_with(0.0)
78+
79+
80+
class TestRetryAfterHandlerRetryLimit:
81+
def test_raises_http_error_when_max_retries_exhausted(self):
82+
handler = RetryAfterHandler(max_retries=2)
83+
handler.parent = MagicMock()
84+
req = _req()
85+
handler._retry_counts[req.full_url] = 2
86+
87+
with patch("web_algebra.client.time.sleep"):
88+
with pytest.raises(urllib.error.HTTPError):
89+
handler.http_error_429(req, None, 429, "Too Many Requests", {})
90+
91+
def test_clears_retry_count_after_exhaustion(self):
92+
handler = RetryAfterHandler(max_retries=1)
93+
handler.parent = MagicMock()
94+
req = _req()
95+
handler._retry_counts[req.full_url] = 1
96+
97+
with patch("web_algebra.client.time.sleep"):
98+
with pytest.raises(urllib.error.HTTPError):
99+
handler.http_error_429(req, None, 429, "Too Many Requests", {})
100+
101+
assert req.full_url not in handler._retry_counts
102+
103+
def test_increments_retry_count_on_each_attempt(self):
104+
handler = RetryAfterHandler(max_retries=3)
105+
handler.parent = MagicMock()
106+
handler.parent.open.return_value = "ok"
107+
req = _req()
108+
109+
with patch("web_algebra.client.time.sleep"):
110+
handler.http_error_429(req, None, 429, "Too Many Requests", {"Retry-After": "0"})
111+
112+
assert handler._retry_counts[req.full_url] == 1
113+
114+
def test_retries_up_to_but_not_exceeding_max_retries(self):
115+
handler = RetryAfterHandler(max_retries=2)
116+
handler.parent = MagicMock()
117+
handler.parent.open.return_value = "ok"
118+
req = _req()
119+
120+
with patch("web_algebra.client.time.sleep"):
121+
handler.http_error_429(req, None, 429, "Too Many Requests", {"Retry-After": "0"})
122+
handler.http_error_429(req, None, 429, "Too Many Requests", {"Retry-After": "0"})
123+
with pytest.raises(urllib.error.HTTPError):
124+
handler.http_error_429(req, None, 429, "Too Many Requests", {"Retry-After": "0"})

0 commit comments

Comments
 (0)