Add license and protect repository #2
9 changed files with 100 additions and 99 deletions
|
|
@ -1,7 +1,6 @@
|
|||
"""Monitoring module for API endpoint tracking and schema detection."""
|
||||
|
||||
from .endpoint.endpoint_monitor import (MONITORED_ENDPOINTS, EndpointMonitor,
|
||||
endpoint_monitor)
|
||||
from .endpoint.endpoint_monitor import MONITORED_ENDPOINTS, EndpointMonitor, endpoint_monitor
|
||||
from .schema.endpoint_schema import EndpointSchema
|
||||
from .schema.schema_detector import SchemaDetector, SchemaField
|
||||
from .schema.schema_registry import SchemaRegistry, schema_registry
|
||||
|
|
|
|||
|
|
@ -74,9 +74,7 @@ def register_game_tools(mcp: FastMCP, game_service: GameService):
|
|||
"success": True,
|
||||
"total_entries": len(entries),
|
||||
"entry_types": list(categorized.keys()),
|
||||
"entries_by_type": {
|
||||
t: len(e) for t, e in categorized.items()
|
||||
},
|
||||
"entries_by_type": {t: len(e) for t, e in categorized.items()},
|
||||
"recent_entries": entries[:5], # First 5 for context
|
||||
"available_fields": response.available_fields,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ def register_monitoring_tools(mcp: FastMCP):
|
|||
session_token = get_current_session_token()
|
||||
if session_token:
|
||||
from ..auth.session import SessionManager
|
||||
|
||||
session_manager = SessionManager()
|
||||
session = await session_manager.get_session(session_token)
|
||||
if session:
|
||||
|
|
@ -136,14 +137,16 @@ def register_monitoring_tools(mcp: FastMCP):
|
|||
previous = history[-1] if history else None
|
||||
|
||||
if current and previous:
|
||||
changes.append({
|
||||
"endpoint": endpoint,
|
||||
"current_hash": current.schema_hash,
|
||||
"previous_hash": previous.schema_hash,
|
||||
"current_fields": len(current.fields),
|
||||
"previous_fields": len(previous.fields),
|
||||
"changed_at": current.last_updated.isoformat(),
|
||||
})
|
||||
changes.append(
|
||||
{
|
||||
"endpoint": endpoint,
|
||||
"current_hash": current.schema_hash,
|
||||
"previous_hash": previous.schema_hash,
|
||||
"current_fields": len(current.fields),
|
||||
"previous_fields": len(previous.fields),
|
||||
"changed_at": current.last_updated.isoformat(),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"total_changes_tracked": len(changes),
|
||||
|
|
|
|||
|
|
@ -87,7 +87,9 @@ def register_profile_tools(mcp: FastMCP, profile_service: ProfileService):
|
|||
"total": len(achievements),
|
||||
"unlocked": len(unlocked),
|
||||
"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": [
|
||||
{"name": a.name, "description": a.description, "unlocked_at": a.unlocked_at}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Shared test fixtures."""
|
||||
|
||||
import os
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
|
@ -16,7 +17,7 @@ from geoguessr_mcp.services import AnalysisService, GameService, ProfileService
|
|||
def mock_env(request, monkeypatch):
|
||||
"""Set up environment variables for testing."""
|
||||
# Skip this fixture if the test has the 'real_env' marker
|
||||
if 'real_env' in request.keywords:
|
||||
if "real_env" in request.keywords:
|
||||
yield
|
||||
return
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ def mock_env(request, monkeypatch):
|
|||
|
||||
# Clear schema registry to avoid interference from registered schemas
|
||||
from geoguessr_mcp.monitoring.schema.schema_registry import schema_registry
|
||||
|
||||
# Store original schemas
|
||||
original_schemas = schema_registry.schemas.copy()
|
||||
# Clear all schemas for testing
|
||||
|
|
|
|||
|
|
@ -304,6 +304,7 @@ class TestGeoGuessrClientIntegration:
|
|||
"""Test real API call to profile endpoint."""
|
||||
# This test requires GEOGUESSR_NCFA_COOKIE to be set
|
||||
import os
|
||||
|
||||
if not os.environ.get("GEOGUESSR_NCFA_COOKIE"):
|
||||
pytest.skip("GEOGUESSR_NCFA_COOKIE not set")
|
||||
|
||||
|
|
@ -317,6 +318,7 @@ class TestGeoGuessrClientIntegration:
|
|||
"""Test real API call to stats' endpoint."""
|
||||
# This test requires GEOGUESSR_NCFA_COOKIE to be set
|
||||
import os
|
||||
|
||||
if not os.environ.get("GEOGUESSR_NCFA_COOKIE"):
|
||||
pytest.skip("GEOGUESSR_NCFA_COOKIE not set")
|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ class TestAnalysisService:
|
|||
games = []
|
||||
for i in range(6):
|
||||
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(
|
||||
token=f"game-{i}",
|
||||
map_name="World",
|
||||
|
|
@ -124,7 +126,9 @@ class TestAnalysisService:
|
|||
games = []
|
||||
for i in range(6):
|
||||
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(
|
||||
token=f"game-{i}",
|
||||
map_name="World",
|
||||
|
|
@ -182,17 +186,11 @@ class TestAnalysisService:
|
|||
assert all(area["score"] >= 4500 for area in result.strong_areas)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_recent_games(
|
||||
self, analysis_service, mock_game_service, sample_games
|
||||
):
|
||||
async def test_analyze_recent_games(self, analysis_service, mock_game_service, sample_games):
|
||||
"""Test analyze_recent_games method."""
|
||||
mock_game_service.get_recent_games.return_value = sample_games
|
||||
|
||||
with patch.object(
|
||||
analysis_service,
|
||||
'analyze_games',
|
||||
wraps=AnalysisService.analyze_games
|
||||
):
|
||||
with patch.object(analysis_service, "analyze_games", wraps=AnalysisService.analyze_games):
|
||||
result = await analysis_service.analyze_recent_games(count=5)
|
||||
|
||||
assert "analysis" in result
|
||||
|
|
@ -207,17 +205,20 @@ class TestAnalysisService:
|
|||
"""Test analyze_recent_games with session token."""
|
||||
mock_game_service.get_recent_games.return_value = sample_games
|
||||
|
||||
await analysis_service.analyze_recent_games(
|
||||
count=10,
|
||||
session_token="test_token"
|
||||
)
|
||||
await analysis_service.analyze_recent_games(count=10, session_token="test_token")
|
||||
|
||||
mock_game_service.get_recent_games.assert_called_once_with(10, "test_token")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_performance_summary(
|
||||
self, analysis_service, mock_game_service, mock_profile_service,
|
||||
mock_client, sample_games, mock_season_stats_data, mock_dynamic_response
|
||||
self,
|
||||
analysis_service,
|
||||
mock_game_service,
|
||||
mock_profile_service,
|
||||
mock_client,
|
||||
sample_games,
|
||||
mock_season_stats_data,
|
||||
mock_dynamic_response,
|
||||
):
|
||||
"""Test comprehensive performance summary."""
|
||||
mock_profile_service.get_comprehensive_profile.return_value = {
|
||||
|
|
@ -227,6 +228,7 @@ class TestAnalysisService:
|
|||
|
||||
mock_season_response = mock_dynamic_response(mock_season_stats_data)
|
||||
from geoguessr_mcp.models import SeasonStats
|
||||
|
||||
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_recent_games.return_value = sample_games[:3]
|
||||
|
|
@ -267,7 +269,14 @@ class TestAnalysisService:
|
|||
for i in range(5)
|
||||
]
|
||||
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)
|
||||
]
|
||||
mock_game_service.get_recent_games.return_value = games
|
||||
|
|
@ -289,7 +298,14 @@ class TestAnalysisService:
|
|||
for i in range(5)
|
||||
]
|
||||
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)
|
||||
]
|
||||
mock_game_service.get_recent_games.return_value = games
|
||||
|
|
@ -308,7 +324,9 @@ class TestAnalysisService:
|
|||
games = []
|
||||
for i in range(6):
|
||||
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(
|
||||
token=f"game-{i}",
|
||||
map_name="World",
|
||||
|
|
|
|||
|
|
@ -40,32 +40,23 @@ class TestGameService:
|
|||
"""Test game details with explicit session token."""
|
||||
mock_client.get.return_value = mock_dynamic_response(mock_game_data)
|
||||
|
||||
game, response = await game_service.get_game_details(
|
||||
"ABC123",
|
||||
session_token="test_token"
|
||||
)
|
||||
game, response = await game_service.get_game_details("ABC123", session_token="test_token")
|
||||
|
||||
call_args = mock_client.get.call_args
|
||||
assert call_args[0][1] == "test_token"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_game_details_failure(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
async def test_get_game_details_failure(self, game_service, mock_client, mock_dynamic_response):
|
||||
"""Test game details retrieval failure."""
|
||||
mock_client.get.return_value = mock_dynamic_response(
|
||||
{"error": "Game not found"},
|
||||
success=False,
|
||||
status_code=404
|
||||
{"error": "Game not found"}, success=False, status_code=404
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to get game details"):
|
||||
await game_service.get_game_details("INVALID")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_unfinished_games(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
async def test_get_unfinished_games(self, game_service, mock_client, mock_dynamic_response):
|
||||
"""Test unfinished games retrieval."""
|
||||
unfinished_data = [
|
||||
{"token": "game-1", "map": {"name": "World"}},
|
||||
|
|
@ -79,9 +70,7 @@ class TestGameService:
|
|||
assert len(response.data) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_streak_game(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
async def test_get_streak_game(self, game_service, mock_client, mock_dynamic_response):
|
||||
"""Test streak game retrieval."""
|
||||
streak_data = {
|
||||
"token": "streak-123",
|
||||
|
|
@ -122,7 +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
|
||||
|
|
@ -153,10 +147,7 @@ class TestGameService:
|
|||
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
|
||||
)
|
||||
mock_client.get.return_value = mock_dynamic_response({"error": "Failed"}, success=False)
|
||||
|
||||
games = await game_service.get_recent_games(count=5)
|
||||
|
||||
|
|
@ -164,7 +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 = [
|
||||
|
|
@ -193,14 +189,10 @@ class TestGameService:
|
|||
assert stats.division == "Gold"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_season_stats_failure(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
async def test_get_season_stats_failure(self, game_service, mock_client, mock_dynamic_response):
|
||||
"""Test season stats failure."""
|
||||
mock_client.get.return_value = mock_dynamic_response(
|
||||
{"error": "No active season"},
|
||||
success=False,
|
||||
status_code=404
|
||||
{"error": "No active season"}, success=False, status_code=404
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to get season stats"):
|
||||
|
|
@ -246,18 +238,14 @@ class TestGameService:
|
|||
):
|
||||
"""Test daily challenge failure."""
|
||||
mock_client.get.return_value = mock_dynamic_response(
|
||||
{"error": "Challenge not found"},
|
||||
success=False,
|
||||
status_code=404
|
||||
{"error": "Challenge not found"}, success=False, status_code=404
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to get daily challenge"):
|
||||
await game_service.get_daily_challenge()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_battle_royale(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
async def test_get_battle_royale(self, game_service, mock_client, mock_dynamic_response):
|
||||
"""Test battle royale game retrieval."""
|
||||
br_data = {
|
||||
"gameId": "br-123",
|
||||
|
|
@ -272,9 +260,7 @@ class TestGameService:
|
|||
assert response.data["players"] == 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_duel(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
async def test_get_duel(self, game_service, mock_client, mock_dynamic_response):
|
||||
"""Test duel game retrieval."""
|
||||
duel_data = {
|
||||
"duelId": "duel-456",
|
||||
|
|
@ -289,9 +275,7 @@ class TestGameService:
|
|||
assert response.data["player1"]["score"] == 5000
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_tournaments(
|
||||
self, game_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
async def test_get_tournaments(self, game_service, mock_client, mock_dynamic_response):
|
||||
"""Test tournaments retrieval."""
|
||||
tournaments_data = [
|
||||
{"id": "t1", "name": "Weekly Tournament", "status": "active"},
|
||||
|
|
|
|||
|
|
@ -46,14 +46,10 @@ class TestProfileService:
|
|||
assert call_args[0][1] == "test_token"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_profile_failure(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
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
|
||||
{"error": "Unauthorized"}, success=False, status_code=401
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="Failed to get profile"):
|
||||
|
|
@ -75,23 +71,17 @@ class TestProfileService:
|
|||
assert stats.win_rate == 0.65
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_stats_failure(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
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
|
||||
{"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
|
||||
):
|
||||
async def test_get_extended_stats(self, profile_service, mock_client, mock_dynamic_response):
|
||||
"""Test extended stats retrieval."""
|
||||
extended_data = {
|
||||
"totalDistance": 1500000,
|
||||
|
|
@ -154,9 +144,7 @@ class TestProfileService:
|
|||
assert achievements[0].name == "Winner"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_public_profile(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
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",
|
||||
|
|
@ -172,9 +160,7 @@ class TestProfileService:
|
|||
assert profile.nick == "OtherPlayer"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_user_maps(
|
||||
self, profile_service, mock_client, mock_dynamic_response
|
||||
):
|
||||
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"},
|
||||
|
|
@ -189,7 +175,12 @@ class TestProfileService:
|
|||
|
||||
@pytest.mark.asyncio
|
||||
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."""
|
||||
# Setup mock responses for each call
|
||||
|
|
@ -197,9 +188,11 @@ class TestProfileService:
|
|||
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"},
|
||||
]),
|
||||
mock_dynamic_response(
|
||||
[ # achievements
|
||||
{"id": "ach-1", "name": "Test", "unlocked": True, "unlockedAt": "2024-01-01"},
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
result = await profile_service.get_comprehensive_profile()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue