Fix CI/CD issues and add comprehensive tests for multi-user features
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
This commit is contained in:
parent
80ed791b01
commit
482daa73e0
14 changed files with 422 additions and 82 deletions
|
|
@ -19,7 +19,7 @@ class TestGameService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_game_details_success(
|
||||
self, game_service, mock_client, mock_game_data, mock_dynamic_response
|
||||
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)
|
||||
|
|
@ -35,7 +35,7 @@ class TestGameService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_game_details_with_session_token(
|
||||
self, game_service, mock_client, mock_game_data, mock_dynamic_response
|
||||
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)
|
||||
|
|
@ -86,7 +86,7 @@ class TestGameService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_activity_feed(
|
||||
self, game_service, mock_client, mock_activity_feed_data, mock_dynamic_response
|
||||
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)
|
||||
|
|
@ -98,7 +98,7 @@ class TestGameService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_activity_feed_pagination(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test activity feed with pagination."""
|
||||
page_2_data = {"entries": [{"type": "PlayedGame", "payload": {"gameToken": "old-game"}}]}
|
||||
|
|
@ -111,12 +111,12 @@ class TestGameService:
|
|||
|
||||
@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,
|
||||
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
|
||||
|
|
@ -133,7 +133,7 @@ class TestGameService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_games_empty_feed(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test recent games with empty activity feed."""
|
||||
mock_client.get.return_value = mock_dynamic_response({"entries": []})
|
||||
|
|
@ -144,7 +144,7 @@ class TestGameService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_recent_games_feed_failure(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
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)
|
||||
|
|
@ -155,12 +155,12 @@ class TestGameService:
|
|||
|
||||
@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,
|
||||
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 = [
|
||||
|
|
@ -175,7 +175,7 @@ class TestGameService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_season_stats_success(
|
||||
self, game_service, mock_client, mock_season_stats_data, mock_dynamic_response
|
||||
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)
|
||||
|
|
@ -200,7 +200,7 @@ class TestGameService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_daily_challenge_today(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test daily challenge retrieval for today."""
|
||||
challenge_data = {
|
||||
|
|
@ -219,7 +219,7 @@ class TestGameService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_daily_challenge_specific_day(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test daily challenge for specific day."""
|
||||
challenge_data = {
|
||||
|
|
@ -234,7 +234,7 @@ class TestGameService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_daily_challenge_failure(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
"""Test daily challenge failure."""
|
||||
mock_client.get.return_value = mock_dynamic_response(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue