This commit is contained in:
Yûki VACHOT 2025-11-28 22:15:27 +01:00
parent ce5abcc217
commit cfe4a641a6
40 changed files with 1728 additions and 1445 deletions

54
src/tests/conftest.py Normal file
View file

@ -0,0 +1,54 @@
"""Shared test fixtures."""
from unittest.mock import AsyncMock
import pytest
@pytest.fixture(autouse=True)
def mock_env(monkeypatch):
"""Set up environment variables for testing."""
monkeypatch.setenv("GEOGUESSR_NCFA_COOKIE", "test_cookie_value")
@pytest.fixture
def mock_session():
"""Create a mock async HTTP session."""
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
return mock_client
@pytest.fixture
def mock_profile_data():
"""Standard profile response data."""
return {
"id": "test-user-id",
"nick": "TestPlayer",
"email": "test@example.com",
"country": "FR",
"created": "2025-01-01T00:00:00.000Z",
"isVerified": True,
"level": 50,
"rating": {
"rating": 1500,
"deviation": 100
}
}
@pytest.fixture
def mock_game_data():
"""Standard game response data."""
return {
"token": "ABC123",
"type": "standard",
"map": {"name": "World"},
"player": {
"guesses": [
{"roundScoreInPoints": 5000, "distanceInMeters": 0, "time": 10},
{"roundScoreInPoints": 4500, "distanceInMeters": 100, "time": 15},
]
},
}

View file

View file

@ -0,0 +1,16 @@
"""Integration tests for authentication."""
import pytest
from geoguessr_mcp.auth.session import SessionManager
@pytest.mark.integration
class TestAuthFlow:
"""Integration tests for authentication flow."""
@pytest.mark.asyncio
async def test_full_login_logout_cycle(self):
"""Test complete login and logout cycle."""
# This would use real API calls in a test environment
pass

View file

View file

View file

View file

@ -1,201 +0,0 @@
"""
Tests for GeoGuessr MCP Server
"""
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
import httpx
# Mock the environment variable before importing server
@pytest.fixture(autouse=True)
def mock_env(monkeypatch):
"""Set up environment variables for testing."""
monkeypatch.setenv("GEOGUESSR_NCFA_COOKIE", "test_cookie_value")
class TestProfileTools:
"""Tests for profile-related tools."""
@pytest.mark.asyncio
async def test_get_my_profile_success(self):
"""Test successful profile retrieval."""
from server import get_my_profile
mock_response = {
"id": "test-user-id",
"nick": "TestPlayer",
"country": "US",
"level": 50,
}
with patch("server.get_async_session") as mock_session:
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_http_response = MagicMock()
mock_http_response.json.return_value = mock_response
mock_http_response.raise_for_status = MagicMock()
mock_client.get = AsyncMock(return_value=mock_http_response)
mock_session.return_value = mock_client
result = await get_my_profile()
assert result["nick"] == "TestPlayer"
assert result["id"] == "test-user-id"
@pytest.mark.asyncio
async def test_get_my_stats_success(self):
"""Test successful stats retrieval."""
from server import get_my_stats
mock_response = {
"gamesPlayed": 100,
"averageScore": 4500,
"highScore": 5000,
}
with patch("server.get_async_session") as mock_session:
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_http_response = MagicMock()
mock_http_response.json.return_value = mock_response
mock_http_response.raise_for_status = MagicMock()
mock_client.get = AsyncMock(return_value=mock_http_response)
mock_session.return_value = mock_client
result = await get_my_stats()
assert result["gamesPlayed"] == 100
assert result["averageScore"] == 4500
class TestGameTools:
"""Tests for game-related tools."""
@pytest.mark.asyncio
async def test_get_game_details_success(self):
"""Test successful game details retrieval."""
from server import get_game_details
mock_response = {
"token": "ABC123",
"type": "standard",
"map": {"name": "World"},
"player": {
"guesses": [
{"roundScoreInPoints": 5000, "distanceInMeters": 0},
{"roundScoreInPoints": 4500, "distanceInMeters": 100},
]
}
}
with patch("server.get_async_session") as mock_session:
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_http_response = MagicMock()
mock_http_response.json.return_value = mock_response
mock_http_response.raise_for_status = MagicMock()
mock_client.get = AsyncMock(return_value=mock_http_response)
mock_session.return_value = mock_client
result = await get_game_details("ABC123")
assert result["token"] == "ABC123"
assert result["map"]["name"] == "World"
assert len(result["player"]["guesses"]) == 2
class TestAnalysisTools:
"""Tests for analysis tools."""
@pytest.mark.asyncio
async def test_analyze_recent_games_empty(self):
"""Test analysis with no games in feed."""
from server import analyze_recent_games
mock_feed_response = {"entries": []}
with patch("server.get_async_session") as mock_session:
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_http_response = MagicMock()
mock_http_response.json.return_value = mock_feed_response
mock_http_response.raise_for_status = MagicMock()
mock_client.get = AsyncMock(return_value=mock_http_response)
mock_session.return_value = mock_client
result = await analyze_recent_games(count=5)
assert result["games_analyzed"] == 0
assert result["total_score"] == 0
assert result["games"] == []
class TestAuthentication:
"""Tests for authentication handling."""
def test_get_ncfa_cookie_missing(self, monkeypatch):
"""Test error when cookie is not set."""
monkeypatch.delenv("GEOGUESSR_NCFA_COOKIE", raising=False)
from server import get_ncfa_cookie
with pytest.raises(ValueError, match="GEOGUESSR_NCFA_COOKIE"):
get_ncfa_cookie()
def test_get_ncfa_cookie_present(self, monkeypatch):
"""Test cookie retrieval when set."""
monkeypatch.setenv("GEOGUESSR_NCFA_COOKIE", "my_test_cookie")
from server import get_ncfa_cookie
cookie = get_ncfa_cookie()
assert cookie == "my_test_cookie"
# Integration tests (marked to skip by default)
@pytest.mark.integration
class TestIntegration:
"""Integration tests that require a real GeoGuessr cookie."""
@pytest.mark.asyncio
async def test_real_profile_fetch(self):
"""Test fetching real profile data."""
import os
if not os.environ.get("GEOGUESSR_NCFA_COOKIE") or \
os.environ.get("GEOGUESSR_NCFA_COOKIE") == "test_cookie_value":
pytest.skip("Real NCFA cookie not configured")
from server import get_my_profile
result = await get_my_profile()
assert "nick" in result
assert "id" in result
if __name__ == "__main__":
"""Run tests automatically when script is executed directly."""
import sys
# Run pytest with verbose output and show print statements
exit_code = pytest.main([
__file__,
"-v", # Verbose output
"-s", # Show print statements
"--tb=short", # Shorter traceback format
"-m", "not integration", # Skip integration tests by default
])
sys.exit(exit_code)

