We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/isdaniel/mcp_weather_server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""
Unit tests for weather tool handlers.
"""
import pytest
import json
from unittest.mock import AsyncMock, Mock, patch
from mcp.types import TextContent
from src.mcp_weather_server.tools.tools_weather import (
GetCurrentWeatherToolHandler,
GetWeatherByDateRangeToolHandler,
GetWeatherDetailsToolHandler
)
class TestGetCurrentWeatherToolHandler:
"""Test cases for GetCurrentWeatherToolHandler."""
@pytest.fixture
def handler(self):
"""Create a GetCurrentWeatherToolHandler instance."""
return GetCurrentWeatherToolHandler()
def test_tool_description(self, handler):
"""Test the tool description is properly formatted."""
description = handler.get_tool_description()
assert description.name == "get_current_weather"
assert "current weather" in description.description.lower()
assert description.inputSchema["type"] == "object"
assert "city" in description.inputSchema["properties"]
assert description.inputSchema["required"] == ["city"]
@pytest.mark.asyncio
async def test_run_tool_success(self, handler, sample_current_weather_data):
"""Test successful tool execution."""
# Mock the weather service
from unittest.mock import Mock
mock_service = Mock()
mock_service.get_current_weather = AsyncMock(return_value=sample_current_weather_data)
mock_service.format_current_weather_response.return_value = "Formatted weather response"
handler.weather_service = mock_service
args = {"city": "New York"}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert result[0].text == "Formatted weather response"
mock_service.get_current_weather.assert_called_once_with("New York")
mock_service.format_current_weather_response.assert_called_once_with(sample_current_weather_data)
@pytest.mark.asyncio
async def test_run_tool_missing_city(self, handler):
"""Test tool execution with missing city argument."""
args = {}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "Missing required arguments: city" in result[0].text
@pytest.mark.asyncio
async def test_run_tool_weather_service_error(self, handler):
"""Test tool execution when weather service raises ValueError."""
mock_service = AsyncMock()
mock_service.get_current_weather.side_effect = ValueError("API error")
handler.weather_service = mock_service
args = {"city": "Invalid City"}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "Error: API error" in result[0].text
@pytest.mark.asyncio
async def test_run_tool_unexpected_error(self, handler):
"""Test tool execution with unexpected error."""
mock_service = AsyncMock()
mock_service.get_current_weather.side_effect = Exception("Unexpected error")
handler.weather_service = mock_service
args = {"city": "Test City"}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "Unexpected error occurred: Unexpected error" in result[0].text
class TestGetWeatherByDateRangeToolHandler:
"""Test cases for GetWeatherByDateRangeToolHandler."""
@pytest.fixture
def handler(self):
"""Create a GetWeatherByDateRangeToolHandler instance."""
return GetWeatherByDateRangeToolHandler()
def test_tool_description(self, handler):
"""Test the tool description is properly formatted."""
description = handler.get_tool_description()
assert description.name == "get_weather_byDateTimeRange"
assert "weather information" in description.description.lower()
assert description.inputSchema["type"] == "object"
required_fields = ["city", "start_date", "end_date"]
for field in required_fields:
assert field in description.inputSchema["properties"]
assert set(description.inputSchema["required"]) == set(required_fields)
@pytest.mark.asyncio
async def test_run_tool_success(self, handler, sample_weather_range_data):
"""Test successful tool execution."""
from unittest.mock import Mock
mock_service = Mock()
mock_service.get_weather_by_date_range = AsyncMock(return_value=sample_weather_range_data)
mock_service.format_weather_range_response.return_value = "Formatted range response"
handler.weather_service = mock_service
args = {
"city": "New York",
"start_date": "2024-01-01",
"end_date": "2024-01-02"
}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert result[0].text == "Formatted range response"
mock_service.get_weather_by_date_range.assert_called_once_with(
"New York", "2024-01-01", "2024-01-02"
)
@pytest.mark.asyncio
async def test_run_tool_missing_required_args(self, handler):
"""Test tool execution with missing required arguments."""
args = {"city": "New York"} # Missing dates
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "Missing required arguments" in result[0].text
assert "start_date" in result[0].text
assert "end_date" in result[0].text
@pytest.mark.asyncio
async def test_run_tool_service_error(self, handler):
"""Test tool execution when weather service raises error."""
mock_service = AsyncMock()
mock_service.get_weather_by_date_range.side_effect = ValueError("Date range error")
handler.weather_service = mock_service
args = {
"city": "Test City",
"start_date": "invalid-date",
"end_date": "2024-01-02"
}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "Error: Date range error" in result[0].text
class TestGetWeatherDetailsToolHandler:
"""Test cases for GetWeatherDetailsToolHandler."""
@pytest.fixture
def handler(self):
"""Create a GetWeatherDetailsToolHandler instance."""
return GetWeatherDetailsToolHandler()
def test_tool_description(self, handler):
"""Test the tool description is properly formatted."""
description = handler.get_tool_description()
assert description.name == "get_weather_details"
assert "detailed weather information" in description.description.lower()
assert description.inputSchema["type"] == "object"
assert "city" in description.inputSchema["properties"]
assert description.inputSchema["required"] == ["city"]
@pytest.mark.asyncio
async def test_run_tool_success(self, handler, sample_current_weather_data):
"""Test successful tool execution."""
mock_service = AsyncMock()
mock_service.get_current_weather.return_value = sample_current_weather_data
handler.weather_service = mock_service
args = {"city": "New York"}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
# Parse the JSON response to verify structure
response_data = json.loads(result[0].text)
assert response_data["city"] == "New York"
assert response_data["temperature_c"] == 25.0
assert response_data["weather_description"] == "Mainly clear"
mock_service.get_current_weather.assert_called_once_with("New York")
@pytest.mark.asyncio
async def test_run_tool_missing_city(self, handler):
"""Test tool execution with missing city argument."""
args = {}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "Missing required arguments: city" in result[0].text
@pytest.mark.asyncio
async def test_run_tool_service_error(self, handler):
"""Test tool execution when weather service raises error."""
mock_service = AsyncMock()
mock_service.get_current_weather.side_effect = ValueError("Service error")
handler.weather_service = mock_service
args = {"city": "Invalid City"}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
# Check that the error is properly formatted as JSON
response_data = json.loads(result[0].text)
assert "error" in response_data
assert "Service error" in response_data["error"]
@pytest.mark.asyncio
async def test_run_tool_json_serialization_error(self, handler):
"""Test tool execution when JSON serialization fails."""
mock_service = AsyncMock()
# Create an object that can't be JSON serialized
invalid_data = {"city": "Test", "invalid": object()}
mock_service.get_current_weather.return_value = invalid_data
handler.weather_service = mock_service
args = {"city": "Test City"}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "Unexpected error occurred" in result[0].text
# Additional integration-style tests for tool handlers
class TestToolHandlerIntegration:
"""Integration tests for tool handlers with realistic scenarios."""
@pytest.mark.asyncio
async def test_current_weather_end_to_end(self):
"""Test current weather tool with mocked HTTP responses."""
handler = GetCurrentWeatherToolHandler()
# Mock the entire HTTP call chain
mock_geo_data = {
"results": [{"latitude": 51.5074, "longitude": -0.1278}]
}
mock_weather_data = {
"hourly": {
"time": ["2024-01-01T12:00"],
"temperature_2m": [15.5],
"relative_humidity_2m": [80],
"dew_point_2m": [12.0],
"weather_code": [61]
}
}
with patch('httpx.AsyncClient') as mock_client_class:
mock_client = AsyncMock()
# Setup mock responses for geo and weather API calls
geo_response = Mock()
geo_response.status_code = 200
geo_response.json.return_value = mock_geo_data
weather_response = Mock()
weather_response.status_code = 200
weather_response.json.return_value = mock_weather_data
# Return different responses for different URLs
def mock_get(url):
if "geocoding-api" in url:
return geo_response
elif "api.open-meteo.com" in url:
return weather_response
else:
raise ValueError(f"Unexpected URL: {url}")
mock_client.get.side_effect = mock_get
mock_client_class.return_value.__aenter__.return_value = mock_client
with patch('src.mcp_weather_server.utils.get_closest_utc_index', return_value=0):
result = await handler.run_tool({"city": "London"})
assert len(result) == 1
assert isinstance(result[0], TextContent)
assert "London" in result[0].text
assert "15.5°C" in result[0].text
assert "Slight rain" in result[0].text
@pytest.mark.asyncio
async def test_weather_range_end_to_end(self):
"""Test weather range tool with mocked HTTP responses."""
handler = GetWeatherByDateRangeToolHandler()
mock_geo_data = {
"results": [{"latitude": 48.8566, "longitude": 2.3522}]
}
mock_weather_data = {
"hourly": {
"time": ["2024-01-01T00:00", "2024-01-01T12:00"],
"temperature_2m": [10.0, 15.0],
"relative_humidity_2m": [90, 75],
"dew_point_2m": [8.5, 11.0],
"weather_code": [3, 1]
}
}
with patch('httpx.AsyncClient') as mock_client_class:
mock_client = AsyncMock()
geo_response = Mock()
geo_response.status_code = 200
geo_response.json.return_value = mock_geo_data
weather_response = Mock()
weather_response.status_code = 200
weather_response.json.return_value = mock_weather_data
def mock_get(url):
if "geocoding-api" in url:
return geo_response
else:
return weather_response
mock_client.get.side_effect = mock_get
mock_client_class.return_value.__aenter__.return_value = mock_client
args = {
"city": "Paris",
"start_date": "2024-01-01",
"end_date": "2024-01-01"
}
result = await handler.run_tool(args)
assert len(result) == 1
assert isinstance(result[0], TextContent)
# The response should contain analysis prompt with JSON data
assert "analyze" in result[0].text.lower()
assert "Paris" in result[0].text