"""
Integration tests for the GeoGuessr API client.
These tests verify the API client functionality with mocked HTTP responses,
simulating real API interactions without making actual network calls.
"""
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from geoguessr_mcp.api import DynamicResponse, EndpointInfo, Endpoints, GeoGuessrClient
from geoguessr_mcp.config import settings
class TestDynamicResponse:
"""Tests for DynamicResponse wrapper class."""
def test_is_success_200(self):
"""Test success detection for 200 status code."""
response = DynamicResponse(
data={"id": "123"},
endpoint="/v3/profiles",
status_code=200,
response_time_ms=150.0,
)
assert response.is_success is True
def test_is_success_201(self):
"""Test success detection for 201 status code."""
response = DynamicResponse(
data={"created": True},
endpoint="/v3/games",
status_code=201,
response_time_ms=200.0,
)
assert response.is_success is True
def test_is_success_failure(self):
"""Test success detection for error status codes."""
response = DynamicResponse(
data={"error": "Not found"},
endpoint="/v3/profiles",
status_code=404,
response_time_ms=100.0,
)
assert response.is_success is False
def test_available_fields_dict(self):
"""Test available_fields with dict data."""
response = DynamicResponse(
data={"id": "123", "name": "Test", "score": 5000},
endpoint="/mock/endpoint",
status_code=200,
response_time_ms=100.0,
)
assert response.available_fields == ["id", "name", "score"]
def test_available_fields_non_dict(self):
"""Test available_fields with non-dict data."""
response = DynamicResponse(
data=["item1", "item2"],
endpoint="/mock/endpoint",
status_code=200,
response_time_ms=100.0,
)
assert response.available_fields == []
def test_get_field_simple(self):
"""Test getting a simple field."""
response = DynamicResponse(
data={"id": "123", "name": "Test"},
endpoint="/mock/endpoint",
status_code=200,
response_time_ms=100.0,
)
assert response.get_field("id") == "123"
assert response.get_field("name") == "Test"
def test_get_field_nested(self):
"""Test getting a nested field with dot notation."""
response = DynamicResponse(
data={
"user": {
"profile": {
"name": "TestUser",
"level": 50,
}
}
},
endpoint="/mock/endpoint",
status_code=200,
response_time_ms=100.0,
)
assert response.get_field("user.profile.name") == "TestUser"
assert response.get_field("user.profile.level") == 50
def test_get_field_missing_with_default(self):
"""Test getting missing field returns default."""
response = DynamicResponse(
data={"id": "123"},
endpoint="/mock/endpoint",
status_code=200,
response_time_ms=100.0,
)
assert response.get_field("missing", default="default_value") == "default_value"
assert response.get_field("nested.missing", default=None) is None
def test_to_dict(self):
"""Test converting response to dict."""
response = DynamicResponse(
data={"id": "123"},
endpoint="/v3/profiles",
status_code=200,
response_time_ms=150.5,
)
result = response.to_dict()
assert result["success"] is True
assert result["status_code"] == 200
assert result["endpoint"] == "/v3/profiles"
assert result["response_time_ms"] == 150.5
assert result["data"] == {"id": "123"}
assert "available_fields" in result
def test_summarize(self):
"""Test response summarization."""
response = DynamicResponse(
data={
"items": [
{"id": 1, "name": "Item 1"},
{"id": 2, "name": "Item 2"},
{"id": 3, "name": "Item 3"},
{"id": 4, "name": "Item 4"},
],
"total": 4,
},
endpoint="/mock/endpoint",
status_code=200,
response_time_ms=100.0,
)
summary = response.summarize(max_depth=1)
assert summary["endpoint"] == "/mock/endpoint"
assert summary["status"] == "success"
assert "data_summary" in summary
def test_summarize_long_string(self):
"""Test that long strings are truncated in summaries."""
long_text = "x" * 200
response = DynamicResponse(
data={"description": long_text},
endpoint="/mock/endpoint",
status_code=200,
response_time_ms=100.0,
)
summary = response.summarize(max_depth=2)
# The long string should be truncated
assert len(summary["data_summary"]["description"]) <= 103 # 100 + "..."
class TestGeoGuessrClient:
"""Tests for GeoGuessrClient."""
@pytest.mark.asyncio
async def test_get_authenticated_client(self, client, mock_session_manager):
"""Test getting authenticated HTTP client."""
http_client = await client._get_authenticated_client()
assert http_client is not None
mock_session_manager.get_session.assert_called_once()
@pytest.mark.asyncio
async def test_get_authenticated_client_no_session(self, mock_session_manager):
"""Test error when no session is available."""
mock_session_manager.get_session = AsyncMock(return_value=None)
client = GeoGuessrClient(mock_session_manager)
with pytest.raises(ValueError, match="No valid session available"):
await client._get_authenticated_client()
def test_get_base_url_main_api(self, client):
"""Test base URL selection for main API."""
endpoint = EndpointInfo(path="/v3/profiles", use_game_server=False)
url = client._get_base_url(endpoint)
assert url == settings.GEOGUESSR_API_URL
def test_get_base_url_game_server(self, client):
"""Test base URL selection for game server."""
endpoint = EndpointInfo(path="/tournaments", use_game_server=True)
url = client._get_base_url(endpoint)
assert url == settings.GAME_SERVER_URL
@pytest.mark.asyncio
async def test_get_request_success(self, client):
"""Test successful GET request."""
with patch.object(client, "_get_authenticated_client") as mock_auth:
mock_http_client = AsyncMock()
mock_http_client.__aenter__.return_value = mock_http_client
mock_http_client.__aexit__.return_value = None
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"id": "123", "nick": "TestUser"}
mock_http_client.get = AsyncMock(return_value=mock_response)
mock_auth.return_value = mock_http_client
response = await client.get(Endpoints.PROFILES.GET_PROFILE)
assert response.is_success
assert response.data["id"] == "123"
@pytest.mark.asyncio
async def test_get_request_failure(self, client):
"""Test failed GET request."""
with patch.object(client, "_get_authenticated_client") as mock_auth:
mock_http_client = AsyncMock()
mock_http_client.__aenter__.return_value = mock_http_client
mock_http_client.__aexit__.return_value = None
mock_response = MagicMock()
mock_response.status_code = 404
mock_response.text = "Not found"
mock_http_client.get = AsyncMock(return_value=mock_response)
mock_auth.return_value = mock_http_client
response = await client.get(Endpoints.PROFILES.GET_PROFILE)
assert not response.is_success
assert response.status_code == 404
@pytest.mark.asyncio
async def test_post_request(self, client):
"""Test POST request."""
with patch.object(client, "_get_authenticated_client") as mock_auth:
mock_http_client = AsyncMock()
mock_http_client.__aenter__.return_value = mock_http_client
mock_http_client.__aexit__.return_value = None
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"success": True}
mock_http_client.post = AsyncMock(return_value=mock_response)
mock_auth.return_value = mock_http_client
endpoint = EndpointInfo(path="/mock/endpoint", method="POST")
response = await client.post(endpoint, json_data={"data": "test"})
assert response.is_success
@pytest.mark.asyncio
async def test_get_raw_request(self, client):
"""Test raw GET request to arbitrary path."""
with patch.object(client, "_get_authenticated_client") as mock_auth:
mock_http_client = AsyncMock()
mock_http_client.__aenter__.return_value = mock_http_client
mock_http_client.__aexit__.return_value = None
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"discovered": True}
mock_http_client.get = AsyncMock(return_value=mock_response)
mock_auth.return_value = mock_http_client
response = await client.get_raw("/v3/unknown-endpoint")
assert response.is_success
assert response.endpoint == "/v3/unknown-endpoint"
@pytest.mark.asyncio
async def test_timeout_handling(self, client):
"""Test handling of timeout exceptions."""
with patch.object(client, "_get_authenticated_client") as mock_auth:
mock_http_client = AsyncMock()
mock_http_client.__aenter__.return_value = mock_http_client
mock_http_client.__aexit__.return_value = None
mock_http_client.get = AsyncMock(side_effect=httpx.TimeoutException("Timeout"))
mock_auth.return_value = mock_http_client
with pytest.raises(httpx.TimeoutException):
await client.get(Endpoints.PROFILES.GET_PROFILE)
@pytest.mark.integration
@pytest.mark.real_env
class TestGeoGuessrClientIntegration:
"""
Integration tests that would make real API calls.
These tests are marked with @pytest.mark.integration and should only
be run when explicitly requested (pytest -m integration) with a valid
authentication cookie.
"""
@pytest.mark.asyncio
async def test_real_profile_endpoint(self, real_client):
"""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")
response = await real_client.get(Endpoints.PROFILES.GET_PROFILE)
assert response.is_success
assert "user" in response.available_fields or "email" in response.available_fields
@pytest.mark.asyncio
async def test_real_stats_endpoint(self, real_client):
"""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")
response = await real_client.get(Endpoints.PROFILES.GET_STATS)
assert response.is_success
# Stats' endpoint should have some numeric data
assert len(response.available_fields) > 0