"""
Integration tests for challenges module MCP tools
Tests all 9 challenges and badges tools using FastMCP integration with mocked Garmin API responses.
"""
import pytest
from unittest.mock import Mock
from mcp.server.fastmcp import FastMCP
from garmin_mcp import challenges
from tests.fixtures.garmin_responses import (
MOCK_GOALS,
MOCK_PERSONAL_RECORD,
MOCK_BADGES,
)
@pytest.fixture
def app_with_challenges(mock_garmin_client):
"""Create FastMCP app with challenges tools registered"""
challenges.configure(mock_garmin_client)
app = FastMCP("Test Challenges")
app = challenges.register_tools(app)
return app
@pytest.mark.asyncio
async def test_get_goals_active(app_with_challenges, mock_garmin_client):
"""Test get_goals tool returns active goals"""
# Setup mock
mock_garmin_client.get_goals.return_value = MOCK_GOALS
# Call tool
result = await app_with_challenges.call_tool(
"get_goals",
{"goal_type": "active"}
)
# Verify
assert result is not None
mock_garmin_client.get_goals.assert_called_once_with("active")
@pytest.mark.asyncio
async def test_get_goals_default(app_with_challenges, mock_garmin_client):
"""Test get_goals tool with default parameter (active)"""
# Setup mock
mock_garmin_client.get_goals.return_value = MOCK_GOALS
# Call tool without goal_type (should default to "active")
result = await app_with_challenges.call_tool(
"get_goals",
{}
)
# Verify
assert result is not None
mock_garmin_client.get_goals.assert_called_once_with("active")
@pytest.mark.asyncio
async def test_get_goals_future(app_with_challenges, mock_garmin_client):
"""Test get_goals tool returns future goals"""
# Setup mock
future_goals = {"goals": [{"goalType": "STEPS", "goalValue": 10000, "startDate": "2024-02-01"}]}
mock_garmin_client.get_goals.return_value = future_goals
# Call tool
result = await app_with_challenges.call_tool(
"get_goals",
{"goal_type": "future"}
)
# Verify
assert result is not None
mock_garmin_client.get_goals.assert_called_once_with("future")
@pytest.mark.asyncio
async def test_get_personal_record_tool(app_with_challenges, mock_garmin_client):
"""Test get_personal_record tool returns personal records"""
# Setup mock
mock_garmin_client.get_personal_record.return_value = MOCK_PERSONAL_RECORD
# Call tool
result = await app_with_challenges.call_tool(
"get_personal_record",
{}
)
# Verify
assert result is not None
mock_garmin_client.get_personal_record.assert_called_once()
@pytest.mark.asyncio
async def test_get_earned_badges_tool(app_with_challenges, mock_garmin_client):
"""Test get_earned_badges tool returns earned badges"""
# Setup mock
mock_garmin_client.get_earned_badges.return_value = MOCK_BADGES
# Call tool
result = await app_with_challenges.call_tool(
"get_earned_badges",
{}
)
# Verify
assert result is not None
mock_garmin_client.get_earned_badges.assert_called_once()
@pytest.mark.asyncio
async def test_get_adhoc_challenges_default(app_with_challenges, mock_garmin_client):
"""Test get_adhoc_challenges tool with default parameters"""
# Setup mock
adhoc_challenges = [
{
"socialChallengeStatusId": 2,
"socialChallengeActivityTypeId": 4,
"adHocChallengeName": "January Step Challenge",
"adHocChallengeDesc": "Steps Challenge",
"uuid": "ABC123",
"startDate": "2024-01-01T00:00:00.0",
"endDate": "2024-01-31T23:59:59.0",
"userRanking": 1,
"playerCount": 5,
}
]
mock_garmin_client.get_adhoc_challenges.return_value = adhoc_challenges
# Call tool
result = await app_with_challenges.call_tool(
"get_adhoc_challenges",
{}
)
# Verify
assert result is not None
mock_garmin_client.get_adhoc_challenges.assert_called_once_with(0, 20)
@pytest.mark.asyncio
async def test_get_adhoc_challenges_custom_params(app_with_challenges, mock_garmin_client):
"""Test get_adhoc_challenges tool with custom parameters"""
# Setup mock
mock_garmin_client.get_adhoc_challenges.return_value = []
# Call tool
result = await app_with_challenges.call_tool(
"get_adhoc_challenges",
{"start": 10, "limit": 50}
)
# Verify
assert result is not None
mock_garmin_client.get_adhoc_challenges.assert_called_once_with(10, 50)
@pytest.mark.asyncio
async def test_get_available_badge_challenges_tool(app_with_challenges, mock_garmin_client):
"""Test get_available_badge_challenges tool"""
# Setup mock
badge_challenges = [
{
"uuid": "ABC123",
"badgeChallengeName": "Marathon Challenge",
"challengeCategoryId": 1,
"badgeChallengeStatusId": 2,
"startDate": "2024-01-01T00:00:00.0",
"endDate": "2024-01-31T23:59:59.0",
"badgePoints": 4,
"badgeUnitId": 1,
"badgeProgressValue": None,
"badgeTargetValue": 42195.0,
"userJoined": False,
"joinable": True,
}
]
mock_garmin_client.get_available_badge_challenges.return_value = badge_challenges
# Call tool
result = await app_with_challenges.call_tool(
"get_available_badge_challenges",
{}
)
# Verify
assert result is not None
mock_garmin_client.get_available_badge_challenges.assert_called_once_with(1, 20)
@pytest.mark.asyncio
async def test_get_badge_challenges_tool(app_with_challenges, mock_garmin_client):
"""Test get_badge_challenges tool"""
# Setup mock
badge_challenges = [MOCK_BADGES[0]]
mock_garmin_client.get_badge_challenges.return_value = badge_challenges
# Call tool
result = await app_with_challenges.call_tool(
"get_badge_challenges",
{"start": 1, "limit": 50}
)
# Verify
assert result is not None
mock_garmin_client.get_badge_challenges.assert_called_once_with(1, 50)
@pytest.mark.asyncio
async def test_get_non_completed_badge_challenges_tool(app_with_challenges, mock_garmin_client):
"""Test get_non_completed_badge_challenges tool"""
# Setup mock
non_completed = [
{
"uuid": "DEF456",
"badgeChallengeName": "Ultra Marathon Challenge",
"challengeCategoryId": 1,
"badgeChallengeStatusId": 2,
"startDate": "2024-01-01T00:00:00.0",
"endDate": "2024-01-31T23:59:59.0",
"badgePoints": 4,
"badgeUnitId": 1,
"badgeProgressValue": 21000.0,
"badgeTargetValue": 50000.0,
"badgeEarnedDate": None,
"userJoined": True,
}
]
mock_garmin_client.get_non_completed_badge_challenges.return_value = non_completed
# Call tool
result = await app_with_challenges.call_tool(
"get_non_completed_badge_challenges",
{}
)
# Verify
assert result is not None
mock_garmin_client.get_non_completed_badge_challenges.assert_called_once_with(1, 20)
@pytest.mark.asyncio
async def test_get_race_predictions_tool(app_with_challenges, mock_garmin_client):
"""Test get_race_predictions tool"""
# Setup mock
race_predictions = {
"5K": {"time": 1200, "unit": "seconds"}, # 20 minutes
"10K": {"time": 2520, "unit": "seconds"}, # 42 minutes
"halfMarathon": {"time": 5400, "unit": "seconds"}, # 1h 30m
"marathon": {"time": 11400, "unit": "seconds"} # 3h 10m
}
mock_garmin_client.get_race_predictions.return_value = race_predictions
# Call tool
result = await app_with_challenges.call_tool(
"get_race_predictions",
{}
)
# Verify
assert result is not None
mock_garmin_client.get_race_predictions.assert_called_once()
@pytest.mark.asyncio
async def test_get_inprogress_virtual_challenges_tool(app_with_challenges, mock_garmin_client):
"""Test get_inprogress_virtual_challenges tool"""
# Setup mock
virtual_challenges = [
{
"uuid": "GHI789",
"name": "Virtual NYC Marathon",
"startDate": "2024-01-01T00:00:00.0",
"endDate": "2024-01-31T23:59:59.0",
"progress": 28000.0,
"target": 42195.0,
}
]
mock_garmin_client.get_inprogress_virtual_challenges.return_value = virtual_challenges
# Call tool with default parameters
result = await app_with_challenges.call_tool(
"get_inprogress_virtual_challenges",
{}
)
# Verify
assert result is not None
mock_garmin_client.get_inprogress_virtual_challenges.assert_called_once_with(0, 20)
# Error handling tests
@pytest.mark.asyncio
async def test_get_goals_no_data(app_with_challenges, mock_garmin_client):
"""Test get_goals tool when no goals found"""
# Setup mock to return None
mock_garmin_client.get_goals.return_value = None
# Call tool
result = await app_with_challenges.call_tool(
"get_goals",
{"goal_type": "active"}
)
# Verify error message is returned
assert result is not None
# Should indicate no goals found
@pytest.mark.asyncio
async def test_get_personal_record_exception(app_with_challenges, mock_garmin_client):
"""Test get_personal_record tool when API raises exception"""
# Setup mock to raise exception
mock_garmin_client.get_personal_record.side_effect = Exception("API Error")
# Call tool
result = await app_with_challenges.call_tool(
"get_personal_record",
{}
)
# Verify error is handled gracefully
assert result is not None
# Should return error message, not crash