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
37
.env.example
37
.env.example
|
|
@ -2,9 +2,10 @@
|
|||
# Copy this file to .env and fill in your values
|
||||
|
||||
# =============================================================================
|
||||
# REQUIRED: GeoGuessr Authentication
|
||||
# OPTIONAL: GeoGuessr Authentication
|
||||
# =============================================================================
|
||||
# Your GeoGuessr _ncfa cookie for API authentication
|
||||
# If not set, users must authenticate using the login tool
|
||||
#
|
||||
# How to get your _ncfa cookie:
|
||||
# 1. Log in to GeoGuessr in your browser
|
||||
|
|
@ -15,9 +16,8 @@
|
|||
# 6. Copy its value and paste it below
|
||||
#
|
||||
# IMPORTANT: Keep this secret! Anyone with this cookie can access your account.
|
||||
# The cookie typically expires after some time, so you may need to update it periodically.
|
||||
|
||||
GEOGUESSR_NCFA_COOKIE=your_ncfa_cookie_value_here
|
||||
GEOGUESSR_NCFA_COOKIE=
|
||||
|
||||
# =============================================================================
|
||||
# MCP Server Configuration
|
||||
|
|
@ -25,12 +25,35 @@ GEOGUESSR_NCFA_COOKIE=your_ncfa_cookie_value_here
|
|||
# Transport protocol: "streamable-http" (recommended) or "sse" (legacy)
|
||||
MCP_TRANSPORT=streamable-http
|
||||
|
||||
# Host to bind to (0.0.0.0 for all interfaces)
|
||||
MCP_HOST=0.0.0.0
|
||||
|
||||
# Port to expose the server on
|
||||
MCP_PORT=8000
|
||||
|
||||
# =============================================================================
|
||||
# Optional: API Key Authentication (recommended for production)
|
||||
# API Monitoring Configuration
|
||||
# =============================================================================
|
||||
# If you want to require API key authentication for accessing the MCP server
|
||||
# Uncomment and set a secure API key
|
||||
# API_KEYS=your-secure-api-key-here,another-api-key-if-needed
|
||||
# Enable automatic API endpoint monitoring
|
||||
MONITORING_ENABLED=true
|
||||
|
||||
# How often to check API endpoints (in hours)
|
||||
MONITORING_INTERVAL_HOURS=24
|
||||
|
||||
# Directory to store schema cache (persisted between restarts)
|
||||
SCHEMA_CACHE_DIR=/app/data/schemas
|
||||
|
||||
# =============================================================================
|
||||
# Logging Configuration
|
||||
# =============================================================================
|
||||
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# =============================================================================
|
||||
# Request Configuration
|
||||
# =============================================================================
|
||||
# Request timeout in seconds
|
||||
REQUEST_TIMEOUT=30.0
|
||||
|
||||
# Maximum retry attempts for failed requests
|
||||
MAX_RETRIES=3
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
"""Shared test fixtures."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from geoguessr_mcp.api.dynamic_response import DynamicResponse
|
||||
from geoguessr_mcp.models import RoundGuess, Game
|
||||
from geoguessr_mcp.services import AnalysisService, GameService, ProfileService
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_env(monkeypatch):
|
||||
|
|
@ -11,6 +15,14 @@ def mock_env(monkeypatch):
|
|||
monkeypatch.setenv("GEOGUESSR_NCFA_COOKIE", "test_cookie_value")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client():
|
||||
"""Create a mock GeoGuessrClient."""
|
||||
client = MagicMock()
|
||||
client.get = AsyncMock()
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session():
|
||||
"""Create a mock async HTTP session."""
|
||||
|
|
@ -20,6 +32,61 @@ def mock_session():
|
|||
return mock_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_game_service():
|
||||
"""Create a mock GameService."""
|
||||
service = MagicMock()
|
||||
service.get_recent_games = AsyncMock()
|
||||
service.get_season_stats = AsyncMock()
|
||||
return service
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_profile_service():
|
||||
"""Create a mock ProfileService."""
|
||||
service = MagicMock()
|
||||
service.get_comprehensive_profile = AsyncMock()
|
||||
return service
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def analysis_service(mock_client, mock_game_service, mock_profile_service):
|
||||
"""Create AnalysisService with mocked dependencies."""
|
||||
return AnalysisService(
|
||||
mock_client,
|
||||
game_service=mock_game_service,
|
||||
profile_service=mock_profile_service,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def game_service(mock_client):
|
||||
"""Create GameService with mocked client."""
|
||||
return GameService(mock_client)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def profile_service(mock_client):
|
||||
"""Create ProfileService with mocked client."""
|
||||
return ProfileService(mock_client)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_dynamic_response():
|
||||
"""Create a mock DynamicResponse factory."""
|
||||
|
||||
def create_response(data, success=True, status_code=200):
|
||||
response = MagicMock(spec=DynamicResponse)
|
||||
response.data = data
|
||||
response.is_success = success
|
||||
response.status_code = status_code
|
||||
response.available_fields = list(data.keys()) if isinstance(data, dict) else []
|
||||
response.summarize.return_value = {"data_summary": data}
|
||||
return response
|
||||
|
||||
return create_response
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_profile_data():
|
||||
"""Standard profile response data."""
|
||||
|
|
@ -81,3 +148,53 @@ def mock_season_stats_data():
|
|||
"wins": 30,
|
||||
"tier": "Gold",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_activity_feed_data():
|
||||
"""Activity feed response data."""
|
||||
return {
|
||||
"entries": [
|
||||
{
|
||||
"type": "PlayedGame",
|
||||
"payload": {"gameToken": "game-token-1"},
|
||||
"timestamp": "2024-01-15T10:00:00Z",
|
||||
},
|
||||
{
|
||||
"type": "PlayedGame",
|
||||
"payload": {"gameToken": "game-token-2"},
|
||||
"timestamp": "2024-01-14T10:00:00Z",
|
||||
},
|
||||
{
|
||||
"type": "Achievement",
|
||||
"payload": {"achievementId": "ach-1"},
|
||||
"timestamp": "2024-01-13T10:00:00Z",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_games():
|
||||
"""Create sample Game objects for testing."""
|
||||
games = []
|
||||
for i in range(5):
|
||||
rounds = [
|
||||
RoundGuess(
|
||||
round_number=j + 1,
|
||||
score=5000 - (i * 200) - (j * 100), # Varying scores
|
||||
distance_meters=100.0 * (i + 1),
|
||||
time_seconds=30 + i * 5,
|
||||
)
|
||||
for j in range(5)
|
||||
]
|
||||
game = Game(
|
||||
token=f"game-{i}",
|
||||
map_name="World",
|
||||
mode="standard",
|
||||
total_score=sum(r.score for r in rounds),
|
||||
rounds=rounds,
|
||||
finished=True,
|
||||
)
|
||||
games.append(game)
|
||||
return games
|
||||
|
|
|
|||
|
|
@ -1 +1,351 @@
|
|||
# TODO
|
||||
"""
|
||||
Integration tests for the GeoGuessr API client.
|
||||
|
||||
These tests verify the API client functionality with mocked HTTP responses,
|
||||
simulating real API interactions without making actual network calls.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from geoguessr_mcp.api import DynamicResponse, GeoGuessrClient, EndpointInfo, Endpoints
|
||||
from geoguessr_mcp.auth.session import SessionManager, UserSession
|
||||
from geoguessr_mcp.config import settings
|
||||
|
||||
|
||||
class TestDynamicResponse:
|
||||
"""Tests for DynamicResponse wrapper class."""
|
||||
|
||||
def test_is_success_200(self):
|
||||
"""Test success detection for 200 status code."""
|
||||
response = DynamicResponse(
|
||||
data={"id": "123"},
|
||||
endpoint="/v3/profiles",
|
||||
status_code=200,
|
||||
response_time_ms=150.0,
|
||||
)
|
||||
assert response.is_success is True
|
||||
|
||||
def test_is_success_201(self):
|
||||
"""Test success detection for 201 status code."""
|
||||
response = DynamicResponse(
|
||||
data={"created": True},
|
||||
endpoint="/v3/games",
|
||||
status_code=201,
|
||||
response_time_ms=200.0,
|
||||
)
|
||||
assert response.is_success is True
|
||||
|
||||
def test_is_success_failure(self):
|
||||
"""Test success detection for error status codes."""
|
||||
response = DynamicResponse(
|
||||
data={"error": "Not found"},
|
||||
endpoint="/v3/profiles",
|
||||
status_code=404,
|
||||
response_time_ms=100.0,
|
||||
)
|
||||
assert response.is_success is False
|
||||
|
||||
def test_available_fields_dict(self):
|
||||
"""Test available_fields with dict data."""
|
||||
response = DynamicResponse(
|
||||
data={"id": "123", "name": "Test", "score": 5000},
|
||||
endpoint="/v3/test",
|
||||
status_code=200,
|
||||
response_time_ms=100.0,
|
||||
)
|
||||
assert response.available_fields == ["id", "name", "score"]
|
||||
|
||||
def test_available_fields_non_dict(self):
|
||||
"""Test available_fields with non-dict data."""
|
||||
response = DynamicResponse(
|
||||
data=["item1", "item2"],
|
||||
endpoint="/v3/test",
|
||||
status_code=200,
|
||||
response_time_ms=100.0,
|
||||
)
|
||||
assert response.available_fields == []
|
||||
|
||||
def test_get_field_simple(self):
|
||||
"""Test getting a simple field."""
|
||||
response = DynamicResponse(
|
||||
data={"id": "123", "name": "Test"},
|
||||
endpoint="/v3/test",
|
||||
status_code=200,
|
||||
response_time_ms=100.0,
|
||||
)
|
||||
assert response.get_field("id") == "123"
|
||||
assert response.get_field("name") == "Test"
|
||||
|
||||
def test_get_field_nested(self):
|
||||
"""Test getting a nested field with dot notation."""
|
||||
response = DynamicResponse(
|
||||
data={
|
||||
"user": {
|
||||
"profile": {
|
||||
"name": "TestUser",
|
||||
"level": 50,
|
||||
}
|
||||
}
|
||||
},
|
||||
endpoint="/v3/test",
|
||||
status_code=200,
|
||||
response_time_ms=100.0,
|
||||
)
|
||||
assert response.get_field("user.profile.name") == "TestUser"
|
||||
assert response.get_field("user.profile.level") == 50
|
||||
|
||||
def test_get_field_missing_with_default(self):
|
||||
"""Test getting missing field returns default."""
|
||||
response = DynamicResponse(
|
||||
data={"id": "123"},
|
||||
endpoint="/v3/test",
|
||||
status_code=200,
|
||||
response_time_ms=100.0,
|
||||
)
|
||||
assert response.get_field("missing", default="default_value") == "default_value"
|
||||
assert response.get_field("nested.missing", default=None) is None
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test converting response to dict."""
|
||||
response = DynamicResponse(
|
||||
data={"id": "123"},
|
||||
endpoint="/v3/profiles",
|
||||
status_code=200,
|
||||
response_time_ms=150.5,
|
||||
)
|
||||
result = response.to_dict()
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["status_code"] == 200
|
||||
assert result["endpoint"] == "/v3/profiles"
|
||||
assert result["response_time_ms"] == 150.5
|
||||
assert result["data"] == {"id": "123"}
|
||||
assert "available_fields" in result
|
||||
|
||||
def test_summarize(self):
|
||||
"""Test response summarization."""
|
||||
response = DynamicResponse(
|
||||
data={
|
||||
"items": [
|
||||
{"id": 1, "name": "Item 1"},
|
||||
{"id": 2, "name": "Item 2"},
|
||||
{"id": 3, "name": "Item 3"},
|
||||
{"id": 4, "name": "Item 4"},
|
||||
],
|
||||
"total": 4,
|
||||
},
|
||||
endpoint="/v3/test",
|
||||
status_code=200,
|
||||
response_time_ms=100.0,
|
||||
)
|
||||
summary = response.summarize(max_depth=1)
|
||||
|
||||
assert summary["endpoint"] == "/v3/test"
|
||||
assert summary["status"] == "success"
|
||||
assert "data_summary" in summary
|
||||
|
||||
def test_summarize_long_string(self):
|
||||
"""Test that long strings are truncated in summaries."""
|
||||
long_text = "x" * 200
|
||||
response = DynamicResponse(
|
||||
data={"description": long_text},
|
||||
endpoint="/v3/test",
|
||||
status_code=200,
|
||||
response_time_ms=100.0,
|
||||
)
|
||||
summary = response.summarize(max_depth=2)
|
||||
|
||||
# The long string should be truncated
|
||||
assert len(summary["data_summary"]["description"]) <= 103 # 100 + "..."
|
||||
|
||||
|
||||
class TestGeoGuessrClient:
|
||||
"""Tests for GeoGuessrClient."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session_manager(self):
|
||||
"""Create a mock session manager."""
|
||||
manager = MagicMock(spec=SessionManager)
|
||||
manager.get_session = AsyncMock(
|
||||
return_value=UserSession(
|
||||
ncfa_cookie="test_cookie",
|
||||
user_id="test-user",
|
||||
username="TestUser",
|
||||
email="test@example.com",
|
||||
)
|
||||
)
|
||||
return manager
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, mock_session_manager):
|
||||
"""Create a GeoGuessrClient with mocked session manager."""
|
||||
return GeoGuessrClient(mock_session_manager)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_authenticated_client(self, client, mock_session_manager):
|
||||
"""Test getting authenticated HTTP client."""
|
||||
http_client = await client._get_authenticated_client()
|
||||
|
||||
assert http_client is not None
|
||||
mock_session_manager.get_session.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_authenticated_client_no_session(self, mock_session_manager):
|
||||
"""Test error when no session is available."""
|
||||
mock_session_manager.get_session = AsyncMock(return_value=None)
|
||||
client = GeoGuessrClient(mock_session_manager)
|
||||
|
||||
with pytest.raises(ValueError, match="No valid session available"):
|
||||
await client._get_authenticated_client()
|
||||
|
||||
def test_get_base_url_main_api(self, client):
|
||||
"""Test base URL selection for main API."""
|
||||
endpoint = EndpointInfo(path="/v3/profiles", use_game_server=False)
|
||||
url = client._get_base_url(endpoint)
|
||||
assert url == settings.GEOGUESSR_API_URL
|
||||
|
||||
def test_get_base_url_game_server(self, client):
|
||||
"""Test base URL selection for game server."""
|
||||
endpoint = EndpointInfo(path="/tournaments", use_game_server=True)
|
||||
url = client._get_base_url(endpoint)
|
||||
assert url == settings.GAME_SERVER_URL
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_request_success(self, client):
|
||||
"""Test successful GET request."""
|
||||
with patch.object(client, "_get_authenticated_client") as mock_auth:
|
||||
mock_http_client = AsyncMock()
|
||||
mock_http_client.__aenter__.return_value = mock_http_client
|
||||
mock_http_client.__aexit__.return_value = None
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"id": "123", "nick": "TestUser"}
|
||||
mock_http_client.get = AsyncMock(return_value=mock_response)
|
||||
|
||||
mock_auth.return_value = mock_http_client
|
||||
|
||||
response = await client.get(Endpoints.PROFILES.GET_PROFILE)
|
||||
|
||||
assert response.is_success
|
||||
assert response.data["id"] == "123"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_request_failure(self, client):
|
||||
"""Test failed GET request."""
|
||||
with patch.object(client, "_get_authenticated_client") as mock_auth:
|
||||
mock_http_client = AsyncMock()
|
||||
mock_http_client.__aenter__.return_value = mock_http_client
|
||||
mock_http_client.__aexit__.return_value = None
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.text = "Not found"
|
||||
mock_http_client.get = AsyncMock(return_value=mock_response)
|
||||
|
||||
mock_auth.return_value = mock_http_client
|
||||
|
||||
response = await client.get(Endpoints.PROFILES.GET_PROFILE)
|
||||
|
||||
assert not response.is_success
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_post_request(self, client):
|
||||
"""Test POST request."""
|
||||
with patch.object(client, "_get_authenticated_client") as mock_auth:
|
||||
mock_http_client = AsyncMock()
|
||||
mock_http_client.__aenter__.return_value = mock_http_client
|
||||
mock_http_client.__aexit__.return_value = None
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"success": True}
|
||||
mock_http_client.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
mock_auth.return_value = mock_http_client
|
||||
|
||||
endpoint = EndpointInfo(path="/v3/test", method="POST")
|
||||
response = await client.post(endpoint, json_data={"data": "test"})
|
||||
|
||||
assert response.is_success
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_raw_request(self, client):
|
||||
"""Test raw GET request to arbitrary path."""
|
||||
with patch.object(client, "_get_authenticated_client") as mock_auth:
|
||||
mock_http_client = AsyncMock()
|
||||
mock_http_client.__aenter__.return_value = mock_http_client
|
||||
mock_http_client.__aexit__.return_value = None
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"discovered": True}
|
||||
mock_http_client.get = AsyncMock(return_value=mock_response)
|
||||
|
||||
mock_auth.return_value = mock_http_client
|
||||
|
||||
response = await client.get_raw("/v3/unknown-endpoint")
|
||||
|
||||
assert response.is_success
|
||||
assert response.endpoint == "/v3/unknown-endpoint"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_handling(self, client):
|
||||
"""Test handling of timeout exceptions."""
|
||||
with patch.object(client, "_get_authenticated_client") as mock_auth:
|
||||
mock_http_client = AsyncMock()
|
||||
mock_http_client.__aenter__.return_value = mock_http_client
|
||||
mock_http_client.__aexit__.return_value = None
|
||||
mock_http_client.get = AsyncMock(side_effect=httpx.TimeoutException("Timeout"))
|
||||
|
||||
mock_auth.return_value = mock_http_client
|
||||
|
||||
with pytest.raises(httpx.TimeoutException):
|
||||
await client.get(Endpoints.PROFILES.GET_PROFILE)
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestGeoGuessrClientIntegration:
|
||||
"""
|
||||
Integration tests that would make real API calls.
|
||||
|
||||
These tests are marked with @pytest.mark.integration and should only
|
||||
be run when explicitly requested (pytest -m integration) with a valid
|
||||
authentication cookie.
|
||||
"""
|
||||
|
||||
@pytest.fixture
|
||||
def real_client(self):
|
||||
"""Create a real client with environment authentication."""
|
||||
session_manager = SessionManager()
|
||||
return GeoGuessrClient(session_manager)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_profile_endpoint(self, real_client):
|
||||
"""Test real API call to profile endpoint."""
|
||||
# This test requires GEOGUESSR_NCFA_COOKIE to be set
|
||||
import os
|
||||
if not os.environ.get("GEOGUESSR_NCFA_COOKIE"):
|
||||
pytest.skip("GEOGUESSR_NCFA_COOKIE not set")
|
||||
|
||||
response = await real_client.get(Endpoints.PROFILES.GET_PROFILE)
|
||||
|
||||
assert response.is_success
|
||||
assert "id" in response.available_fields or "nick" in response.available_fields
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_stats_endpoint(self, real_client):
|
||||
"""Test real API call to stats' endpoint."""
|
||||
import os
|
||||
if not os.environ.get("GEOGUESSR_NCFA_COOKIE"):
|
||||
pytest.skip("GEOGUESSR_NCFA_COOKIE not set")
|
||||
|
||||
response = await real_client.get(Endpoints.PROFILES.GET_STATS)
|
||||
|
||||
assert response.is_success
|
||||
# Stats' endpoint should have some numeric data
|
||||
assert len(response.available_fields) > 0
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ instances when interacting with API responses or performing operations like
|
|||
serialization. The tests cover both standard and edge cases.
|
||||
"""
|
||||
|
||||
from geoguessr_mcp.models.Game import Game
|
||||
from geoguessr_mcp.models.RoundGuess import RoundGuess
|
||||
from geoguessr_mcp.models.game import Game
|
||||
from geoguessr_mcp.models.round_guess import RoundGuess
|
||||
|
||||
|
||||
class TestGame:
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ objects, computation of schema hashes, and parsing of specific data formats
|
|||
such as datetime strings, URLs, and UUIDs.
|
||||
"""
|
||||
|
||||
from geoguessr_mcp.monitoring.schema.EndpointSchema import SchemaField
|
||||
from geoguessr_mcp.monitoring.schema.SchemaDetector import SchemaDetector
|
||||
from geoguessr_mcp.monitoring.schema.endpoint_schema import SchemaField
|
||||
from geoguessr_mcp.monitoring.schema.schema_detector import SchemaDetector
|
||||
|
||||
|
||||
class TestSchemaDetector:
|
||||
|
|
|
|||
|
|
@ -1 +1,355 @@
|
|||
# TODO
|
||||
"""
|
||||
This module contains unit tests for classes and services provided in the
|
||||
`geoguessr_mcp` library. It includes test cases for analyzing games,
|
||||
evaluating statistical trends, and extracting detailed insights from
|
||||
game performance data.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from geoguessr_mcp.models import Game, RoundGuess
|
||||
from geoguessr_mcp.services.analysis_service import AnalysisService, GameAnalysis
|
||||
|
||||
|
||||
class TestGameAnalysis:
|
||||
"""Tests for GameAnalysis dataclass."""
|
||||
|
||||
def test_default_values(self):
|
||||
"""Test GameAnalysis default values."""
|
||||
analysis = GameAnalysis()
|
||||
|
||||
assert analysis.games_analyzed == 0
|
||||
assert analysis.total_score == 0
|
||||
assert analysis.average_score == 0.0
|
||||
assert analysis.perfect_round_percentage == 0.0
|
||||
assert analysis.score_trend == "stable"
|
||||
assert analysis.weak_areas == []
|
||||
assert analysis.strong_areas == []
|
||||
|
||||
def test_to_dict(self):
|
||||
"""Test GameAnalysis to_dict method."""
|
||||
analysis = GameAnalysis(
|
||||
games_analyzed=10,
|
||||
total_score=200000,
|
||||
average_score=20000.5,
|
||||
total_rounds=50,
|
||||
perfect_rounds=15,
|
||||
perfect_round_percentage=30.0,
|
||||
average_distance_meters=500.123,
|
||||
average_time_seconds=45.678,
|
||||
best_game_score=25000,
|
||||
worst_game_score=15000,
|
||||
score_trend="improving",
|
||||
)
|
||||
|
||||
result = analysis.to_dict()
|
||||
|
||||
assert result["games_analyzed"] == 10
|
||||
assert result["average_score"] == 20000.5
|
||||
assert result["perfect_round_percentage"] == 30.0
|
||||
assert result["score_trend"] == "improving"
|
||||
|
||||
|
||||
class TestAnalysisService:
|
||||
"""Tests for AnalysisService."""
|
||||
|
||||
def test_analyze_games_empty(self):
|
||||
"""Test analyzing empty game list."""
|
||||
result = AnalysisService.analyze_games([])
|
||||
|
||||
assert result.games_analyzed == 0
|
||||
assert result.total_score == 0
|
||||
assert result.average_score == 0.0
|
||||
|
||||
def test_analyze_games_single_game(self):
|
||||
"""Test analyzing single game."""
|
||||
rounds = [
|
||||
RoundGuess(round_number=1, score=5000, distance_meters=0, time_seconds=20),
|
||||
RoundGuess(round_number=2, score=4500, distance_meters=100, time_seconds=25),
|
||||
RoundGuess(round_number=3, score=4000, distance_meters=200, time_seconds=30),
|
||||
]
|
||||
game = Game(
|
||||
token="test-game",
|
||||
map_name="World",
|
||||
mode="standard",
|
||||
total_score=13500,
|
||||
rounds=rounds,
|
||||
finished=True,
|
||||
)
|
||||
|
||||
result = AnalysisService.analyze_games([game])
|
||||
|
||||
assert result.games_analyzed == 1
|
||||
assert result.total_score == 13500
|
||||
assert result.average_score == 13500
|
||||
assert result.total_rounds == 3
|
||||
assert result.perfect_rounds == 1
|
||||
assert result.perfect_round_percentage == pytest.approx(33.33, rel=0.01)
|
||||
|
||||
def test_analyze_games_multiple_games(self, sample_games):
|
||||
"""Test analyzing multiple games."""
|
||||
result = AnalysisService.analyze_games(sample_games)
|
||||
|
||||
assert result.games_analyzed == 5
|
||||
assert result.total_rounds == 25
|
||||
assert result.average_score == result.total_score / 5
|
||||
assert result.best_game_score >= result.worst_game_score
|
||||
|
||||
def test_analyze_games_trend_improving(self):
|
||||
"""Test score trend detection - improving."""
|
||||
# Create games with improving scores
|
||||
games = []
|
||||
for i in range(6):
|
||||
base_score = 15000 + (i * 2000) # Increasing scores
|
||||
rounds = [RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=30)]
|
||||
game = Game(
|
||||
token=f"game-{i}",
|
||||
map_name="World",
|
||||
mode="standard",
|
||||
total_score=base_score,
|
||||
rounds=rounds,
|
||||
finished=True,
|
||||
)
|
||||
games.append(game)
|
||||
|
||||
result = AnalysisService.analyze_games(games)
|
||||
|
||||
assert result.score_trend == "improving"
|
||||
|
||||
def test_analyze_games_trend_declining(self):
|
||||
"""Test score trend detection - declining."""
|
||||
# Create games with declining scores
|
||||
games = []
|
||||
for i in range(6):
|
||||
base_score = 25000 - (i * 2000) # Decreasing scores
|
||||
rounds = [RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=30)]
|
||||
game = Game(
|
||||
token=f"game-{i}",
|
||||
map_name="World",
|
||||
mode="standard",
|
||||
total_score=base_score,
|
||||
rounds=rounds,
|
||||
finished=True,
|
||||
)
|
||||
games.append(game)
|
||||
|
||||
result = AnalysisService.analyze_games(games)
|
||||
|
||||
assert result.score_trend == "declining"
|
||||
|
||||
def test_analyze_games_weak_areas(self):
|
||||
"""Test weak areas' identification."""
|
||||
rounds = [
|
||||
RoundGuess(round_number=1, score=5000, distance_meters=0, time_seconds=20),
|
||||
RoundGuess(round_number=2, score=1500, distance_meters=5000, time_seconds=60), # Weak
|
||||
RoundGuess(round_number=3, score=1000, distance_meters=8000, time_seconds=90), # Weak
|
||||
]
|
||||
game = Game(
|
||||
token="test-game",
|
||||
map_name="World",
|
||||
mode="standard",
|
||||
total_score=7500,
|
||||
rounds=rounds,
|
||||
finished=True,
|
||||
)
|
||||
|
||||
result = AnalysisService.analyze_games([game])
|
||||
|
||||
assert len(result.weak_areas) == 2
|
||||
assert all(area["score"] < 2000 for area in result.weak_areas)
|
||||
|
||||
def test_analyze_games_strong_areas(self):
|
||||
"""Test strong areas' identification."""
|
||||
rounds = [
|
||||
RoundGuess(round_number=1, score=5000, distance_meters=0, time_seconds=20),
|
||||
RoundGuess(round_number=2, score=4800, distance_meters=50, time_seconds=25),
|
||||
RoundGuess(round_number=3, score=2000, distance_meters=500, time_seconds=30),
|
||||
]
|
||||
game = Game(
|
||||
token="test-game",
|
||||
map_name="World",
|
||||
mode="standard",
|
||||
total_score=11800,
|
||||
rounds=rounds,
|
||||
finished=True,
|
||||
)
|
||||
|
||||
result = AnalysisService.analyze_games([game])
|
||||
|
||||
assert len(result.strong_areas) == 2
|
||||
assert all(area["score"] >= 4500 for area in result.strong_areas)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_recent_games(
|
||||
self, analysis_service, mock_game_service, sample_games
|
||||
):
|
||||
"""Test analyze_recent_games method."""
|
||||
mock_game_service.get_recent_games.return_value = sample_games
|
||||
|
||||
with patch.object(
|
||||
analysis_service,
|
||||
'analyze_games',
|
||||
wraps=AnalysisService.analyze_games
|
||||
):
|
||||
result = await analysis_service.analyze_recent_games(count=5)
|
||||
|
||||
assert "analysis" in result
|
||||
assert "games" in result
|
||||
assert result["analysis"]["games_analyzed"] == 5
|
||||
mock_game_service.get_recent_games.assert_called_once_with(5, None)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_recent_games_with_session(
|
||||
self, analysis_service, mock_game_service, sample_games
|
||||
):
|
||||
"""Test analyze_recent_games with session token."""
|
||||
mock_game_service.get_recent_games.return_value = sample_games
|
||||
|
||||
await analysis_service.analyze_recent_games(
|
||||
count=10,
|
||||
session_token="test_token"
|
||||
)
|
||||
|
||||
mock_game_service.get_recent_games.assert_called_once_with(10, "test_token")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_performance_summary(
|
||||
self, analysis_service, mock_game_service, mock_profile_service,
|
||||
mock_client, sample_games, mock_season_stats_data, mock_dynamic_response
|
||||
):
|
||||
"""Test comprehensive performance summary."""
|
||||
mock_profile_service.get_comprehensive_profile.return_value = {
|
||||
"profile": {"nick": "TestPlayer"},
|
||||
"stats": {"games_played": 100},
|
||||
}
|
||||
|
||||
mock_season_response = mock_dynamic_response(mock_season_stats_data)
|
||||
from geoguessr_mcp.models import SeasonStats
|
||||
mock_season_stats = SeasonStats.from_api_response(mock_season_stats_data)
|
||||
mock_game_service.get_season_stats.return_value = (mock_season_stats, mock_season_response)
|
||||
mock_game_service.get_recent_games.return_value = sample_games[:3]
|
||||
|
||||
mock_client.get.return_value = mock_dynamic_response({"progress": 0.5})
|
||||
|
||||
result = await analysis_service.get_performance_summary()
|
||||
|
||||
assert result["profile"] is not None
|
||||
assert result["season"] is not None
|
||||
assert result["recent_games_analysis"] is not None
|
||||
assert "api_status" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_performance_summary_with_errors(
|
||||
self, analysis_service, mock_game_service, mock_profile_service, mock_client
|
||||
):
|
||||
"""Test performance summary handles errors gracefully."""
|
||||
mock_profile_service.get_comprehensive_profile.side_effect = Exception("Profile error")
|
||||
mock_game_service.get_season_stats.side_effect = Exception("Season error")
|
||||
mock_game_service.get_recent_games.return_value = []
|
||||
mock_client.get.side_effect = Exception("API error")
|
||||
|
||||
result = await analysis_service.get_performance_summary()
|
||||
|
||||
assert len(result["errors"]) > 0
|
||||
assert result["profile"] is None
|
||||
assert result["season"] is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_strategy_recommendations_low_perfect_rate(
|
||||
self, analysis_service, mock_game_service
|
||||
):
|
||||
"""Test strategy recommendations for low perfect round rate."""
|
||||
# Create games with no perfect rounds
|
||||
rounds = [
|
||||
RoundGuess(round_number=i, score=3000, distance_meters=500, time_seconds=30)
|
||||
for i in range(5)
|
||||
]
|
||||
games = [
|
||||
Game(token="g1", map_name="World", mode="standard", total_score=15000, rounds=rounds, finished=True)
|
||||
for _ in range(5)
|
||||
]
|
||||
mock_game_service.get_recent_games.return_value = games
|
||||
|
||||
result = await analysis_service.get_strategy_recommendations()
|
||||
|
||||
assert len(result["recommendations"]) > 0
|
||||
accuracy_recs = [r for r in result["recommendations"] if r["category"] == "accuracy"]
|
||||
assert len(accuracy_recs) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_strategy_recommendations_fast_play(
|
||||
self, analysis_service, mock_game_service
|
||||
):
|
||||
"""Test strategy recommendations for fast play style."""
|
||||
# Create games with very short time
|
||||
rounds = [
|
||||
RoundGuess(round_number=i, score=3500, distance_meters=300, time_seconds=15)
|
||||
for i in range(5)
|
||||
]
|
||||
games = [
|
||||
Game(token="g1", map_name="World", mode="standard", total_score=17500, rounds=rounds, finished=True)
|
||||
for _ in range(5)
|
||||
]
|
||||
mock_game_service.get_recent_games.return_value = games
|
||||
|
||||
result = await analysis_service.get_strategy_recommendations()
|
||||
|
||||
time_recs = [r for r in result["recommendations"] if r["category"] == "time_management"]
|
||||
assert len(time_recs) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_strategy_recommendations_declining_trend(
|
||||
self, analysis_service, mock_game_service
|
||||
):
|
||||
"""Test strategy recommendations for declining performance."""
|
||||
# Create games with declining scores
|
||||
games = []
|
||||
for i in range(6):
|
||||
base_score = 25000 - (i * 3000)
|
||||
rounds = [RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=45)]
|
||||
game = Game(
|
||||
token=f"game-{i}",
|
||||
map_name="World",
|
||||
mode="standard",
|
||||
total_score=base_score,
|
||||
rounds=rounds,
|
||||
finished=True,
|
||||
)
|
||||
games.append(game)
|
||||
mock_game_service.get_recent_games.return_value = games
|
||||
|
||||
result = await analysis_service.get_strategy_recommendations()
|
||||
|
||||
consistency_recs = [r for r in result["recommendations"] if r["category"] == "consistency"]
|
||||
assert len(consistency_recs) > 0
|
||||
assert result["analysis_summary"]["trend"] == "declining"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_strategy_recommendations_many_weak_areas(
|
||||
self, analysis_service, mock_game_service
|
||||
):
|
||||
"""Test strategy recommendations for many weak rounds."""
|
||||
# Create games with many low scores
|
||||
games = []
|
||||
for i in range(4):
|
||||
rounds = [
|
||||
RoundGuess(round_number=j, score=1500, distance_meters=5000, time_seconds=60)
|
||||
for j in range(5)
|
||||
]
|
||||
game = Game(
|
||||
token=f"game-{i}",
|
||||
map_name="World",
|
||||
mode="standard",
|
||||
total_score=7500,
|
||||
rounds=rounds,
|
||||
finished=True,
|
||||
)
|
||||
games.append(game)
|
||||
mock_game_service.get_recent_games.return_value = games
|
||||
|
||||
result = await analysis_service.get_strategy_recommendations()
|
||||
|
||||
practice_recs = [r for r in result["recommendations"] if r["category"] == "practice"]
|
||||
assert len(practice_recs) > 0
|
||||
|
|
|
|||
|
|
@ -1 +1,305 @@
|
|||
# TODO
|
||||
"""
|
||||
A module for testing the functionalities of `GameService` in the GeoGuessr
|
||||
ecosystem.
|
||||
|
||||
This module contains a collection of test cases designed to validate
|
||||
the correctness, edge cases, and error handling for various
|
||||
asynchronous methods in the `GameService` class. These include
|
||||
retrieving game details, unfinished games, streak games, activity
|
||||
feeds, recent games, season statistics, and daily challenges.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from geoguessr_mcp.models import Game, SeasonStats, DailyChallenge
|
||||
|
||||
|
||||
class TestGameService:
|
||||
"""Tests for GameService."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_game_details_success(
|
||||
self, game_service, mock_client, mock_game_data, mock_dynamic_response
|
||||
):
|
||||
"""Test successful game details retrieval."""
|
||||
mock_client.get.return_value = mock_dynamic_response(mock_game_data)
|
||||
|
||||
game, response = await game_service.get_game_details("ABC123")
|
||||
|
||||
assert isinstance(game, Game)
|
||||
assert game.token == "ABC123"
|
||||
assert game.map_name == "World"
|
||||
assert game.mode == "standard"
|
||||
assert len(game.rounds) == 5
|
||||
assert game.total_score == 23200 # Sum of all round scores
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_game_details_with_session_token(
|
||||
self, game_service, mock_client, mock_game_data, mock_dynamic_response
|
||||
):
|
||||
"""Test game details with explicit session token."""
|
||||
mock_client.get.return_value = mock_dynamic_response(mock_game_data)
|
||||
|
||||
game, response = await game_service.get_game_details(
|
||||
"ABC123",
|
||||
session_token="test_token"
|
||||
)
|
||||
|
||||
call_args = mock_client.get.call_args
|
||||
assert call_args[0][1] == "test_token"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_game_details_failure(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test game details retrieval failure."""
|
||||
mock_client.get.return_value = mock_dynamic_response(
|
||||
{"error": "Game not found"},
|
||||
success=False,
|
||||
status_code=404
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to get game details"):
|
||||
await game_service.get_game_details("INVALID")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_unfinished_games(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test unfinished games retrieval."""
|
||||
unfinished_data = [
|
||||
{"token": "game-1", "map": {"name": "World"}},
|
||||
{"token": "game-2", "map": {"name": "Europe"}},
|
||||
]
|
||||
mock_client.get.return_value = mock_dynamic_response(unfinished_data)
|
||||
|
||||
response = await game_service.get_unfinished_games()
|
||||
|
||||
assert response.is_success
|
||||
assert len(response.data) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_streak_game(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test streak game retrieval."""
|
||||
streak_data = {
|
||||
"token": "streak-123",
|
||||
"currentStreak": 15,
|
||||
"bestStreak": 25,
|
||||
}
|
||||
mock_client.get.return_value = mock_dynamic_response(streak_data)
|
||||
|
||||
response = await game_service.get_streak_game("streak-123")
|
||||
|
||||
assert response.is_success
|
||||
assert response.data["currentStreak"] == 15
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_activity_feed(
|
||||
self, game_service, mock_client, mock_activity_feed_data, mock_dynamic_response
|
||||
):
|
||||
"""Test activity feed retrieval."""
|
||||
mock_client.get.return_value = mock_dynamic_response(mock_activity_feed_data)
|
||||
|
||||
response = await game_service.get_activity_feed(count=10, page=0)
|
||||
|
||||
assert response.is_success
|
||||
assert len(response.data["entries"]) == 3
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_activity_feed_pagination(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test activity feed with pagination."""
|
||||
page_2_data = {"entries": [{"type": "PlayedGame", "payload": {"gameToken": "old-game"}}]}
|
||||
mock_client.get.return_value = mock_dynamic_response(page_2_data)
|
||||
|
||||
response = await game_service.get_activity_feed(count=10, page=1)
|
||||
|
||||
assert response.is_success
|
||||
mock_client.get.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_games_success(
|
||||
self, game_service, mock_client, mock_activity_feed_data, mock_game_data, mock_dynamic_response
|
||||
):
|
||||
"""Test recent games retrieval."""
|
||||
# First call returns activity feed, subsequent calls return game details
|
||||
mock_client.get.side_effect = [
|
||||
mock_dynamic_response(mock_activity_feed_data),
|
||||
mock_dynamic_response(mock_game_data),
|
||||
mock_dynamic_response({**mock_game_data, "token": "game-token-2"}),
|
||||
]
|
||||
|
||||
games = await game_service.get_recent_games(count=2)
|
||||
|
||||
assert len(games) == 2
|
||||
assert all(isinstance(g, Game) for g in games)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_games_empty_feed(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test recent games with empty activity feed."""
|
||||
mock_client.get.return_value = mock_dynamic_response({"entries": []})
|
||||
|
||||
games = await game_service.get_recent_games(count=5)
|
||||
|
||||
assert len(games) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_games_feed_failure(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test recent games when feed fails."""
|
||||
mock_client.get.return_value = mock_dynamic_response(
|
||||
{"error": "Failed"},
|
||||
success=False
|
||||
)
|
||||
|
||||
games = await game_service.get_recent_games(count=5)
|
||||
|
||||
assert len(games) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_games_skips_failed_game_fetch(
|
||||
self, game_service, mock_client, mock_activity_feed_data, mock_game_data, mock_dynamic_response
|
||||
):
|
||||
"""Test that failed individual game fetches are skipped."""
|
||||
mock_client.get.side_effect = [
|
||||
mock_dynamic_response(mock_activity_feed_data),
|
||||
Exception("Game fetch failed"), # First game fails
|
||||
mock_dynamic_response(mock_game_data), # Second game succeeds
|
||||
]
|
||||
|
||||
games = await game_service.get_recent_games(count=2)
|
||||
|
||||
assert len(games) == 1
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_season_stats_success(
|
||||
self, game_service, mock_client, mock_season_stats_data, mock_dynamic_response
|
||||
):
|
||||
"""Test season stats retrieval."""
|
||||
mock_client.get.return_value = mock_dynamic_response(mock_season_stats_data)
|
||||
|
||||
stats, response = await game_service.get_season_stats()
|
||||
|
||||
assert isinstance(stats, SeasonStats)
|
||||
assert stats.season_id == "season-2024-1"
|
||||
assert stats.rank == 150
|
||||
assert stats.rating == 1850
|
||||
assert stats.division == "Gold"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_season_stats_failure(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test season stats failure."""
|
||||
mock_client.get.return_value = mock_dynamic_response(
|
||||
{"error": "No active season"},
|
||||
success=False,
|
||||
status_code=404
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to get season stats"):
|
||||
await game_service.get_season_stats()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_daily_challenge_today(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test daily challenge retrieval for today."""
|
||||
challenge_data = {
|
||||
"token": "daily-2024-01-15",
|
||||
"map": {"name": "World"},
|
||||
"date": "2024-01-15",
|
||||
"timeLimit": 180,
|
||||
}
|
||||
mock_client.get.return_value = mock_dynamic_response(challenge_data)
|
||||
|
||||
challenge, response = await game_service.get_daily_challenge()
|
||||
|
||||
assert isinstance(challenge, DailyChallenge)
|
||||
assert challenge.token == "daily-2024-01-15"
|
||||
assert challenge.time_limit == 180
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_daily_challenge_specific_day(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test daily challenge for specific day."""
|
||||
challenge_data = {
|
||||
"token": "daily-2024-01-10",
|
||||
"date": "2024-01-10",
|
||||
}
|
||||
mock_client.get.return_value = mock_dynamic_response(challenge_data)
|
||||
|
||||
challenge, response = await game_service.get_daily_challenge(day="2024-01-10")
|
||||
|
||||
assert challenge.date == "2024-01-10"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_daily_challenge_failure(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test daily challenge failure."""
|
||||
mock_client.get.return_value = mock_dynamic_response(
|
||||
{"error": "Challenge not found"},
|
||||
success=False,
|
||||
status_code=404
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to get daily challenge"):
|
||||
await game_service.get_daily_challenge()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_battle_royale(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test battle royale game retrieval."""
|
||||
br_data = {
|
||||
"gameId": "br-123",
|
||||
"players": 10,
|
||||
"status": "in_progress",
|
||||
}
|
||||
mock_client.get.return_value = mock_dynamic_response(br_data)
|
||||
|
||||
response = await game_service.get_battle_royale("br-123")
|
||||
|
||||
assert response.is_success
|
||||
assert response.data["players"] == 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_duel(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test duel game retrieval."""
|
||||
duel_data = {
|
||||
"duelId": "duel-456",
|
||||
"player1": {"id": "p1", "score": 5000},
|
||||
"player2": {"id": "p2", "score": 4500},
|
||||
}
|
||||
mock_client.get.return_value = mock_dynamic_response(duel_data)
|
||||
|
||||
response = await game_service.get_duel("duel-456")
|
||||
|
||||
assert response.is_success
|
||||
assert response.data["player1"]["score"] == 5000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tournaments(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test tournaments retrieval."""
|
||||
tournaments_data = [
|
||||
{"id": "t1", "name": "Weekly Tournament", "status": "active"},
|
||||
{"id": "t2", "name": "Monthly Cup", "status": "upcoming"},
|
||||
]
|
||||
mock_client.get.return_value = mock_dynamic_response(tournaments_data)
|
||||
|
||||
response = await game_service.get_tournaments()
|
||||
|
||||
assert response.is_success
|
||||
assert len(response.data) == 2
|
||||
|
|
|
|||
|
|
@ -1 +1,236 @@
|
|||
# TODO
|
||||
"""
|
||||
Tests for the ProfileService behaviors and functionality.
|
||||
|
||||
This module includes tests for various aspects of ProfileService such
|
||||
as profile management, statistics retrieval, achievement
|
||||
retrieval, and handling edge cases or failures during service
|
||||
operations.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from geoguessr_mcp.models import UserProfile, UserStats, Achievement
|
||||
|
||||
|
||||
class TestProfileService:
|
||||
"""Tests for ProfileService."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_profile_success(
|
||||
self, profile_service, mock_client, mock_profile_data, mock_dynamic_response
|
||||
):
|
||||
"""Test successful profile retrieval."""
|
||||
mock_client.get.return_value = mock_dynamic_response(mock_profile_data)
|
||||
|
||||
profile, response = await profile_service.get_profile()
|
||||
|
||||
assert isinstance(profile, UserProfile)
|
||||
assert profile.id == "test-user-id"
|
||||
assert profile.nick == "TestPlayer"
|
||||
assert profile.email == "test@example.com"
|
||||
assert profile.country == "FR"
|
||||
assert profile.level == 50
|
||||
mock_client.get.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_profile_with_session_token(
|
||||
self, profile_service, mock_client, mock_profile_data, mock_dynamic_response
|
||||
):
|
||||
"""Test profile retrieval with explicit session token."""
|
||||
mock_client.get.return_value = mock_dynamic_response(mock_profile_data)
|
||||
|
||||
await profile_service.get_profile(session_token="test_token")
|
||||
|
||||
mock_client.get.assert_called_once()
|
||||
call_args = mock_client.get.call_args
|
||||
assert call_args[0][1] == "test_token"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_profile_failure(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test profile retrieval failure."""
|
||||
mock_client.get.return_value = mock_dynamic_response(
|
||||
{"error": "Unauthorized"},
|
||||
success=False,
|
||||
status_code=401
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to get profile"):
|
||||
await profile_service.get_profile()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_stats_success(
|
||||
self, profile_service, mock_client, mock_stats_data, mock_dynamic_response
|
||||
):
|
||||
"""Test successful stats retrieval."""
|
||||
mock_client.get.return_value = mock_dynamic_response(mock_stats_data)
|
||||
|
||||
stats, response = await profile_service.get_stats()
|
||||
|
||||
assert isinstance(stats, UserStats)
|
||||
assert stats.games_played == 100
|
||||
assert stats.rounds_played == 500
|
||||
assert stats.total_score == 2250000
|
||||
assert stats.win_rate == 0.65
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_stats_failure(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test stats retrieval failure."""
|
||||
mock_client.get.return_value = mock_dynamic_response(
|
||||
{"error": "Server error"},
|
||||
success=False,
|
||||
status_code=500
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to get stats"):
|
||||
await profile_service.get_stats()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_extended_stats(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test extended stats retrieval."""
|
||||
extended_data = {
|
||||
"totalDistance": 1500000,
|
||||
"averageTime": 45.5,
|
||||
"favoriteMap": "World",
|
||||
}
|
||||
mock_client.get.return_value = mock_dynamic_response(extended_data)
|
||||
|
||||
response = await profile_service.get_extended_stats()
|
||||
|
||||
assert response.is_success
|
||||
assert response.data["totalDistance"] == 1500000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_achievements_list_format(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test achievements retrieval with list format response."""
|
||||
achievements_data = [
|
||||
{
|
||||
"id": "ach-1",
|
||||
"name": "First Win",
|
||||
"description": "Win your first game",
|
||||
"unlocked": True,
|
||||
"unlockedAt": "2024-01-15T00:00:00Z",
|
||||
},
|
||||
{
|
||||
"id": "ach-2",
|
||||
"name": "Explorer",
|
||||
"description": "Play 100 games",
|
||||
"unlocked": False,
|
||||
"progress": 0.45,
|
||||
},
|
||||
]
|
||||
mock_client.get.return_value = mock_dynamic_response(achievements_data)
|
||||
|
||||
achievements, response = await profile_service.get_achievements()
|
||||
|
||||
assert len(achievements) == 2
|
||||
assert all(isinstance(a, Achievement) for a in achievements)
|
||||
assert achievements[0].name == "First Win"
|
||||
assert achievements[0].unlocked is True
|
||||
assert achievements[1].unlocked is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_achievements_dict_format(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test achievements retrieval with dict format response."""
|
||||
achievements_data = {
|
||||
"achievements": [
|
||||
{"id": "ach-1", "name": "Winner", "unlocked": True},
|
||||
]
|
||||
}
|
||||
mock_client.get.return_value = mock_dynamic_response(achievements_data)
|
||||
|
||||
achievements, response = await profile_service.get_achievements()
|
||||
|
||||
assert len(achievements) == 1
|
||||
assert achievements[0].name == "Winner"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_public_profile(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test public profile retrieval."""
|
||||
public_profile_data = {
|
||||
"id": "other-user-123",
|
||||
"nick": "OtherPlayer",
|
||||
"country": "US",
|
||||
"level": 75,
|
||||
}
|
||||
mock_client.get.return_value = mock_dynamic_response(public_profile_data)
|
||||
|
||||
profile, response = await profile_service.get_public_profile("other-user-123")
|
||||
|
||||
assert profile.id == "other-user-123"
|
||||
assert profile.nick == "OtherPlayer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_maps(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test user maps retrieval."""
|
||||
maps_data = [
|
||||
{"id": "map-1", "name": "My Custom Map"},
|
||||
{"id": "map-2", "name": "Another Map"},
|
||||
]
|
||||
mock_client.get.return_value = mock_dynamic_response(maps_data)
|
||||
|
||||
response = await profile_service.get_user_maps()
|
||||
|
||||
assert response.is_success
|
||||
assert len(response.data) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_comprehensive_profile_success(
|
||||
self, profile_service, mock_client, mock_profile_data, mock_stats_data, mock_dynamic_response
|
||||
):
|
||||
"""Test comprehensive profile aggregation."""
|
||||
# Setup mock responses for each call
|
||||
mock_client.get.side_effect = [
|
||||
mock_dynamic_response(mock_profile_data), # profile
|
||||
mock_dynamic_response(mock_stats_data), # stats
|
||||
mock_dynamic_response({"totalDistance": 1000}), # extended stats
|
||||
mock_dynamic_response([ # achievements
|
||||
{"id": "ach-1", "name": "Test", "unlocked": True, "unlockedAt": "2024-01-01"},
|
||||
]),
|
||||
]
|
||||
|
||||
result = await profile_service.get_comprehensive_profile()
|
||||
|
||||
assert result["profile"] is not None
|
||||
assert result["profile"]["nick"] == "TestPlayer"
|
||||
assert result["stats"] is not None
|
||||
assert result["stats"]["games_played"] == 100
|
||||
assert result["extended_stats"] is not None
|
||||
assert result["achievements"]["total"] == 1
|
||||
assert result["achievements"]["unlocked"] == 1
|
||||
assert len(result["errors"]) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_comprehensive_profile_partial_failure(
|
||||
self, profile_service, mock_client, mock_profile_data, mock_dynamic_response
|
||||
):
|
||||
"""Test comprehensive profile with some endpoints failing."""
|
||||
mock_client.get.side_effect = [
|
||||
mock_dynamic_response(mock_profile_data), # profile succeeds
|
||||
Exception("Stats endpoint down"), # stats fails
|
||||
mock_dynamic_response({"data": "test"}), # extended stats succeed
|
||||
Exception("Achievements unavailable"), # achievements fails
|
||||
]
|
||||
|
||||
result = await profile_service.get_comprehensive_profile()
|
||||
|
||||
assert result["profile"] is not None
|
||||
assert result["stats"] is None
|
||||
assert result["extended_stats"] is not None
|
||||
assert result["achievements"] is None
|
||||
assert len(result["errors"]) == 2
|
||||
assert any("Stats" in e for e in result["errors"])
|
||||
assert any("Achievements" in e for e in result["errors"])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue