184 lines
7.1 KiB
Python
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"])
|