Add and enhance unit tests: include tests for ProfileService, GameService, and AnalysisService; integrate comprehensive mock data and fixtures. Refactor imports and naming conventions in tests for consistency. Augment .env.example with monitoring and logging configurations.
This commit is contained in:
parent
8cc53378b7
commit
1b7963c239
9 changed files with 1814 additions and 17 deletions
|
|
@ -1 +1,415 @@
|
|||
# TODO
|
||||
"""
|
||||
Integration tests for authentication flow.
|
||||
|
||||
These tests verify the complete authentication workflow including
|
||||
login, session management, and token validation.
|
||||
"""
|
||||
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from geoguessr_mcp.auth.session import SessionManager, UserSession
|
||||
|
||||
|
||||
class TestAuthenticationFlow:
|
||||
"""Integration tests for authentication flow with mocked HTTP."""
|
||||
|
||||
@pytest.fixture
|
||||
def session_manager(self):
|
||||
"""Create a fresh session manager for each test."""
|
||||
return SessionManager()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_httpx_client(self):
|
||||
"""Create a mock httpx client."""
|
||||
with patch("httpx.AsyncClient") as mock_client_class:
|
||||
mock_client = AsyncMock()
|
||||
mock_client.__aenter__.return_value = mock_client
|
||||
mock_client.__aexit__.return_value = None
|
||||
mock_client_class.return_value = mock_client
|
||||
yield mock_client
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_login_flow(self, session_manager, mock_httpx_client, mock_profile_data):
|
||||
"""Test complete login flow from credentials to session."""
|
||||
# Setup mock responses
|
||||
login_response = MagicMock()
|
||||
login_response.status_code = 200
|
||||
login_response.cookies.jar = []
|
||||
login_response.headers = {"set-cookie": "_ncfa=test_cookie_value; Path=/; HttpOnly"}
|
||||
|
||||
mock_cookie = MagicMock()
|
||||
mock_cookie.name = "_ncfa"
|
||||
mock_cookie.value = "test_cookie_value"
|
||||
login_response.cookies.jar.append(mock_cookie)
|
||||
|
||||
profile_response = MagicMock()
|
||||
profile_response.status_code = 200
|
||||
profile_response.json.return_value = mock_profile_data
|
||||
|
||||
mock_httpx_client.post = AsyncMock(return_value=login_response)
|
||||
mock_httpx_client.get = AsyncMock(return_value=profile_response)
|
||||
mock_httpx_client.cookies.set = MagicMock()
|
||||
|
||||
# Perform login
|
||||
session_token, session = await session_manager.login("user@example.com", "password123")
|
||||
|
||||
# Verify session was created
|
||||
assert session_token is not None
|
||||
assert len(session_token) > 20 # Token should be substantial
|
||||
assert session.ncfa_cookie == "test_cookie_value"
|
||||
assert session.username == "TestPlayer"
|
||||
assert session.user_id == "test-user-id"
|
||||
assert session.is_valid()
|
||||
|
||||
# Verify session can be retrieved
|
||||
retrieved_session = await session_manager.get_session(session_token)
|
||||
assert retrieved_session is not None
|
||||
assert retrieved_session.username == session.username
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_then_logout(self, session_manager, mock_httpx_client, mock_profile_data):
|
||||
"""Test login followed by logout invalidates session."""
|
||||
# Setup login mocks
|
||||
login_response = MagicMock()
|
||||
login_response.status_code = 200
|
||||
login_response.cookies.jar = []
|
||||
mock_cookie = MagicMock()
|
||||
mock_cookie.name = "_ncfa"
|
||||
mock_cookie.value = "test_cookie"
|
||||
login_response.cookies.jar.append(mock_cookie)
|
||||
|
||||
profile_response = MagicMock()
|
||||
profile_response.status_code = 200
|
||||
profile_response.json.return_value = mock_profile_data
|
||||
|
||||
mock_httpx_client.post = AsyncMock(return_value=login_response)
|
||||
mock_httpx_client.get = AsyncMock(return_value=profile_response)
|
||||
mock_httpx_client.cookies.set = MagicMock()
|
||||
|
||||
# Login
|
||||
session_token, _ = await session_manager.login("user@example.com", "password")
|
||||
|
||||
# Verify session exists
|
||||
session_before = await session_manager.get_session(session_token)
|
||||
assert session_before is not None
|
||||
|
||||
# Logout
|
||||
logout_result = await session_manager.logout(session_token)
|
||||
assert logout_result is True
|
||||
|
||||
# Verify session is invalidated
|
||||
session_after = await session_manager.get_session(session_token)
|
||||
assert session_after is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_user_sessions(self, session_manager, mock_httpx_client):
|
||||
"""Test managing multiple user sessions."""
|
||||
# Setup responses for two different users
|
||||
user1_profile = {"id": "user1", "nick": "User1", "email": "user1@example.com"}
|
||||
user2_profile = {"id": "user2", "nick": "User2", "email": "user2@example.com"}
|
||||
|
||||
login_response = MagicMock()
|
||||
login_response.status_code = 200
|
||||
login_response.cookies.jar = []
|
||||
mock_cookie = MagicMock()
|
||||
mock_cookie.name = "_ncfa"
|
||||
mock_cookie.value = "cookie_value"
|
||||
login_response.cookies.jar.append(mock_cookie)
|
||||
|
||||
profile_response1 = MagicMock()
|
||||
profile_response1.status_code = 200
|
||||
profile_response1.json.return_value = user1_profile
|
||||
|
||||
profile_response2 = MagicMock()
|
||||
profile_response2.status_code = 200
|
||||
profile_response2.json.return_value = user2_profile
|
||||
|
||||
mock_httpx_client.post = AsyncMock(return_value=login_response)
|
||||
mock_httpx_client.cookies.set = MagicMock()
|
||||
|
||||
# Login user 1
|
||||
mock_httpx_client.get = AsyncMock(return_value=profile_response1)
|
||||
token1, session1 = await session_manager.login("user1@example.com", "pass1")
|
||||
|
||||
# Login user 2
|
||||
mock_httpx_client.get = AsyncMock(return_value=profile_response2)
|
||||
token2, session2 = await session_manager.login("user2@example.com", "pass2")
|
||||
|
||||
# Both sessions should be valid
|
||||
assert token1 != token2
|
||||
assert (await session_manager.get_session(token1)) is not None
|
||||
assert (await session_manager.get_session(token2)) is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_replacement_same_user(
|
||||
self, session_manager, mock_httpx_client, mock_profile_data
|
||||
):
|
||||
"""Test that logging in as same user replaces old session."""
|
||||
login_response = MagicMock()
|
||||
login_response.status_code = 200
|
||||
login_response.cookies.jar = []
|
||||
mock_cookie = MagicMock()
|
||||
mock_cookie.name = "_ncfa"
|
||||
mock_cookie.value = "cookie_value"
|
||||
login_response.cookies.jar.append(mock_cookie)
|
||||
|
||||
profile_response = MagicMock()
|
||||
profile_response.status_code = 200
|
||||
profile_response.json.return_value = mock_profile_data
|
||||
|
||||
mock_httpx_client.post = AsyncMock(return_value=login_response)
|
||||
mock_httpx_client.get = AsyncMock(return_value=profile_response)
|
||||
mock_httpx_client.cookies.set = MagicMock()
|
||||
|
||||
# First login
|
||||
token1, _ = await session_manager.login("user@example.com", "pass")
|
||||
|
||||
# Second login as same user
|
||||
token2, _ = await session_manager.login("user@example.com", "pass")
|
||||
|
||||
# First token should be invalid, second should be valid
|
||||
assert token1 != token2
|
||||
assert (await session_manager.get_session(token1)) is None
|
||||
assert (await session_manager.get_session(token2)) is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expired_session_cleanup(self, session_manager):
|
||||
"""Test that expired sessions are cleaned up when accessed."""
|
||||
# Manually create an expired session
|
||||
expired_session = UserSession(
|
||||
ncfa_cookie="expired_cookie",
|
||||
user_id="expired_user",
|
||||
username="ExpiredUser",
|
||||
email="expired@example.com",
|
||||
expires_at=datetime.now(UTC) - timedelta(days=1), # Expired yesterday
|
||||
)
|
||||
|
||||
# Store the expired session
|
||||
async with session_manager._lock:
|
||||
session_manager._sessions["expired_token"] = expired_session
|
||||
session_manager._user_sessions["expired_user"] = "expired_token"
|
||||
|
||||
# Try to get the session - should return None and clean up
|
||||
session = await session_manager.get_session("expired_token")
|
||||
assert session is None
|
||||
|
||||
# Verify cleanup
|
||||
assert "expired_token" not in session_manager._sessions
|
||||
assert "expired_user" not in session_manager._user_sessions
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_cookie_fallback(self, session_manager):
|
||||
"""Test falling back to default cookie when no session exists."""
|
||||
# Create manager with default cookie
|
||||
manager_with_default = SessionManager(default_cookie="default_test_cookie")
|
||||
|
||||
# Get session without logging in - should return default
|
||||
session = await manager_with_default.get_session()
|
||||
|
||||
assert session is not None
|
||||
assert session.ncfa_cookie == "default_test_cookie"
|
||||
assert session.user_id == "default"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_set_default_cookie(self, session_manager):
|
||||
"""Test setting default cookie after initialization."""
|
||||
# Initially no default
|
||||
session = await session_manager.get_session()
|
||||
assert session is None
|
||||
|
||||
# Set default cookie
|
||||
await session_manager.set_default_cookie("new_default_cookie")
|
||||
|
||||
# Now should return default session
|
||||
session = await session_manager.get_session()
|
||||
assert session is not None
|
||||
assert session.ncfa_cookie == "new_default_cookie"
|
||||
|
||||
|
||||
class TestLoginErrorHandling:
|
||||
"""Tests for login error scenarios."""
|
||||
|
||||
@pytest.fixture
|
||||
def session_manager(self):
|
||||
return SessionManager()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_httpx_client(self):
|
||||
with patch("httpx.AsyncClient") as mock_client_class:
|
||||
mock_client = AsyncMock()
|
||||
mock_client.__aenter__.return_value = mock_client
|
||||
mock_client.__aexit__.return_value = None
|
||||
mock_client_class.return_value = mock_client
|
||||
yield mock_client
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_invalid_credentials(self, session_manager, mock_httpx_client):
|
||||
"""Test login with invalid credentials."""
|
||||
response = MagicMock()
|
||||
response.status_code = 401
|
||||
mock_httpx_client.post = AsyncMock(return_value=response)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid email or password"):
|
||||
await session_manager.login("wrong@example.com", "wrong_password")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_account_denied(self, session_manager, mock_httpx_client):
|
||||
"""Test login when account access is denied."""
|
||||
response = MagicMock()
|
||||
response.status_code = 403
|
||||
mock_httpx_client.post = AsyncMock(return_value=response)
|
||||
|
||||
with pytest.raises(ValueError, match="Account access denied"):
|
||||
await session_manager.login("banned@example.com", "password")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_rate_limited(self, session_manager, mock_httpx_client):
|
||||
"""Test login when rate limited."""
|
||||
response = MagicMock()
|
||||
response.status_code = 429
|
||||
mock_httpx_client.post = AsyncMock(return_value=response)
|
||||
|
||||
with pytest.raises(ValueError, match="Too many login attempts"):
|
||||
await session_manager.login("user@example.com", "password")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_server_error(self, session_manager, mock_httpx_client):
|
||||
"""Test login with server error."""
|
||||
response = MagicMock()
|
||||
response.status_code = 500
|
||||
mock_httpx_client.post = AsyncMock(return_value=response)
|
||||
|
||||
with pytest.raises(ValueError, match="Login failed: 500"):
|
||||
await session_manager.login("user@example.com", "password")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_no_cookie_received(self, session_manager, mock_httpx_client):
|
||||
"""Test login when no cookie is received."""
|
||||
login_response = MagicMock()
|
||||
login_response.status_code = 200
|
||||
login_response.cookies.jar = [] # No cookies
|
||||
login_response.headers = {} # No set-cookie header
|
||||
|
||||
mock_httpx_client.post = AsyncMock(return_value=login_response)
|
||||
|
||||
with pytest.raises(ValueError, match="No session cookie received"):
|
||||
await session_manager.login("user@example.com", "password")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_login_profile_fetch_fails(self, session_manager, mock_httpx_client):
|
||||
"""Test login when profile fetch fails after successful auth."""
|
||||
# Login succeeds
|
||||
login_response = MagicMock()
|
||||
login_response.status_code = 200
|
||||
login_response.cookies.jar = []
|
||||
mock_cookie = MagicMock()
|
||||
mock_cookie.name = "_ncfa"
|
||||
mock_cookie.value = "valid_cookie"
|
||||
login_response.cookies.jar.append(mock_cookie)
|
||||
|
||||
# Profile fetch fails
|
||||
profile_response = MagicMock()
|
||||
profile_response.status_code = 500
|
||||
|
||||
mock_httpx_client.post = AsyncMock(return_value=login_response)
|
||||
mock_httpx_client.get = AsyncMock(return_value=profile_response)
|
||||
mock_httpx_client.cookies.set = MagicMock()
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to retrieve user profile"):
|
||||
await session_manager.login("user@example.com", "password")
|
||||
|
||||
|
||||
class TestCookieValidation:
|
||||
"""Tests for cookie validation functionality."""
|
||||
|
||||
@pytest.fixture
|
||||
def session_manager(self):
|
||||
return SessionManager()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_valid_cookie(self, session_manager, mock_profile_data):
|
||||
"""Test validating a valid cookie."""
|
||||
with patch("httpx.AsyncClient") as mock_client_class:
|
||||
mock_client = AsyncMock()
|
||||
mock_client.__aenter__.return_value = mock_client
|
||||
mock_client.__aexit__.return_value = None
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
response = MagicMock()
|
||||
response.status_code = 200
|
||||
response.json.return_value = mock_profile_data
|
||||
mock_client.get = AsyncMock(return_value=response)
|
||||
mock_client.cookies.set = MagicMock()
|
||||
|
||||
result = await session_manager.validate_cookie("valid_cookie")
|
||||
|
||||
assert result is not None
|
||||
assert result["id"] == "test-user-id"
|
||||
assert result["nick"] == "TestPlayer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_invalid_cookie(self, session_manager):
|
||||
"""Test validating an invalid cookie."""
|
||||
with patch("httpx.AsyncClient") as mock_client_class:
|
||||
mock_client = AsyncMock()
|
||||
mock_client.__aenter__.return_value = mock_client
|
||||
mock_client.__aexit__.return_value = None
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
response = MagicMock()
|
||||
response.status_code = 401
|
||||
mock_client.get = AsyncMock(return_value=response)
|
||||
mock_client.cookies.set = MagicMock()
|
||||
|
||||
result = await session_manager.validate_cookie("invalid_cookie")
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_validate_cookie_network_error(self, session_manager):
|
||||
"""Test cookie validation with network error."""
|
||||
with patch("httpx.AsyncClient") as mock_client_class:
|
||||
mock_client = AsyncMock()
|
||||
mock_client.__aenter__.return_value = mock_client
|
||||
mock_client.__aexit__.return_value = None
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
mock_client.get = AsyncMock(side_effect=httpx.ConnectError("Network error"))
|
||||
mock_client.cookies.set = MagicMock()
|
||||
|
||||
result = await session_manager.validate_cookie("cookie")
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestRealAuthFlow:
|
||||
"""
|
||||
Real integration tests requiring actual GeoGuessr credentials.
|
||||
|
||||
These tests are skipped unless GEOGUESSR_NCFA_COOKIE is set and
|
||||
running with -m integration flag.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def session_manager(self):
|
||||
return SessionManager()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_cookie_validation(self, session_manager):
|
||||
"""Test validating a real cookie against the API."""
|
||||
import os
|
||||
|
||||
cookie = os.environ.get("GEOGUESSR_NCFA_COOKIE")
|
||||
if not cookie:
|
||||
pytest.skip("GEOGUESSR_NCFA_COOKIE not set")
|
||||
|
||||
result = await session_manager.validate_cookie(cookie)
|
||||
|
||||
assert result is not None
|
||||
assert "id" in result
|
||||
assert "nick" in result
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue