1- import asyncio
21from datetime import datetime , timedelta
32
3+ from freezegun import freeze_time
4+ from freezegun .api import FrozenDateTimeFactory
45from httpx import Response
56import pytest
67
8+ from tests .common import get_response_json
79from xbox .webapi .api .provider .ratelimitedprovider import RateLimitedProvider
810from xbox .webapi .common .exceptions import RateLimitExceededException , XboxException
911from xbox .webapi .common .ratelimits import CombinedRateLimit
1012from xbox .webapi .common .ratelimits .models import TimePeriod
1113
12- from tests .common import get_response_json
13-
1414
1515def helper_test_combinedratelimit (
1616 crl : CombinedRateLimit , burstLimit : int , sustainLimit : int
@@ -143,7 +143,11 @@ async def make_request():
143143
144144
145145async def helper_reach_and_wait_for_burst (
146- make_request , start_time , burst_limit : int , expected_counter : int
146+ make_request ,
147+ start_time ,
148+ burst_limit : int ,
149+ expected_counter : int ,
150+ frozen_datetime : FrozenDateTimeFactory
147151):
148152 # Make as many requests as possible without exceeding the BURST limit.
149153 for _ in range (burst_limit ):
@@ -164,79 +168,80 @@ async def helper_reach_and_wait_for_burst(
164168 burst_resets_after = ex .rate_limit .get_reset_after ()
165169
166170 # Wait for the burst limit timeout to elapse.
167- await asyncio . sleep ( TimePeriod .BURST .value ) # 15 seconds
171+ frozen_datetime . tick ( timedelta ( seconds = TimePeriod .BURST .value ))
168172
169173 # Assert that the reset_after value has passed.
170- assert burst_resets_after < datetime .now ()
174+ assert burst_resets_after == datetime .now ()
175+ frozen_datetime .tick (timedelta (seconds = 1 ))
171176
172177
173178@pytest .mark .asyncio
174179async def test_ratelimits_exceeded_sustain_only (respx_mock , xbl_client ):
175- async def make_request ():
176- route = respx_mock .get ("https://social.xboxlive.com" ).mock (
177- return_value = Response (200 , json = get_response_json ("people_summary_own" ))
178- )
179- await xbl_client .people .get_friends_summary_own ()
180-
181- assert route .called
180+ with freeze_time ("2025-10-30T00:00:00-00:00" ) as frozen_datetime :
181+ async def make_request ():
182+ route = respx_mock .get ("https://social.xboxlive.com" ).mock (
183+ return_value = Response (200 , json = get_response_json ("people_summary_own" ))
184+ )
185+ await xbl_client .people .get_friends_summary_own ()
182186
183- # Record the start time to ensure that the timeouts are the correct length
184- start_time = datetime .now ()
187+ assert route .called
185188
186- # Get the max requests for this route.
187- max_request_num = xbl_client .people .RATE_LIMITS ["sustain" ] # 30
188- burst_max_request_num = xbl_client .people .RATE_LIMITS ["burst" ] # 10
189+ # Record the start time to ensure that the timeouts are the correct length
190+ start_time = datetime .now ()
189191
190- # In this case, the BURST limit is three times that of SUSTAIN, so we need to exceed the burst limit three times.
192+ # Get the max requests for this route.
193+ max_request_num = xbl_client .people .RATE_LIMITS ["sustain" ] # 30
194+ burst_max_request_num = xbl_client .people .RATE_LIMITS ["burst" ] # 10
191195
192- # Exceed the burst limit and wait for it to reset (10 requests)
193- await helper_reach_and_wait_for_burst (
194- make_request , start_time , burst_limit = burst_max_request_num , expected_counter = 10
195- )
196+ # In this case, the BURST limit is three times that of SUSTAIN, so we need to exceed the burst limit three times.
196197
197- # Repeat: Exceed the burst limit and wait for it to reset (10 requests)
198- # Counter (the sustain one will be returned)
199- # For (CombinedRateLimit).get_counter(), the highest counter is returned. (sustain in this case)
200- await helper_reach_and_wait_for_burst (
201- make_request , start_time , burst_limit = burst_max_request_num , expected_counter = 20
202- )
198+ # Exceed the burst limit and wait for it to reset (10 requests)
199+ await helper_reach_and_wait_for_burst (
200+ make_request , start_time , burst_limit = burst_max_request_num , expected_counter = 10 , frozen_datetime = frozen_datetime
201+ )
203202
204- # Now, make the rest of the requests (10 left, 20/30 done!)
205- for _ in range (10 ):
206- await make_request ()
203+ # Repeat: Exceed the burst limit and wait for it to reset (10 requests)
204+ # Counter (the sustain one will be returned)
205+ # For (CombinedRateLimit).get_counter(), the highest counter is returned. (sustain in this case)
206+ await helper_reach_and_wait_for_burst (
207+ make_request , start_time , burst_limit = burst_max_request_num , expected_counter = 20 , frozen_datetime = frozen_datetime
208+ )
207209
208- # Wait for the burst limit to 'reset'.
209- await asyncio .sleep (TimePeriod .BURST .value ) # 15 seconds
210+ # Now, make the rest of the requests (10 left, 20/30 done!)
211+ for _ in range (10 ):
212+ await make_request ()
210213
211- # Now, we have made 30 requests.
212- # The counters should be as follows:
213- # - BURST: 0* (will reset on next check)
214- # - SUSTAIN: 30
215- # The next request we make should exceed the SUSTAIN rate limit.
214+ # Wait for the burst limit to 'reset'.
215+ frozen_datetime .tick (timedelta (seconds = TimePeriod .BURST .value + 1 ))
216+ # Now, we have made 30 requests.
217+ # The counters should be as follows:
218+ # - BURST: 0* (will reset on next check)
219+ # - SUSTAIN: 30
220+ # The next request we make should exceed the SUSTAIN rate limit.
216221
217- # Make another request, ensure that it raises the exception.
218- with pytest .raises (RateLimitExceededException ) as exception :
219- await make_request ()
222+ # Make another request, ensure that it raises the exception.
223+ with pytest .raises (RateLimitExceededException ) as exception :
224+ await make_request ()
220225
221- # Get the error instance from pytest
222- ex : RateLimitExceededException = exception .value
226+ # Get the error instance from pytest
227+ ex : RateLimitExceededException = exception .value
223228
224- # Get the SingleRateLimit objects from the exception
225- rl : CombinedRateLimit = ex .rate_limit
226- burst = rl .get_limits_by_period (TimePeriod .BURST )[0 ]
227- sustain = rl .get_limits_by_period (TimePeriod .SUSTAIN )[0 ]
229+ # Get the SingleRateLimit objects from the exception
230+ rl : CombinedRateLimit = ex .rate_limit
231+ burst = rl .get_limits_by_period (TimePeriod .BURST )[0 ]
232+ sustain = rl .get_limits_by_period (TimePeriod .SUSTAIN )[0 ]
228233
229- # Assert that we have only exceeded the sustain limit.
230- assert not burst .is_exceeded ()
231- assert sustain .is_exceeded ()
234+ # Assert that we have only exceeded the sustain limit.
235+ assert not burst .is_exceeded ()
236+ assert sustain .is_exceeded ()
232237
233- # Assert that the counter matches the max request num (should not have incremented above max value)
234- assert ex .rate_limit .get_counter () == max_request_num
238+ # Assert that the counter matches the max request num (should not have incremented above max value)
239+ assert ex .rate_limit .get_counter () == max_request_num
235240
236- # Get the timeout we were issued
237- try_again_in = ex .rate_limit .get_reset_after ()
241+ # Get the timeout we were issued
242+ try_again_in = ex .rate_limit .get_reset_after ()
238243
239- # Assert that the timeout is the correct length
240- # The SUSTAIN counter has not been reset during this test, so the try again in should be 300 seconds since we started this test.
241- delta : timedelta = try_again_in - start_time
242- assert delta .seconds == TimePeriod .SUSTAIN .value # 300 seconds (5 minutes)
244+ # Assert that the timeout is the correct length
245+ # The SUSTAIN counter has not been reset during this test, so the try again in should be 300 seconds since we started this test.
246+ delta : timedelta = try_again_in - start_time
247+ assert delta .seconds == TimePeriod .SUSTAIN .value # 300 seconds (5 minutes)
0 commit comments