npc-clients-python/tests/test_auth_retry_fix.py

184 lines
7.1 KiB
Python

"""
Test that the client doesn't send Authorization:[] headers after auth failures.
This test ensures HTTP 500 errors are prevented by proper auth handling.
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
import httpx
from gumyum_npc_client import GumYumClient, GumYumAuthError
class TestAuthRetryFix:
"""Test that auth retry logic doesn't create Authorization:[] headers"""
@pytest.mark.asyncio
async def test_no_retry_with_cleared_token(self):
"""Test that client doesn't retry after clearing token on 401 error"""
client = GumYumClient("http://test.com")
# Set a valid token initially
client.set_token("valid_token")
# Mock the httpx response to return 401
mock_response = Mock()
mock_response.status_code = 401
mock_response.headers = {"content-type": "application/json"}
mock_response.text = '{"error": "Unauthorized"}'
mock_response.json.return_value = {"error": "Unauthorized"}
# Track all requests made
request_headers = []
async def mock_request(*args, **kwargs):
# Capture headers from each request
request_headers.append(kwargs.get("headers", {}))
return mock_response
# Replace the httpx client request method
with patch.object(client._client, "request", side_effect=mock_request):
# This should fail with auth error, not retry
with pytest.raises(GumYumAuthError) as exc_info:
await client.get("npc", params={"universe_id": "test", "seed": "123"})
# Should contain "token cleared" in the error message
assert "token cleared" in str(exc_info.value)
# Should only make ONE request (no retry)
assert len(request_headers) == 1
# The single request should have had the valid token
assert request_headers[0].get("Authorization") == "Bearer valid_token"
# Token should be cleared after 401
assert client._token is None
@pytest.mark.asyncio
async def test_no_empty_array_authorization_header(self):
"""Test that Authorization header is never an empty array"""
client = GumYumClient("http://test.com")
# Test various token states
test_cases = [
(None, "no_header"), # No token -> no Authorization header
("", "no_header"), # Empty string -> no Authorization header
("valid", "Bearer valid"), # Valid token -> proper header
(" ", "no_header"), # Whitespace -> no Authorization header
]
for token, expected in test_cases:
client._token = token
headers = client.get_headers()
if expected == "no_header":
assert (
"Authorization" not in headers
), f"Token {repr(token)} should not create Authorization header"
else:
assert (
headers.get("Authorization") == expected
), f"Token {repr(token)} should create {expected}"
# Never should be an empty array
assert headers.get("Authorization") != []
assert headers.get("Authorization") != [""]
@pytest.mark.asyncio
async def test_auto_auth_disabled_after_explicit_auth(self):
"""Test that auto-auth doesn't interfere with explicit authentication"""
client = GumYumClient("http://test.com")
# Explicitly authenticate
client.set_token("explicit_token")
# This should be set by set_token
assert hasattr(client, "_auth_attempted_by_user")
assert client._auth_attempted_by_user is True
# Clear the token (simulating token expiry)
client._token = None
# Mock a request that would normally trigger auto-auth
with patch.object(client, "_ensure_authenticated") as mock_ensure_auth:
with patch.object(client._client, "request") as mock_request:
mock_response = Mock()
mock_response.status_code = 401
mock_response.headers = {"content-type": "application/json"}
mock_response.text = '{"error": "Unauthorized"}'
mock_request.return_value = mock_response
try:
await client.get("npc", params={"test": "test"})
except GumYumAuthError:
pass
# _ensure_authenticated should have been called
mock_ensure_auth.assert_called_once()
@pytest.mark.asyncio
async def test_headers_sanitization(self):
"""Test that malformed Authorization headers are sanitized"""
client = GumYumClient("http://test.com")
client.set_token("valid_token")
# Mock the request to check final headers
final_headers = None
async def capture_headers(*args, **kwargs):
nonlocal final_headers
final_headers = kwargs.get("headers", {})
mock_response = Mock()
mock_response.status_code = 200
mock_response.headers = {"content-type": "application/json"}
mock_response.text = '{"test": "data"}'
return mock_response
# Test that empty array Authorization is removed
with patch.object(client._client, "request", side_effect=capture_headers):
# Try to inject Authorization: [] via extra headers
await client._request("GET", "test", headers={"Authorization": []})
# The final headers should not have Authorization: []
assert final_headers.get("Authorization") != []
# It should either be the valid token or not present
assert final_headers.get("Authorization") in ["Bearer valid_token", None]
def test_sync_client_same_behavior(self):
"""Test that sync client has same auth retry behavior"""
from gumyum_npc_sync import GumYumClient as GumYumSyncClient
client = GumYumSyncClient("http://test.com")
client.set_token("valid_token")
# Mock the response to return 401
mock_response = Mock()
mock_response.status_code = 401
mock_response.headers = {"content-type": "application/json"}
mock_response.text = '{"error": "Unauthorized"}'
mock_response.json.return_value = {"error": "Unauthorized"}
request_count = 0
def mock_request(*args, **kwargs):
nonlocal request_count
request_count += 1
return mock_response
# Replace the session request method
with patch.object(client._session, "request", side_effect=mock_request):
# This should fail with auth error, not retry
with pytest.raises(GumYumAuthError) as exc_info:
client.get("npc", params={"universe_id": "test", "seed": "123"})
# Should contain "token cleared" in the error message
assert "token cleared" in str(exc_info.value)
# Should only make ONE request (no retry)
assert request_count == 1
# Token should be cleared after 401
assert client._token is None
if __name__ == "__main__":
pytest.main([__file__, "-v"])