Expand Up
@@ -43,7 +43,9 @@ def rate_limit_detected(w):
return False
def create_mock_rate_limit_response(status_code=429, retry_after=None, content_type="application/json"):
def create_mock_rate_limit_response(
status_code=429, retry_after=None, content_type="application/json"
):
"""Create a mock response object for testing rate limit scenarios."""
# Use Mock(spec=requests.Response) to properly simulate a requests.Response object
mock_response = Mock(spec=requests.Response)
Expand All
@@ -52,12 +54,12 @@ def create_mock_rate_limit_response(status_code=429, retry_after=None, content_t
mock_response.headers = {}
if retry_after is not None:
mock_response.headers[' Retry-After' ] = retry_after
mock_response.headers[" Retry-After" ] = retry_after
mock_response.headers[' Content-Type' ] = content_type
mock_response.headers[" Content-Type" ] = content_type
mock_response.json.return_value = {
' message': ' Rate limit exceeded' ,
' trackingId': ' test-tracking-id-12345'
" message": " Rate limit exceeded" ,
" trackingId": " test-tracking-id-12345",
}
# Mock the request attribute that ApiError constructor needs
Expand Down
Expand Up
@@ -91,22 +93,27 @@ def test_rate_limit_error_with_valid_retry_after():
"""Test RateLimitError works correctly with valid Retry-After headers."""
# Test with various valid integer values
test_cases = [
('30' , 30), # Normal case
('60' , 60), # One minute
('0' , 1), # Zero should default to 1 (minimum)
('1' , 1), # Minimum value
(' 300' , 300), # Five minutes
("30" , 30), # Normal case
("60" , 60), # One minute
("0" , 1), # Zero should default to 1 (minimum)
("1" , 1), # Minimum value
(" 300" , 300), # Five minutes
]
for header_value, expected_value in test_cases:
mock_response = create_mock_rate_limit_response(retry_after=header_value)
mock_response = create_mock_rate_limit_response(
retry_after=header_value
)
try:
error = webexpythonsdk.RateLimitError(mock_response)
assert error.retry_after == expected_value, \
f"Expected retry_after={expected_value}, got {error.retry_after} for header '{header_value}'"
assert (
error.retry_after == expected_value
), f"Expected retry_after={expected_value}, got {error.retry_after} for header '{header_value}'"
except Exception as e:
pytest.fail(f"RateLimitError creation failed for valid header '{header_value}': {e}")
pytest.fail(
f"RateLimitError creation failed for valid header '{header_value}': {e}"
)
def test_rate_limit_error_without_retry_after():
Expand All
@@ -115,9 +122,13 @@ def test_rate_limit_error_without_retry_after():
try:
error = webexpythonsdk.RateLimitError(mock_response)
assert error.retry_after == 15, f"Expected default retry_after=15, got {error.retry_after}"
assert (
error.retry_after == 15
), f"Expected default retry_after=15, got {error.retry_after}"
except Exception as e:
pytest.fail(f"RateLimitError creation failed when Retry-After header is missing: {e}")
pytest.fail(
f"RateLimitError creation failed when Retry-After header is missing: {e}"
)
def test_rate_limit_error_with_malformed_retry_after():
Expand All
@@ -127,65 +138,77 @@ def test_rate_limit_error_with_malformed_retry_after():
like 'rand(30),add(30)' cause ValueError exceptions.
"""
malformed_headers = [
' rand(30),add(30)', # The exact case from the user report
' invalid', # Non-numeric string
' 30.5', # Float (should fail int conversion)
' 30 seconds', # String with numbers and text
' 30,60', # Comma-separated values
'', # Empty string
' None', # String 'None'
' null', # String 'null'
" rand(30),add(30)", # The exact case from the user report
" invalid", # Non-numeric string
" 30.5", # Float (should fail int conversion)
" 30 seconds", # String with numbers and text
" 30,60", # Comma-separated values
"", # Empty string
" None", # String 'None'
" null", # String 'null'
]
for malformed_header in malformed_headers:
mock_response = create_mock_rate_limit_response(retry_after=malformed_header)
mock_response = create_mock_rate_limit_response(
retry_after=malformed_header
)
try:
# This should NOT raise a ValueError - it should handle gracefully
error = webexpythonsdk.RateLimitError(mock_response)
# If we get here, the error was handled gracefully
# The retry_after should default to 15 for malformed headers
assert error.retry_after == 15, \
f"Expected default retry_after=15 for malformed header '{malformed_header}', got {error.retry_after}"
assert (
error.retry_after == 15
), f"Expected default retry_after=15 for malformed header '{malformed_header}', got {error.retry_after}"
except ValueError as e:
# This is the bug we're testing for - it should NOT happen
pytest.fail(f"RateLimitError raised ValueError for malformed header '{malformed_header}': {e}")
pytest.fail(
f"RateLimitError raised ValueError for malformed header '{malformed_header}': {e}"
)
except Exception as e:
# Other exceptions are acceptable as long as they're not ValueError
if isinstance(e, ValueError):
pytest.fail(f"RateLimitError raised ValueError for malformed header '{malformed_header}': {e}")
pytest.fail(
f"RateLimitError raised ValueError for malformed header '{malformed_header}': {e}"
)
def test_rate_limit_error_with_non_string_retry_after():
"""Test RateLimitError handles non-string Retry-After header values."""
# Test cases with expected behavior based on how Python int() actually works
test_cases = [
(None, 15), # None value -> defaults to 15
(30, 30), # Integer -> converts to 30 (not malformed)
(30.5, 30), # Float -> converts to 30 (truncated)
(True, 1), # Boolean True -> converts to 1
(False, 1), # Boolean False -> converts to 0, then max(1, 0) = 1
([], 15), # List -> TypeError, defaults to 15
({}, 15), # Dict -> TypeError, defaults to 15
]
(None, 15), # None value -> defaults to 15
(30, 30), # Integer -> converts to 30 (not malformed)
(30.5, 30), # Float -> converts to 30 (truncated)
(True, 1), # Boolean True -> converts to 1
(False, 1), # Boolean False -> converts to 0, then max(1, 0) = 1
([], 15), # List -> TypeError, defaults to 15
({}, 15), # Dict -> TypeError, defaults to 15
]
for non_string_value, expected_value in test_cases:
mock_response = create_mock_rate_limit_response(retry_after=non_string_value)
mock_response = create_mock_rate_limit_response(
retry_after=non_string_value
)
try:
error = webexpythonsdk.RateLimitError(mock_response)
assert error.retry_after == expected_value, \
f"Expected retry_after={expected_value}, got {error.retry_after} for non-string value {non_string_value}"
assert (
error.retry_after == expected_value
), f"Expected retry_after={expected_value}, got {error.retry_after} for non-string value {non_string_value}"
except Exception as e:
pytest.fail(f"RateLimitError creation failed for non-string value {non_string_value}: {e}")
pytest.fail(
f"RateLimitError creation failed for non-string value {non_string_value}: {e}"
)
def test_rate_limit_error_integration_with_check_response_code():
"""Test that check_response_code properly raises RateLimitError for 429 responses."""
from webexpythonsdk.utils import check_response_code
# Test with valid Retry-After header
mock_response = create_mock_rate_limit_response(retry_after='45' )
mock_response = create_mock_rate_limit_response(retry_after="45" )
with pytest.raises(webexpythonsdk.RateLimitError) as exc_info:
check_response_code(mock_response, 200) # Expect 200, get 429
Expand All
@@ -200,7 +223,9 @@ def test_rate_limit_error_integration_with_malformed_header():
from webexpythonsdk.utils import check_response_code
# Test with malformed Retry-After header
mock_response = create_mock_rate_limit_response(retry_after='rand(30),add(30)')
mock_response = create_mock_rate_limit_response(
retry_after="rand(30),add(30)"
)
with pytest.raises(webexpythonsdk.RateLimitError) as exc_info:
check_response_code(mock_response, 200) # Expect 200, get 429
Expand All
@@ -213,37 +238,42 @@ def test_rate_limit_error_integration_with_malformed_header():
def test_rate_limit_error_edge_cases():
"""Test RateLimitError with edge case Retry-After values."""
# Test cases based on how Python int() actually works with strings
# Test cases based on how Python int() actually works with strings
edge_cases = [
('-1' , 1), # Negative string -> converts to -1, then max(1, -1) = 1
(' 999999' , 999999), # Very large number string -> converts to 999999
(' 0.0' , 15), # Float string -> treated as malformed, defaults to 15
(' 0.9' , 15), # Float string -> treated as malformed, defaults to 15
(' 1.0' , 15), # Float string -> treated as malformed, defaults to 15
(' 1.9' , 15), # Float string -> treated as malformed, defaults to 15
(' 2.0' , 15), # Float string -> treated as malformed, defaults to 15
]
("-1" , 1), # Negative string -> converts to -1, then max(1, -1) = 1
(" 999999" , 999999), # Very large number string -> converts to 999999
(" 0.0" , 15), # Float string -> treated as malformed, defaults to 15
(" 0.9" , 15), # Float string -> treated as malformed, defaults to 15
(" 1.0" , 15), # Float string -> treated as malformed, defaults to 15
(" 1.9" , 15), # Float string -> treated as malformed, defaults to 15
(" 2.0" , 15), # Float string -> treated as malformed, defaults to 15
]
for header_value, expected_value in edge_cases:
mock_response = create_mock_rate_limit_response(retry_after=header_value)
mock_response = create_mock_rate_limit_response(
retry_after=header_value
)
try:
error = webexpythonsdk.RateLimitError(mock_response)
# All float strings are being treated as malformed and defaulting to 15
# Integer strings work normally with max(1, value)
if '.' in header_value: # Float strings
if "." in header_value: # Float strings
actual_expected = 15 # Treated as malformed
else:
actual_expected = max(1, expected_value)
assert error.retry_after == actual_expected, \
f"Expected retry_after={actual_expected}, got {error.retry_after} for header '{header_value}'"
assert (
error.retry_after == actual_expected
), f"Expected retry_after={actual_expected}, got {error.retry_after} for header '{header_value}'"
except Exception as e:
pytest.fail(f"RateLimitError creation failed for edge case header '{header_value}': {e}")
pytest.fail(
f"RateLimitError creation failed for edge case header '{header_value}': {e}"
)
def test_rate_limit_error_response_attributes():
"""Test that RateLimitError properly extracts all response attributes."""
mock_response = create_mock_rate_limit_response(retry_after='60' )
mock_response = create_mock_rate_limit_response(retry_after="60" )
error = webexpythonsdk.RateLimitError(mock_response)
Expand Down