Skip to content

Commit c4e8efe

Browse files
tobixenclaude
andcommitted
Update tests for new architecture, features, and compatibility fixes
- test_async_davclient.py: add unit tests for async rate-limit handling (429/503 Retry-After, max_sleep, default_sleep, async retry loop) - test_async_integration.py: expand async integration test coverage - test_caldav.py: update integration tests to match new architecture; remove assertions now handled by library-level compatibility workarounds - test_caldav_unit.py: add tests for rate-limit helpers; is-not-defined undef-filter with DTEND and recurring events - test_compatibility_hints.py: add tests for parent-feature default override fix; is-not-defined.category workaround; duplicate cert logic - test_search.py: add unit tests for is-not-defined DTEND filter with recurrences; category workaround - test_lazy_import.py: new — verify lazy import behaviour of caldav package - test_sync_token_fallback.py: minor additions - test_docs.py: minor update Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b8d54a1 commit c4e8efe

9 files changed

Lines changed: 1137 additions & 220 deletions

tests/test_async_davclient.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -819,3 +819,119 @@ def test_has_component_with_only_vcalendar(self) -> None:
819819
obj = AsyncCalendarObjectResource(client=None, data=data)
820820
# This should return False since there's no VEVENT/VTODO/VJOURNAL
821821
assert obj.has_component() is False
822+
823+
824+
class TestAsyncRateLimiting:
825+
"""
826+
Unit tests for 429/503 rate-limit handling in AsyncDAVClient.
827+
Mirrors TestRateLimiting in test_caldav_unit.py.
828+
No real server communication.
829+
"""
830+
831+
def _make_response(self, status_code, headers=None):
832+
resp = MagicMock()
833+
resp.status_code = status_code
834+
resp.headers = headers or {}
835+
resp.reason = "Too Many Requests" if status_code == 429 else "Service Unavailable"
836+
resp.reason_phrase = resp.reason
837+
return resp
838+
839+
@pytest.mark.asyncio
840+
async def test_429_no_retry_after_raises(self):
841+
client = AsyncDAVClient(url="http://cal.example.com/")
842+
client.session.request = AsyncMock(return_value=self._make_response(429))
843+
with pytest.raises(error.RateLimitError) as exc_info:
844+
await client.request("/")
845+
assert exc_info.value.retry_after is None
846+
assert exc_info.value.retry_after_seconds is None
847+
848+
@pytest.mark.asyncio
849+
async def test_429_with_integer_retry_after(self):
850+
client = AsyncDAVClient(url="http://cal.example.com/")
851+
client.session.request = AsyncMock(
852+
return_value=self._make_response(429, {"Retry-After": "30"})
853+
)
854+
with pytest.raises(error.RateLimitError) as exc_info:
855+
await client.request("/")
856+
assert exc_info.value.retry_after == "30"
857+
assert exc_info.value.retry_after_seconds == 30.0
858+
859+
@pytest.mark.asyncio
860+
async def test_503_without_retry_after_does_not_raise_rate_limit(self):
861+
client = AsyncDAVClient(url="http://cal.example.com/")
862+
client.session.request = AsyncMock(return_value=self._make_response(503))
863+
# Should not raise RateLimitError; falls through as a normal 503 response
864+
response = await client.request("/")
865+
assert response.status == 503
866+
867+
@pytest.mark.asyncio
868+
async def test_503_with_retry_after_raises(self):
869+
client = AsyncDAVClient(url="http://cal.example.com/")
870+
client.session.request = AsyncMock(
871+
return_value=self._make_response(503, {"Retry-After": "10"})
872+
)
873+
with pytest.raises(error.RateLimitError) as exc_info:
874+
await client.request("/")
875+
assert exc_info.value.retry_after_seconds == 10.0
876+
877+
@pytest.mark.asyncio
878+
async def test_rate_limit_handle_sleeps_and_retries(self):
879+
ok_response = self._make_response(200)
880+
client = AsyncDAVClient(url="http://cal.example.com/", rate_limit_handle=True)
881+
client.session.request = AsyncMock(
882+
side_effect=[
883+
self._make_response(429, {"Retry-After": "5"}),
884+
ok_response,
885+
]
886+
)
887+
with patch("caldav.async_davclient.asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
888+
response = await client.request("/")
889+
mock_sleep.assert_awaited_once_with(5.0)
890+
assert response.status == 200
891+
assert client.session.request.call_count == 2
892+
893+
@pytest.mark.asyncio
894+
async def test_rate_limit_handle_default_sleep_used_when_no_retry_after(self):
895+
ok_response = self._make_response(200)
896+
client = AsyncDAVClient(
897+
url="http://cal.example.com/", rate_limit_handle=True, rate_limit_default_sleep=3
898+
)
899+
client.session.request = AsyncMock(side_effect=[self._make_response(429), ok_response])
900+
with patch("caldav.async_davclient.asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
901+
response = await client.request("/")
902+
mock_sleep.assert_awaited_once_with(3.0)
903+
assert response.status == 200
904+
905+
@pytest.mark.asyncio
906+
async def test_rate_limit_handle_no_sleep_info_raises(self):
907+
client = AsyncDAVClient(url="http://cal.example.com/", rate_limit_handle=True)
908+
client.session.request = AsyncMock(return_value=self._make_response(429))
909+
with pytest.raises(error.RateLimitError):
910+
await client.request("/")
911+
912+
@pytest.mark.asyncio
913+
async def test_rate_limit_max_sleep_caps_sleep_time(self):
914+
ok_response = self._make_response(200)
915+
client = AsyncDAVClient(
916+
url="http://cal.example.com/", rate_limit_handle=True, rate_limit_max_sleep=60
917+
)
918+
client.session.request = AsyncMock(
919+
side_effect=[
920+
self._make_response(429, {"Retry-After": "3600"}),
921+
ok_response,
922+
]
923+
)
924+
with patch("caldav.async_davclient.asyncio.sleep", new_callable=AsyncMock) as mock_sleep:
925+
await client.request("/")
926+
mock_sleep.assert_awaited_once_with(60.0)
927+
928+
@pytest.mark.asyncio
929+
async def test_rate_limit_max_sleep_zero_raises(self):
930+
client = AsyncDAVClient(
931+
url="http://cal.example.com/", rate_limit_handle=True, rate_limit_max_sleep=0
932+
)
933+
client.session.request = AsyncMock(
934+
return_value=self._make_response(429, {"Retry-After": "30"})
935+
)
936+
with pytest.raises(error.RateLimitError):
937+
await client.request("/")

0 commit comments

Comments
 (0)