View file

View file

View file

View file

@ -0,0 +1,81 @@
"""Unit tests for ProfileService."""
from unittest.mock import AsyncMock, MagicMock
import pytest
from geoguessr_mcp.models.profile import UserProfile
from geoguessr_mcp.services.profile_service import ProfileService
from geoguessr_mcp.config import settings
class TestProfileService:
"""Tests for ProfileService."""
@pytest.mark.asyncio
async def test_get_profile_success(self, mock_session, mock_profile_data):
"""Test successful profile retrieval."""
# Create mock client
mock_client = MagicMock()
mock_client.base_url = settings.GEOGUESSR_BASE_URL
mock_client.get_async_session = AsyncMock(return_value=mock_session)
# Mock HTTP response
mock_response = MagicMock()
mock_response.json.return_value = mock_profile_data
mock_response.raise_for_status = MagicMock()
mock_session.get = AsyncMock(return_value=mock_response)
# Test
service = ProfileService(mock_client)
profile = await service.get_profile()
assert isinstance(profile, UserProfile)
assert profile.nick == "TestPlayer"
assert profile.id == "test-user-id"
@pytest.mark.asyncio
async def test_get_my_stats_success(self, mock_session, mock_profile_data):
"""Test successful stats retrieval."""
# Create mock client
mock_client = MagicMock()
mock_client.base_url = settings.GEOGUESSR_BASE_URL
mock_client.get_async_session = AsyncMock(return_value=mock_session)
# Mock HTTP response
mock_response = MagicMock()
mock_response.json.return_value = mock_profile_data
mock_response.raise_for_status = MagicMock()
mock_session.get = AsyncMock(return_value=mock_response)
service = ProfileService(mock_client)
profile = await service.get_stats()
assert isinstance(profile, UserProfile)
assert profile. == 100
assert result["averageScore"] == 4500
@pytest.mark.asyncio
async def test_get_extended_stats(self, mock_session):
"""Test extended stats retrieval."""
from server import get_extended_stats
extended_stats = {
"totalGames": 150,
"winRate": 0.65,
"averageTime": 180
}
with patch("server.get_async_session") as mock_get_session:
mock_http_response = MagicMock()
mock_http_response.json.return_value = extended_stats
mock_http_response.raise_for_status = MagicMock()
mock_session.get = AsyncMock(return_value=mock_http_response)
mock_get_session.return_value = mock_session
result = await get_extended_stats()
assert result["totalGames"] == 150
assert result["winRate"] == 0.65

