Add license and protect repository #2

Merged
NyxiumYuuki merged 4 commits from claude/add-license-protection-01VCHnCLUEvqQJjEynZR5LFs into master 2025-11-29 06:30:31 +01:00
9 changed files with 100 additions and 99 deletions
Showing only changes of commit 0482fff8c5 - Show all commits

View file

@ -1,7 +1,6 @@
"""Monitoring module for API endpoint tracking and schema detection.""" """Monitoring module for API endpoint tracking and schema detection."""
from .endpoint.endpoint_monitor import (MONITORED_ENDPOINTS, EndpointMonitor, from .endpoint.endpoint_monitor import MONITORED_ENDPOINTS, EndpointMonitor, endpoint_monitor
endpoint_monitor)
from .schema.endpoint_schema import EndpointSchema from .schema.endpoint_schema import EndpointSchema
from .schema.schema_detector import SchemaDetector, SchemaField from .schema.schema_detector import SchemaDetector, SchemaField
from .schema.schema_registry import SchemaRegistry, schema_registry from .schema.schema_registry import SchemaRegistry, schema_registry

View file

@ -74,9 +74,7 @@ def register_game_tools(mcp: FastMCP, game_service: GameService):
"success": True, "success": True,
"total_entries": len(entries), "total_entries": len(entries),
"entry_types": list(categorized.keys()), "entry_types": list(categorized.keys()),
"entries_by_type": { "entries_by_type": {t: len(e) for t, e in categorized.items()},
t: len(e) for t, e in categorized.items()
},
"recent_entries": entries[:5], # First 5 for context "recent_entries": entries[:5], # First 5 for context
"available_fields": response.available_fields, "available_fields": response.available_fields,
} }

View file

