@@ -23,7 +23,7 @@ async def test_octopus_read_response_retry(my_predbat):
2323 - Test 4: All retries fail, returns None and increments failures_total
2424 - Test 5: Exponential backoff timing is correct
2525 - Test 6: ignore_errors parameter is passed through correctly
26- - Test 7: Rate limit errors trigger retry with backoff
26+ - Test 7: A rate limit is not re-read or slept on
2727 - Test 8: Auth errors trigger retry with backoff
2828 """
2929 print ("**** Running Octopus async_read_response_retry tests ****" )
@@ -207,38 +207,48 @@ def create_mock_response(status, text_content, headers=None):
207207 print ("PASS: ignore_errors parameter passed through correctly" )
208208
209209 # Test 7: Rate limit errors trigger retry with backoff
210- print ("\n *** Test 7: Rate limit errors trigger retry with backoff ***" )
210+ # Test 7: A rate limit (KT-CT-1199) is definitive for the response and is not re-read. aiohttp
211+ # caches the body, so every re-read returned the same rate limit: a single rate-limited request
212+ # logged the warning five times and stalled for ~35s (5s + 1/2/4/8s) before giving up anyway.
213+ print ("\n *** Test 7: Rate limit is not re-read or slept on ***" )
211214 api = OctopusAPI (my_predbat , key = "test-key" , account_id = "test-account" , automatic = False )
212215 api .failures_total = 0
216+ logged = []
217+ api .log = lambda message , * args , ** kwargs : logged .append (message )
213218
214219 rate_limit_error = {
215220 "errors" : [
216221 {
217222 "message" : "Too many requests." ,
223+ "path" : ["flexPlannedDispatches" ],
218224 "extensions" : {"errorCode" : "KT-CT-1199" },
219225 }
220226 ]
221227 }
222- successful_data = {"data" : {"result" : "success" }}
223228 response = create_mock_response (200 , json .dumps (rate_limit_error ))
224-
225- # Mock async_read_response to return None (rate limit), then succeed
226- api .async_read_response = AsyncMock (side_effect = [None , successful_data ])
229+ graphql_url = "https://api.octopus.energy/v1/graphql/"
227230
228231 with patch ("asyncio.sleep" , new_callable = AsyncMock ) as mock_sleep :
229- result = await api .async_read_response_retry (response , url , ignore_errors = False )
232+ result = await api .async_read_response_retry (response , graphql_url , ignore_errors = True )
230233
231- if result != successful_data :
232- print (f"ERROR: Expected successful data after rate limit retry, got { result } " )
234+ rate_limit_logs = [message for message in logged if "Rate limit error" in message ]
235+ if result is not None :
236+ print (f"ERROR: Expected None for a rate-limited response, got { result } " )
233237 failed = True
234- elif api . async_read_response . call_count != 2 :
235- print (f"ERROR: Expected 2 calls ( rate limit + retry) , got { api . async_read_response . call_count } " )
238+ elif response . text . await_count != 1 :
239+ print (f"ERROR: Expected the rate-limited response to be read once , got { response . text . await_count } " )
236240 failed = True
237- elif mock_sleep .call_count != 1 :
238- print (f"ERROR: Expected 1 sleep call after rate limit, got { mock_sleep .call_count } " )
241+ elif mock_sleep .call_count != 0 :
242+ print (f"ERROR: Expected no sleep on a rate limit, got { mock_sleep .call_count } sleep calls" )
243+ failed = True
244+ elif len (rate_limit_logs ) != 1 :
245+ print (f"ERROR: Expected one rate limit warning, got { len (rate_limit_logs )} : { rate_limit_logs } " )
246+ failed = True
247+ elif api .failures_total != 1 :
248+ print (f"ERROR: failures_total should be 1, got { api .failures_total } " )
239249 failed = True
240250 else :
241- print ("PASS: Rate limit error triggers retry with exponential backoff " )
251+ print ("PASS: Rate limit returns at once, read once, no sleep, one warning " )
242252
243253 # Test 8: Auth/blocked responses are definitive and are not re-read.
244254 # aiohttp caches the response body, so re-reading the same 401/403 response can only
0 commit comments