|
1 | 1 | from datetime import datetime, timedelta |
2 | 2 | from httpx import Response |
3 | 3 | import pytest |
| 4 | +import asyncio |
4 | 5 |
|
5 | 6 | from tests.common import get_response_json |
6 | 7 | from xbox.webapi.api.provider.ratelimitedprovider import RateLimitedProvider |
@@ -137,4 +138,104 @@ async def make_request(): |
137 | 138 |
|
138 | 139 | # Assert that the timeout is the correct length |
139 | 140 | 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