This commit fixes three critical issues identified in CI/CD and adds comprehensive test coverage for the new multi-user functionality. ## Fixes ### 1. FastMCP Middleware Registration Error **Problem**: `AttributeError: 'FastMCP' object has no attribute 'app'` **Solution**: Implemented robust middleware registration that: - Tries multiple possible locations where FastMCP might store the app - Gracefully handles cases where app isn't immediately available - Wraps the run() method to defer middleware addition if needed - Attempts: _transport.app, sse.app, http_server.app, _app, _asgi_app - Falls back gracefully with warning if middleware can't be added **Files Changed**: - src/geoguessr_mcp/main.py: Added smart middleware registration logic ### 2. Test Permission Errors **Problem**: `PermissionError: [Errno 13] Permission denied: '/app'` Schema registry tried to create /app/data/schemas in CI without permission **Solution**: Made schema cache directory creation fault-tolerant: - Catches PermissionError and OSError when creating cache directory - Falls back to temporary directory (tempfile.mkdtemp) if permission denied - Logs clear warning messages about fallback behavior - Tests can now run in restricted environments **Files Changed**: - src/geoguessr_mcp/monitoring/schema/schema_registry.py: Added fallback logic ### 3. Black Formatting Issues **Problem**: 10 files needed reformatting **Solution**: Ran `black src/ --line-length 100` on all source files **Files Formatted**: - src/geoguessr_mcp/config.py - src/geoguessr_mcp/api/dynamic_response.py - src/geoguessr_mcp/middleware/auth.py - src/geoguessr_mcp/main.py - src/geoguessr_mcp/auth/multi_user_session.py - src/geoguessr_mcp/tools/auth_tools.py - src/tests/integration/test_auth_flow.py - src/tests/unit/services/*.py (3 files) ## New Tests Added comprehensive test coverage for multi-user features: ### test_user_context.py - Tests UserContext creation with/without sessions - Tests authentication status checking - Tests session expiration handling - Tests string representation - Tests API key hashing for anonymous users - Tests consistency of anonymous user IDs ### test_multi_user_session.py - Tests MultiUserSessionManager initialization - Tests session manager creation per API key - Tests session manager reuse for same API key - Tests isolation between different users - Tests auth status reporting - Tests context creation and retrieval ### test_request_context.py - Tests context variable get/set operations - Tests require_user_context() error handling - Tests context isolation between requests - Tests context updates and clearing - Tests None handling ## Code Quality All changes pass: - ✅ Python syntax checks (py_compile) - ✅ Black formatting (line-length 100) - ✅ Test structure validation - ✅ Import resolution ## CI/CD Impact These fixes should resolve: - ❌ Test execution failures (permission errors) - ❌ Black formatting check failures - ❌ Runtime errors when starting server with auth enabled Tests can now run in CI environment without requiring: - Root permissions - /app directory access - Pre-created cache directories
229 lines
8.4 KiB
Python
229 lines
8.4 KiB
Python
"""
|
|
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"])
|