View file

@ -0,0 +1,169 @@
"""Unit tests for session management."""
from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from geoguessr_mcp.auth.session import SessionManager, UserSession
# ============================================================================
# USER SESSION TESTS
# ============================================================================
class TestUserSession:
"""Tests for UserSession dataclass."""
def test_valid_session(self):
"""Test that a valid session is recognized as valid."""
session = UserSession(
ncfa_cookie="test_cookie",
user_id="user123",
username="TestUser",
email="test@example.com",
expires_at=datetime.now(UTC) + timedelta(days=1),
)
assert session.is_valid()
def test_expired_session(self):
"""Test that an expired session is invalid."""
session = UserSession(
ncfa_cookie="test_cookie",
user_id="user123",
username="TestUser",
email="test@example.com",
expires_at=datetime.now(UTC) - timedelta(days=1),
)
assert not session.is_valid()
def test_session_without_cookie(self):
"""Test that a session without cookie is invalid."""
session = UserSession(
ncfa_cookie="", user_id="user123", username="TestUser", email="test@example.com"
)
assert not session.is_valid()
# ============================================================================
# SESSION MANAGER TESTS
# ============================================================================
class TestSessionManager:
"""Tests for SessionManager."""
@pytest.mark.asyncio
async def test_login_success(self, mock_profile_response):
"""Test successful login flow."""
manager = SessionManager()
with patch("httpx.AsyncClient") as mock_client_class:
# Create mock client
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_client_class.return_value = mock_client
# Mock login response
login_response = MagicMock()
login_response.status_code = 200
login_response.cookies.jar = []
# Create mock cookie
mock_cookie = MagicMock()
mock_cookie.name = "_ncfa"
mock_cookie.value = "test_ncfa_cookie_value"
login_response.cookies.jar.append(mock_cookie)
# Mock profile response
profile_response = MagicMock()
profile_response.status_code = 200
profile_response.json.return_value = mock_profile_response
# Set up mock client responses
mock_client.post = AsyncMock(return_value=login_response)
mock_client.get = AsyncMock(return_value=profile_response)
mock_client.cookies.set = MagicMock()
# Perform login
session_token, session = await manager.login("test@example.com", "password123")
# Assertions
assert session_token is not None
assert len(session_token) > 0
assert session.ncfa_cookie == "test_ncfa_cookie_value"
assert session.user_id == "test-user-id"
assert session.username == "TestPlayer"
assert session.email == "test@example.com"
assert session.is_valid()
@pytest.mark.asyncio
async def test_login_invalid_credentials(self):
"""Test login with invalid credentials."""
manager = SessionManager()
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 401 response
login_response = MagicMock()
login_response.status_code = 401
mock_client.post = AsyncMock(return_value=login_response)
# Attempt login and expect error
with pytest.raises(ValueError, match="Invalid email or password"):
await manager.login("wrong@example.com", "wrong_pass")
@pytest.mark.asyncio
async def test_logout(self, mock_profile_response):
"""Test logout functionality."""
manager = SessionManager()
with patch("httpx.AsyncClient") as mock_client_class:
# Set up successful login first
mock_client = AsyncMock()
mock_client.__aenter__.return_value = mock_client
mock_client.__aexit__.return_value = None
mock_client_class.return_value = mock_client
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_response
mock_client.post = AsyncMock(return_value=login_response)
mock_client.get = AsyncMock(return_value=profile_response)
mock_client.cookies.set = MagicMock()
session_token, _ = await manager.login("test@example.com", "password")
# Now logout
result = await manager.logout(session_token)
assert result is True
# Verify session is removed
session = await manager.get_session(session_token)
assert session is None
@pytest.mark.asyncio
async def test_get_session_with_default_cookie(self):
"""Test getting session with default cookie from environment."""
manager = SessionManager()
# Should use default cookie from environment
session = await manager.get_session()
assert session is not None
assert session.ncfa_cookie == "test_cookie_value"