@@ -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