#!/usr/bin/env python3
"""
Comprehensive tests for the MCP Geocoding Server
"""
import json
import os
import sys
from unittest.mock import AsyncMock, patch
import pytest # type: ignore
# Add the parent directory to the path so we can import the server
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from geocode_mcp.server import geocode_location, handle_call_tool, handle_list_tools
class TestGeocodingServer:
"""Test cases for the geocoding server functionality."""
@pytest.mark.asyncio
async def test_geocode_location_success(self) -> None:
"""Test successful geocoding with mocked HTTP response."""
mock_response_data = [
{
"lat": "40.7127281",
"lon": "-74.0060152",
"display_name": "New York, United States",
"place_id": 298085,
"type": "city",
"class": "place",
"importance": 0.9756419939577,
"boundingbox": [
"40.4960439",
"40.9152414",
"-74.2557349",
"-73.7000091",
],
}
]
with patch("aiohttp.ClientSession.get") as mock_get:
mock_response = AsyncMock()
mock_response.ok = True
mock_response.json = AsyncMock(return_value=mock_response_data)
mock_get.return_value.__aenter__.return_value = mock_response
result = await geocode_location("New York")
assert result["query"] == "New York"
assert result["results_count"] == 1
assert len(result["coordinates"]) == 1
assert result["coordinates"][0]["latitude"] == 40.7127281
assert result["coordinates"][0]["longitude"] == -74.0060152
@pytest.mark.asyncio
async def test_geocode_location_not_found(self) -> None:
"""Test geocoding when no results are found."""
with patch("aiohttp.ClientSession.get") as mock_get:
mock_response = AsyncMock()
mock_response.ok = True
mock_response.json = AsyncMock(return_value=[])
mock_get.return_value.__aenter__.return_value = mock_response
result = await geocode_location("NonexistentPlace12345")
assert "error" in result
assert result["query"] == "NonexistentPlace12345"
assert "suggestions" in result
@pytest.mark.asyncio
async def test_geocode_location_multiple_results(self) -> None:
"""Test geocoding with multiple results."""
mock_response_data = [
{
"lat": "39.7817213",
"lon": "-89.6501481",
"display_name": "Springfield, Illinois, United States",
"place_id": 123,
"type": "city",
"class": "place",
"importance": 0.8,
"boundingbox": ["39.7", "39.8", "-89.7", "-89.6"],
},
{
"lat": "42.1014831",
"lon": "-72.589811",
"display_name": "Springfield, Massachusetts, United States",
"place_id": 456,
"type": "city",
"class": "place",
"importance": 0.7,
"boundingbox": ["42.0", "42.1", "-72.6", "-72.5"],
},
]
with patch("aiohttp.ClientSession.get") as mock_get:
mock_response = AsyncMock()
mock_response.ok = True
mock_response.json = AsyncMock(return_value=mock_response_data)
mock_get.return_value.__aenter__.return_value = mock_response
result = await geocode_location("Springfield", limit=2)
assert result["results_count"] == 2
assert len(result["coordinates"]) == 2
@pytest.mark.asyncio
async def test_geocode_location_network_error(self) -> None:
"""Test geocoding with network error."""
import aiohttp
with patch("aiohttp.ClientSession.get") as mock_get:
mock_get.side_effect = aiohttp.ClientError("Connection failed")
with pytest.raises(Exception) as exc_info:
await geocode_location("New York")
assert "Network error" in str(exc_info.value)
@pytest.mark.asyncio
async def test_geocode_location_api_error(self) -> None:
"""Test geocoding with API error response."""
with patch("aiohttp.ClientSession.get") as mock_get:
mock_response = AsyncMock()
mock_response.ok = False
mock_response.status = 500
mock_response.reason = "Internal Server Error"
mock_get.return_value.__aenter__.return_value = mock_response
with pytest.raises(Exception) as exc_info:
await geocode_location("New York")
assert "Nominatim API error" in str(exc_info.value)
@pytest.mark.asyncio
async def test_list_tools(self) -> None:
"""Test that the server lists available tools correctly."""
tools = await handle_list_tools()
assert len(tools) == 1
assert tools[0].name == "get_coordinates"
assert "latitude and longitude" in tools[0].description.lower()
assert "location" in tools[0].inputSchema["properties"]
assert "limit" in tools[0].inputSchema["properties"]
@pytest.mark.asyncio
async def test_call_tool_success(self) -> None:
"""Test successful tool call."""
mock_response_data = [
{
"lat": "48.8566969",
"lon": "2.3514616",
"display_name": "Paris, France",
"place_id": 789,
"type": "city",
"class": "place",
"importance": 0.9,
"boundingbox": ["48.8", "48.9", "2.3", "2.4"],
}
]
with patch("aiohttp.ClientSession.get") as mock_get:
mock_response = AsyncMock()
mock_response.ok = True
mock_response.json = AsyncMock(return_value=mock_response_data)
mock_get.return_value.__aenter__.return_value = mock_response
result = await handle_call_tool(
"get_coordinates", {"location": "Paris, France"}
)
assert len(result) == 1
response_data = json.loads(result[0].text)
assert response_data["query"] == "Paris, France"
assert response_data["results_count"] == 1
@pytest.mark.asyncio
async def test_call_tool_invalid_tool(self) -> None:
"""Test calling an invalid tool."""
with pytest.raises(ValueError) as exc_info:
await handle_call_tool("invalid_tool", {"location": "Paris"})
assert "Unknown tool" in str(exc_info.value)
@pytest.mark.asyncio
async def test_call_tool_missing_location(self) -> None:
"""Test calling tool without required location parameter."""
result = await handle_call_tool("get_coordinates", {})
assert len(result) == 1
assert "Error:" in result[0].text
assert "required" in result[0].text.lower()
@pytest.mark.asyncio
async def test_call_tool_empty_location(self) -> None:
"""Test calling tool with empty location parameter."""
result = await handle_call_tool("get_coordinates", {"location": ""})
assert len(result) == 1
assert "Error:" in result[0].text
assert "cannot be empty" in result[0].text.lower()
@pytest.mark.asyncio
async def test_call_tool_with_limit(self) -> None:
"""Test calling tool with limit parameter."""
mock_response_data = [
{
"lat": "40.7127281",
"lon": "-74.0060152",
"display_name": "New York, United States",
"place_id": 298085,
"type": "city",
"class": "place",
"importance": 0.9756419939577,
"boundingbox": [
"40.4960439",
"40.9152414",
"-74.2557349",
"-73.7000091",
],
}
]
with patch("aiohttp.ClientSession.get") as mock_get:
mock_response = AsyncMock()
mock_response.ok = True
mock_response.json = AsyncMock(return_value=mock_response_data)
mock_get.return_value.__aenter__.return_value = mock_response
result = await handle_call_tool(
"get_coordinates", {"location": "New York", "limit": 5}
)
assert len(result) == 1
response_data = json.loads(result[0].text)
assert response_data["query"] == "New York"
def main() -> None:
pytest.main([__file__, "-v"])
if __name__ == "__main__":
main()