Skip to content

Commit 8c4095d

Browse files
committed
Add sustain only rate limit exceed test, use timeperiod values instead of hardcoded ints
1 parent 01c8dbe commit 8c4095d

1 file changed

Lines changed: 102 additions & 1 deletion

File tree

tests/test_ratelimits.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime, timedelta
22
from httpx import Response
33
import pytest
4+
import asyncio
45

56
from tests.common import get_response_json
67
from xbox.webapi.api.provider.ratelimitedprovider import RateLimitedProvider
@@ -137,4 +138,104 @@ async def make_request():
137138

138139
# Assert that the timeout is the correct length
139140
delta: timedelta = try_again_in - start_time
140-
assert delta.seconds == 15
141+
assert delta.seconds == TimePeriod.BURST.value # 15 seconds
142+
143+
144+
async def helper_reach_and_wait_for_burst(
145+
make_request, start_time, burst_limit: int, expected_counter: int
146+
):
147+
# Make as many requests as possible without exceeding the BURST limit.
148+
for i in range(burst_limit):
149+
await make_request()
150+
151+
# Make another request, ensure that it raises the exception.
152+
with pytest.raises(RateLimitExceededException) as exception:
153+
await make_request()
154+
155+
# Get the error instance from pytest
156+
ex: RateLimitExceededException = exception.value
157+
158+
# Assert that the counter matches the what we expect (burst, 2x burstm etc)
159+
assert ex.rate_limit.get_counter() == expected_counter
160+
161+
# Get the reset_after value
162+
# (if we call it after waiting for it to expire the function will return None)
163+
burst_resets_after = ex.rate_limit.get_reset_after()
164+
165+
# Wait for the burst limit timeout to elapse.
166+
await asyncio.sleep(TimePeriod.BURST.value) # 15 seconds
167+
168+
# Assert that the reset_after value has passed.
169+
assert burst_resets_after < datetime.now()
170+
171+
172+
@pytest.mark.asyncio
173+
async def test_ratelimits_exceeded_sustain_only(respx_mock, xbl_client):
174+
async def make_request():
175+
route = respx_mock.get("https://social.xboxlive.com").mock(
176+
return_value=Response(200, json=get_response_json("people_summary_own"))
177+
)
178+
ret = await xbl_client.people.get_friends_summary_own()
179+
180+
assert route.called
181+
182+
# Record the start time to ensure that the timeouts are the correct length
183+
start_time = datetime.now()
184+
185+
# Get the max requests for this route.
186+
max_request_num = xbl_client.people.RATE_LIMITS["sustain"] # 30
187+
burst_max_request_num = xbl_client.people.RATE_LIMITS["burst"] # 10
188+
189+
# In this case, the BURST limit is three times that of SUSTAIN, so we need to exceed the burst limit three times.
190+
191+
# Exceed the burst limit and wait for it to reset (10 requests)
192+
await helper_reach_and_wait_for_burst(
193+
make_request, start_time, burst_limit=burst_max_request_num, expected_counter=10
194+
)
195+
196+
# Repeat: Exceed the burst limit and wait for it to reset (10 requests)
197+
# Counter (the sustain one will be returned)
198+
# For (CombinedRateLimit).get_counter(), the highest counter is returned. (sustain in this case)
199+
await helper_reach_and_wait_for_burst(
200+
make_request, start_time, burst_limit=burst_max_request_num, expected_counter=20
201+
)
202+
203+
# Now, make the rest of the requests (10 left, 20/30 done!)
204+
for i in range(10):
205+
await make_request()
206+
207+
# Wait for the burst limit to 'reset'.
208+
await asyncio.sleep(TimePeriod.BURST.value) # 15 seconds
209+
210+
# Now, we have made 30 requests.
211+
# The counters should be as follows:
212+
# - BURST: 0* (will reset on next check)
213+
# - SUSTAIN: 30
214+
# The next request we make should exceed the SUSTAIN rate limit.
215+
216+
# Make another request, ensure that it raises the exception.
217+
with pytest.raises(RateLimitExceededException) as exception:
218+
await make_request()
219+
220+
# Get the error instance from pytest
221+
ex: RateLimitExceededException = exception.value
222+
223+
# Get the SingleRateLimit objects from the exception
224+
rl: CombinedRateLimit = ex.rate_limit
225+
burst = rl.get_limits_by_period(TimePeriod.BURST)[0]
226+
sustain = rl.get_limits_by_period(TimePeriod.SUSTAIN)[0]
227+
228+
# Assert that we have only exceeded the sustain limit.
229+
assert not burst.is_exceeded()
230+
assert sustain.is_exceeded()
231+
232+
# Assert that the counter matches the max request num (should not have incremented above max value)
233+
assert ex.rate_limit.get_counter() == max_request_num
234+
235+
# Get the timeout we were issued
236+
try_again_in = ex.rate_limit.get_reset_after()
237+
238+
# Assert that the timeout is the correct length
239+
# The SUSTAIN counter has not been reset during this test, so the try again in should be 300 seconds since we started this test.
240+
delta: timedelta = try_again_in - start_time
241+
assert delta.seconds == TimePeriod.SUSTAIN.value # 300 seconds (5 minutes)

0 commit comments

Comments
 (0)