From 283d7deee485f0ae9daf3843ced7af26d86b16c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Y=C3=BBki=20VACHOT?= Date: Sat, 29 Nov 2025 02:41:39 +0100 Subject: [PATCH] Add production Docker Compose setup with Nginx and SSL; enhance test fixtures and environment variables for monitoring, logging, and schema caching; and clean up unused imports. --- docker-compose.prod.yml | 71 +++++++++++++++++++++++++ docker-compose.yml | 37 ++++++------- src/geoguessr_mcp/__init__.py | 2 +- src/geoguessr_mcp/main.py | 1 - src/tests/conftest.py | 18 ++++++- src/tests/integration/test_auth_flow.py | 38 +------------ 6 files changed, 109 insertions(+), 58 deletions(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index e69de29..1e4ca3b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -0,0 +1,71 @@ +version: '3.8' + +# Production deployment with Nginx reverse proxy and SSL support + +services: + geoguessr-mcp: + build: + context: . + dockerfile: Dockerfile + container_name: geoguessr-mcp-server + restart: unless-stopped + expose: + - "8000" + environment: + - GEOGUESSR_NCFA_COOKIE=${GEOGUESSR_NCFA_COOKIE:-} + - MCP_TRANSPORT=streamable-http + - MCP_HOST=0.0.0.0 + - MCP_PORT=8000 + - MONITORING_ENABLED=true + - MONITORING_INTERVAL_HOURS=24 + - SCHEMA_CACHE_DIR=/app/data/schemas + - LOG_LEVEL=INFO + volumes: + - geoguessr-schemas:/app/data/schemas + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8000/health" ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "5" + networks: + - internal + + nginx: + image: nginx:alpine + container_name: geoguessr-mcp-nginx + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./nginx/ssl:/etc/nginx/ssl:ro + - ./nginx/logs:/var/log/nginx + depends_on: + geoguessr-mcp: + condition: service_healthy + networks: + - internal + - external + logging: + driver: "json-file" + options: + max-size: "5m" + max-file: "3" + +volumes: + geoguessr-schemas: + name: geoguessr-mcp-schemas-prod + +networks: + internal: + name: geoguessr-internal + internal: true + external: + name: geoguessr-external diff --git a/docker-compose.yml b/docker-compose.yml index b97c42e..92221cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,39 +10,40 @@ services: ports: - "${MCP_PORT:-8000}:8000" environment: - # Required: Your GeoGuessr _ncfa cookie for authentication - - GEOGUESSR_NCFA_COOKIE=${GEOGUESSR_NCFA_COOKIE} + # GeoGuessr Authentication (optional - can use login tool instead) + - GEOGUESSR_NCFA_COOKIE=${GEOGUESSR_NCFA_COOKIE:-} + # MCP Server configuration - MCP_TRANSPORT=${MCP_TRANSPORT:-streamable-http} - MCP_HOST=0.0.0.0 - MCP_PORT=8000 + + # Monitoring configuration + - MONITORING_ENABLED=${MONITORING_ENABLED:-true} + - MONITORING_INTERVAL_HOURS=${MONITORING_INTERVAL_HOURS:-24} + - SCHEMA_CACHE_DIR=/app/data/schemas + + # Logging + - LOG_LEVEL=${LOG_LEVEL:-INFO} + volumes: + # Persist schema cache between restarts + - geoguessr-schemas:/app/data/schemas healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 - start_period: 10s + start_period: 15s logging: driver: "json-file" options: max-size: "10m" max-file: "3" - # Optional: Nginx reverse proxy with SSL (recommended for production) - # Uncomment and configure if you want SSL termination - # nginx: - # image: nginx:alpine - # container_name: geoguessr-mcp-nginx - # restart: unless-stopped - # ports: - # - "80:80" - # - "443:443" - # volumes: - # - ./nginx.conf:/etc/nginx/nginx.conf:ro - # - ./ssl:/etc/nginx/ssl:ro - # depends_on: - # - geoguessr-mcp +volumes: + geoguessr-schemas: + name: geoguessr-mcp-schemas networks: default: - name: geoguessr-mcp-network \ No newline at end of file + name: geoguessr-mcp-network diff --git a/src/geoguessr_mcp/__init__.py b/src/geoguessr_mcp/__init__.py index db31696..8cf0c64 100644 --- a/src/geoguessr_mcp/__init__.py +++ b/src/geoguessr_mcp/__init__.py @@ -10,4 +10,4 @@ __author__ = "Yûki VACHOT" from .main import main, mcp -__all__ = ["mcp", "main", "__version__"] +__all__ = ["mcp", "main", "__version__", "__author__"] diff --git a/src/geoguessr_mcp/main.py b/src/geoguessr_mcp/main.py index be61040..09128cf 100644 --- a/src/geoguessr_mcp/main.py +++ b/src/geoguessr_mcp/main.py @@ -5,7 +5,6 @@ This server provides tools for analyzing GeoGuessr game statistics, with automatic API monitoring and dynamic schema adaptation. """ -import asyncio import logging import sys diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 4170568..00e0442 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -1,10 +1,11 @@ """Shared test fixtures.""" -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch import pytest from geoguessr_mcp.api.dynamic_response import DynamicResponse +from geoguessr_mcp.auth import SessionManager from geoguessr_mcp.models import RoundGuess, Game from geoguessr_mcp.services import AnalysisService, GameService, ProfileService @@ -32,6 +33,21 @@ def mock_session(): return mock_client +@pytest.fixture +def session_manager(): + return SessionManager() + + +@pytest.fixture +def 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.fixture def mock_game_service(): """Create a mock GameService.""" diff --git a/src/tests/integration/test_auth_flow.py b/src/tests/integration/test_auth_flow.py index 8ebab67..0cb3d95 100644 --- a/src/tests/integration/test_auth_flow.py +++ b/src/tests/integration/test_auth_flow.py @@ -17,21 +17,6 @@ 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.""" @@ -59,7 +44,7 @@ class TestAuthenticationFlow: # Verify session was created assert session_token is not None - assert len(session_token) > 20 # Token should be substantial + assert len(session_token) > 20 # Token should be significant assert session.ncfa_cookie == "test_cookie_value" assert session.username == "TestPlayer" assert session.user_id == "test-user-id" @@ -233,19 +218,6 @@ class TestAuthenticationFlow: 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.""" @@ -326,10 +298,6 @@ class TestLoginErrorHandling: 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.""" @@ -395,10 +363,6 @@ class TestRealAuthFlow: 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."""