@ -36,6 +36,7 @@ def register_monitoring_tools(mcp: FastMCP):
session_token = get_current_session_token() session_token = get_current_session_token()
if session_token: if session_token:
from ..auth.session import SessionManager from ..auth.session import SessionManager
session_manager = SessionManager() session_manager = SessionManager()
session = await session_manager.get_session(session_token) session = await session_manager.get_session(session_token)
if session: if session:
@ -136,14 +137,16 @@ def register_monitoring_tools(mcp: FastMCP):
previous = history[-1] if history else None previous = history[-1] if history else None
if current and previous: if current and previous:
changes.append({ changes.append(
"endpoint": endpoint, {
"current_hash": current.schema_hash, "endpoint": endpoint,
"previous_hash": previous.schema_hash, "current_hash": current.schema_hash,
"current_fields": len(current.fields), "previous_hash": previous.schema_hash,
"previous_fields": len(previous.fields), "current_fields": len(current.fields),
"changed_at": current.last_updated.isoformat(), "previous_fields": len(previous.fields),
}) "changed_at": current.last_updated.isoformat(),
}
)
return { return {
"total_changes_tracked": len(changes), "total_changes_tracked": len(changes),

View file

@ -87,7 +87,9 @@ def register_profile_tools(mcp: FastMCP, profile_service: ProfileService):
"total": len(achievements), "total": len(achievements),
"unlocked": len(unlocked), "unlocked": len(unlocked),
"locked": len(locked), "locked": len(locked),
"completion_rate": f"{len(unlocked) / len(achievements) * 100:.1f}%" if achievements else "0%", "completion_rate": (
f"{len(unlocked) / len(achievements) * 100:.1f}%" if achievements else "0%"
),
}, },
"unlocked_achievements": [ "unlocked_achievements": [
{"name": a.name, "description": a.description, "unlocked_at": a.unlocked_at} {"name": a.name, "description": a.description, "unlocked_at": a.unlocked_at}

View file

@ -1,4 +1,5 @@
"""Shared test fixtures.""" """Shared test fixtures."""
import os import os
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
@ -16,7 +17,7 @@ from geoguessr_mcp.services import AnalysisService, GameService, ProfileService
def mock_env(request, monkeypatch): def mock_env(request, monkeypatch):
"""Set up environment variables for testing.""" """Set up environment variables for testing."""
# Skip this fixture if the test has the 'real_env' marker # Skip this fixture if the test has the 'real_env' marker
if 'real_env' in request.keywords: if "real_env" in request.keywords:
yield yield
return return
@ -25,6 +26,7 @@ def mock_env(request, monkeypatch):
# Clear schema registry to avoid interference from registered schemas # Clear schema registry to avoid interference from registered schemas
from geoguessr_mcp.monitoring.schema.schema_registry import schema_registry from geoguessr_mcp.monitoring.schema.schema_registry import schema_registry
# Store original schemas # Store original schemas
original_schemas = schema_registry.schemas.copy() original_schemas = schema_registry.schemas.copy()
# Clear all schemas for testing # Clear all schemas for testing

View file

@ -304,6 +304,7 @@ class TestGeoGuessrClientIntegration:
"""Test real API call to profile endpoint.""" """Test real API call to profile endpoint."""
# This test requires GEOGUESSR_NCFA_COOKIE to be set # This test requires GEOGUESSR_NCFA_COOKIE to be set
import os import os
if not os.environ.get("GEOGUESSR_NCFA_COOKIE"): if not os.environ.get("GEOGUESSR_NCFA_COOKIE"):
pytest.skip("GEOGUESSR_NCFA_COOKIE not set") pytest.skip("GEOGUESSR_NCFA_COOKIE not set")
@ -317,6 +318,7 @@ class TestGeoGuessrClientIntegration:
"""Test real API call to stats' endpoint.""" """Test real API call to stats' endpoint."""
# This test requires GEOGUESSR_NCFA_COOKIE to be set # This test requires GEOGUESSR_NCFA_COOKIE to be set
import os import os
if not os.environ.get("GEOGUESSR_NCFA_COOKIE"): if not os.environ.get("GEOGUESSR_NCFA_COOKIE"):
pytest.skip("GEOGUESSR_NCFA_COOKIE not set") pytest.skip("GEOGUESSR_NCFA_COOKIE not set")

View file

@ -103,7 +103,9 @@ class TestAnalysisService:
games = [] games = []
for i in range(6): for i in range(6):
base_score = 15000 + (i * 2000) # Increasing scores base_score = 15000 + (i * 2000) # Increasing scores
rounds = [RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=30)] rounds = [
RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=30)
]
game = Game( game = Game(
token=f"game-{i}", token=f"game-{i}",
map_name="World", map_name="World",
@ -124,7 +126,9 @@ class TestAnalysisService:
games = [] games = []
for i in range(6): for i in range(6):
base_score = 25000 - (i * 2000) # Decreasing scores base_score = 25000 - (i * 2000) # Decreasing scores
rounds = [RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=30)] rounds = [
RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=30)
]
game = Game( game = Game(
token=f"game-{i}", token=f"game-{i}",
map_name="World", map_name="World",
@ -182,17 +186,11 @@ class TestAnalysisService:
assert all(area["score"] >= 4500 for area in result.strong_areas) assert all(area["score"] >= 4500 for area in result.strong_areas)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_analyze_recent_games( async def test_analyze_recent_games(self, analysis_service, mock_game_service, sample_games):
self, analysis_service, mock_game_service, sample_games
):
"""Test analyze_recent_games method.""" """Test analyze_recent_games method."""
mock_game_service.get_recent_games.return_value = sample_games mock_game_service.get_recent_games.return_value = sample_games
with patch.object( with patch.object(analysis_service, "analyze_games", wraps=AnalysisService.analyze_games):
analysis_service,
'analyze_games',
wraps=AnalysisService.analyze_games
):
result = await analysis_service.analyze_recent_games(count=5) result = await analysis_service.analyze_recent_games(count=5)
assert "analysis" in result assert "analysis" in result
@ -207,17 +205,20 @@ class TestAnalysisService:
"""Test analyze_recent_games with session token.""" """Test analyze_recent_games with session token."""
mock_game_service.get_recent_games.return_value = sample_games mock_game_service.get_recent_games.return_value = sample_games
await analysis_service.analyze_recent_games( await analysis_service.analyze_recent_games(count=10, session_token="test_token")
count=10,
session_token="test_token"
)
mock_game_service.get_recent_games.assert_called_once_with(10, "test_token") mock_game_service.get_recent_games.assert_called_once_with(10, "test_token")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_performance_summary( async def test_get_performance_summary(
self, analysis_service, mock_game_service, mock_profile_service, self,
mock_client, sample_games, mock_season_stats_data, mock_dynamic_response analysis_service,
mock_game_service,
mock_profile_service,
mock_client,
sample_games,
mock_season_stats_data,
mock_dynamic_response,
): ):
"""Test comprehensive performance summary.""" """Test comprehensive performance summary."""
mock_profile_service.get_comprehensive_profile.return_value = { mock_profile_service.get_comprehensive_profile.return_value = {
@ -227,6 +228,7 @@ class TestAnalysisService:
mock_season_response = mock_dynamic_response(mock_season_stats_data) mock_season_response = mock_dynamic_response(mock_season_stats_data)
from geoguessr_mcp.models import SeasonStats from geoguessr_mcp.models import SeasonStats
mock_season_stats = SeasonStats.from_api_response(mock_season_stats_data) 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_season_stats.return_value = (mock_season_stats, mock_season_response)
mock_game_service.get_recent_games.return_value = sample_games[:3] mock_game_service.get_recent_games.return_value = sample_games[:3]
@ -267,7 +269,14 @@ class TestAnalysisService:
for i in range(5) for i in range(5)
] ]
games = [ games = [
Game(token="g1", map_name="World", mode="standard", total_score=15000, rounds=rounds, finished=True) Game(
token="g1",
map_name="World",
mode="standard",
total_score=15000,
rounds=rounds,
finished=True,
)
for _ in range(5) for _ in range(5)
] ]
mock_game_service.get_recent_games.return_value = games mock_game_service.get_recent_games.return_value = games
@ -289,7 +298,14 @@ class TestAnalysisService:
for i in range(5) for i in range(5)
] ]
games = [ games = [
Game(token="g1", map_name="World", mode="standard", total_score=17500, rounds=rounds, finished=True) Game(
token="g1",
map_name="World",
mode="standard",
total_score=17500,
rounds=rounds,
finished=True,
)
for _ in range(5) for _ in range(5)
] ]
mock_game_service.get_recent_games.return_value = games mock_game_service.get_recent_games.return_value = games
@ -308,7 +324,9 @@ class TestAnalysisService:
games = [] games = []
for i in range(6): for i in range(6):
base_score = 25000 - (i * 3000) base_score = 25000 - (i * 3000)
rounds = [RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=45)] rounds = [
RoundGuess(round_number=1, score=base_score, distance_meters=100, time_seconds=45)
]
game = Game( game = Game(
token=f"game-{i}", token=f"game-{i}",
map_name="World", map_name="World",

View file

@ -40,32 +40,23 @@ class TestGameService:
"""Test game details with explicit session token.""" """Test game details with explicit session token."""
mock_client.get.return_value = mock_dynamic_response(mock_game_data) mock_client.get.return_value = mock_dynamic_response(mock_game_data)
game, response = await game_service.get_game_details( game, response = await game_service.get_game_details("ABC123", session_token="test_token")
"ABC123",
session_token="test_token"
)
call_args = mock_client.get.call_args call_args = mock_client.get.call_args
assert call_args[0][1] == "test_token" assert call_args[0][1] == "test_token"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_game_details_failure( async def test_get_game_details_failure(self, game_service, mock_client, mock_dynamic_response):
self, game_service, mock_client, mock_dynamic_response
):
"""Test game details retrieval failure.""" """Test game details retrieval failure."""
mock_client.get.return_value = mock_dynamic_response( mock_client.get.return_value = mock_dynamic_response(
{"error": "Game not found"}, {"error": "Game not found"}, success=False, status_code=404
success=False,
status_code=404
) )
with pytest.raises(ValueError, match="Failed to get game details"): with pytest.raises(ValueError, match="Failed to get game details"):
await game_service.get_game_details("INVALID") await game_service.get_game_details("INVALID")
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_unfinished_games( async def test_get_unfinished_games(self, game_service, mock_client, mock_dynamic_response):
self, game_service, mock_client, mock_dynamic_response
):
"""Test unfinished games retrieval.""" """Test unfinished games retrieval."""
unfinished_data = [ unfinished_data = [
{"token": "game-1", "map": {"name": "World"}}, {"token": "game-1", "map": {"name": "World"}},
@ -79,9 +70,7 @@ class TestGameService:
assert len(response.data) == 2 assert len(response.data) == 2
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_streak_game( async def test_get_streak_game(self, game_service, mock_client, mock_dynamic_response):
self, game_service, mock_client, mock_dynamic_response
):
"""Test streak game retrieval.""" """Test streak game retrieval."""
streak_data = { streak_data = {
"token": "streak-123", "token": "streak-123",
@ -122,7 +111,12 @@ class TestGameService:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_recent_games_success( 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.""" """Test recent games retrieval."""
# First call returns activity feed, subsequent calls return game details # First call returns activity feed, subsequent calls return game details
@ -153,10 +147,7 @@ class TestGameService:
self, game_service, mock_client, mock_dynamic_response self, game_service, mock_client, mock_dynamic_response
): ):
"""Test recent games when feed fails.""" """Test recent games when feed fails."""
mock_client.get.return_value = mock_dynamic_response( mock_client.get.return_value = mock_dynamic_response({"error": "Failed"}, success=False)
{"error": "Failed"},
success=False
)
games = await game_service.get_recent_games(count=5) games = await game_service.get_recent_games(count=5)
@ -164,7 +155,12 @@ class TestGameService:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_recent_games_skips_failed_game_fetch( 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.""" """Test that failed individual game fetches are skipped."""
mock_client.get.side_effect = [ mock_client.get.side_effect = [
@ -193,14 +189,10 @@ class TestGameService:
assert stats.division == "Gold" assert stats.division == "Gold"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_season_stats_failure( async def test_get_season_stats_failure(self, game_service, mock_client, mock_dynamic_response):
self, game_service, mock_client, mock_dynamic_response
):
"""Test season stats failure.""" """Test season stats failure."""
mock_client.get.return_value = mock_dynamic_response( mock_client.get.return_value = mock_dynamic_response(
{"error": "No active season"}, {"error": "No active season"}, success=False, status_code=404
success=False,
status_code=404
) )
with pytest.raises(ValueError, match="Failed to get season stats"): with pytest.raises(ValueError, match="Failed to get season stats"):
@ -246,18 +238,14 @@ class TestGameService:
): ):
"""Test daily challenge failure.""" """Test daily challenge failure."""
mock_client.get.return_value = mock_dynamic_response( mock_client.get.return_value = mock_dynamic_response(
{"error": "Challenge not found"}, {"error": "Challenge not found"}, success=False, status_code=404
success=False,
status_code=404
) )
with pytest.raises(ValueError, match="Failed to get daily challenge"): with pytest.raises(ValueError, match="Failed to get daily challenge"):
await game_service.get_daily_challenge() await game_service.get_daily_challenge()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_battle_royale( async def test_get_battle_royale(self, game_service, mock_client, mock_dynamic_response):
self, game_service, mock_client, mock_dynamic_response
):
"""Test battle royale game retrieval.""" """Test battle royale game retrieval."""
br_data = { br_data = {
"gameId": "br-123", "gameId": "br-123",
@ -272,9 +260,7 @@ class TestGameService:
assert response.data["players"] == 10 assert response.data["players"] == 10
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_duel( async def test_get_duel(self, game_service, mock_client, mock_dynamic_response):
self, game_service, mock_client, mock_dynamic_response
):
"""Test duel game retrieval.""" """Test duel game retrieval."""
duel_data = { duel_data = {
"duelId": "duel-456", "duelId": "duel-456",
@ -289,9 +275,7 @@ class TestGameService:
assert response.data["player1"]["score"] == 5000 assert response.data["player1"]["score"] == 5000
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_tournaments( async def test_get_tournaments(self, game_service, mock_client, mock_dynamic_response):
self, game_service, mock_client, mock_dynamic_response
):
"""Test tournaments retrieval.""" """Test tournaments retrieval."""
tournaments_data = [ tournaments_data = [
{"id": "t1", "name": "Weekly Tournament", "status": "active"}, {"id": "t1", "name": "Weekly Tournament", "status": "active"},

View file

@ -46,14 +46,10 @@ class TestProfileService:
assert call_args[0][1] == "test_token" assert call_args[0][1] == "test_token"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_profile_failure( async def test_get_profile_failure(self, profile_service, mock_client, mock_dynamic_response):
self, profile_service, mock_client, mock_dynamic_response
):
"""Test profile retrieval failure.""" """Test profile retrieval failure."""
mock_client.get.return_value = mock_dynamic_response( mock_client.get.return_value = mock_dynamic_response(
{"error": "Unauthorized"}, {"error": "Unauthorized"}, success=False, status_code=401
success=False,
status_code=401
) )
with pytest.raises(ValueError, match="Failed to get profile"): with pytest.raises(ValueError, match="Failed to get profile"):
@ -75,23 +71,17 @@ class TestProfileService:
assert stats.win_rate == 0.65 assert stats.win_rate == 0.65
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_stats_failure( async def test_get_stats_failure(self, profile_service, mock_client, mock_dynamic_response):
self, profile_service, mock_client, mock_dynamic_response
):
"""Test stats retrieval failure.""" """Test stats retrieval failure."""
mock_client.get.return_value = mock_dynamic_response( mock_client.get.return_value = mock_dynamic_response(
{"error": "Server error"}, {"error": "Server error"}, success=False, status_code=500
success=False,
status_code=500
) )
with pytest.raises(ValueError, match="Failed to get stats"): with pytest.raises(ValueError, match="Failed to get stats"):
await profile_service.get_stats() await profile_service.get_stats()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_extended_stats( async def test_get_extended_stats(self, profile_service, mock_client, mock_dynamic_response):
self, profile_service, mock_client, mock_dynamic_response
):
"""Test extended stats retrieval.""" """Test extended stats retrieval."""
extended_data = { extended_data = {
"totalDistance": 1500000, "totalDistance": 1500000,
@ -154,9 +144,7 @@ class TestProfileService:
assert achievements[0].name == "Winner" assert achievements[0].name == "Winner"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_public_profile( async def test_get_public_profile(self, profile_service, mock_client, mock_dynamic_response):
self, profile_service, mock_client, mock_dynamic_response
):
"""Test public profile retrieval.""" """Test public profile retrieval."""
public_profile_data = { public_profile_data = {
"id": "other-user-123", "id": "other-user-123",
@ -172,9 +160,7 @@ class TestProfileService:
assert profile.nick == "OtherPlayer" assert profile.nick == "OtherPlayer"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_user_maps( async def test_get_user_maps(self, profile_service, mock_client, mock_dynamic_response):
self, profile_service, mock_client, mock_dynamic_response
):
"""Test user maps retrieval.""" """Test user maps retrieval."""
maps_data = [ maps_data = [
{"id": "map-1", "name": "My Custom Map"}, {"id": "map-1", "name": "My Custom Map"},
@ -189,7 +175,12 @@ class TestProfileService:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_get_comprehensive_profile_success( async def test_get_comprehensive_profile_success(
self, profile_service, mock_client, mock_profile_data, mock_stats_data, mock_dynamic_response self,
profile_service,
mock_client,
mock_profile_data,
mock_stats_data,
mock_dynamic_response,
): ):
"""Test comprehensive profile aggregation.""" """Test comprehensive profile aggregation."""
# Setup mock responses for each call # Setup mock responses for each call
@ -197,9 +188,11 @@ class TestProfileService:
mock_dynamic_response(mock_profile_data), # profile mock_dynamic_response(mock_profile_data), # profile
mock_dynamic_response(mock_stats_data), # stats mock_dynamic_response(mock_stats_data), # stats
mock_dynamic_response({"totalDistance": 1000}), # extended stats mock_dynamic_response({"totalDistance": 1000}), # extended stats
mock_dynamic_response([ # achievements mock_dynamic_response(
{"id": "ach-1", "name": "Test", "unlocked": True, "unlockedAt": "2024-01-01"}, [ # achievements
]), {"id": "ach-1", "name": "Test", "unlocked": True, "unlockedAt": "2024-01-01"},
]
),
] ]
result = await profile_service.get_comprehensive_profile() result = await profile_service.get_comprehensive_profile()