Files
intervalsicu-mcp/tests/test_server.py
T
Chris Farhood f067f9639a
build-image / test (push) Successful in 10s
build-image / build (push) Successful in 42s
feat(multi-tenant): resolve per-caller credentials in every tool
All 20 tools now drop the athlete_id/api_key parameters and instead resolve the
authenticated caller's stored, enabled credentials via
credentials.resolve_caller_credentials() (get_access_token().subject -> store).

Security: there is no tool parameter a caller can pass to supply a key, so a
disabled/unapproved user cannot bypass the admin-approval gate — each tool
returns a helpful "not approved / set up your credentials" message instead.
Gear resolution now uses the caller's athlete id rather than an env var.

Tests: conftest autouse fixture runs tool tests as an enabled user; a
parametrized test asserts every tool refuses when unauthorized; existing tool
tests updated (no more athlete_id/api_key kwargs). 221 passing at 91.5%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 19:28:27 -04:00

952 lines
30 KiB
Python

"""
Unit tests for the main MCP server tool functions in intervals_mcp_server.server.
These tests use monkeypatching to mock API responses and verify the formatting and output of each tool function:
- get_activities
- get_activity_details
- get_activity_intervals
- get_activity_streams
- get_activity_messages
- add_activity_message
- get_events
- get_event_by_id
- add_or_update_event
- get_wellness_data
The tests ensure that the server's public API returns expected strings and handles data correctly.
"""
import asyncio
import os
import pathlib
import sys
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "src"))
os.environ.setdefault("API_KEY", "test")
os.environ.setdefault("ATHLETE_ID", "i1")
from intervals_mcp_server.server import ( # pylint: disable=wrong-import-position
add_activity_message,
get_activities,
get_activity_details,
get_activity_intervals,
get_activity_messages,
get_activity_streams,
add_or_update_event,
get_athlete_power_curves,
get_event_by_id,
get_events,
get_gear_list,
get_wellness_data,
get_custom_items,
get_custom_item_by_id,
create_custom_item,
update_custom_item,
delete_custom_item,
)
from intervals_mcp_server.tools import gear as gear_module # pylint: disable=wrong-import-position
from tests.sample_data import INTERVALS_DATA, POWER_CURVES_DATA # pylint: disable=wrong-import-position
def _reset_gear_cache():
"""Helper to clear the module-level gear cache between tests."""
gear_module._GEAR_RAW_CACHE.clear() # pylint: disable=protected-access
def test_get_activities(monkeypatch):
"""
Test get_activities returns a formatted string containing activity details when given a sample activity.
"""
sample = {
"name": "Morning Ride",
"id": 123,
"type": "Ride",
"startTime": "2024-01-01T08:00:00Z",
"distance": 1000,
"duration": 3600,
}
async def fake_request(*_args, **_kwargs):
return [sample]
# Patch in both api.client and tools modules to ensure it works
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(get_activities(limit=1, include_unnamed=True))
assert "Morning Ride" in result
assert "Activities:" in result
def test_get_activity_details(monkeypatch):
"""
Test get_activity_details returns a formatted string with the activity name and details.
"""
sample = {
"name": "Morning Ride",
"id": 123,
"type": "Ride",
"startTime": "2024-01-01T08:00:00Z",
"distance": 1000,
"duration": 3600,
}
async def fake_request(*_args, **_kwargs):
return sample
# Patch in both api.client and tools modules to ensure it works
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(get_activity_details(123))
assert "Activity: Morning Ride" in result
def test_get_events(monkeypatch):
"""
Test get_events returns a formatted string containing event details when given a sample event.
"""
event = {
"date": "2024-01-01",
"id": "e1",
"name": "Test Event",
"description": "desc",
"race": True,
}
async def fake_request(*_args, **_kwargs):
return [event]
# Patch in both api.client and tools modules to ensure it works
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr("intervals_mcp_server.tools.events.make_intervals_request", fake_request)
result = asyncio.run(get_events(start_date="2024-01-01", end_date="2024-01-02"))
assert "Test Event" in result
assert "Events:" in result
def test_get_event_by_id(monkeypatch):
"""
Test get_event_by_id returns a formatted string with event details for a given event ID.
"""
event = {
"id": "e1",
"date": "2024-01-01",
"name": "Test Event",
"description": "desc",
"race": True,
}
async def fake_request(*_args, **_kwargs):
return event
# Patch in both api.client and tools modules to ensure it works
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr("intervals_mcp_server.tools.events.make_intervals_request", fake_request)
result = asyncio.run(get_event_by_id("e1"))
assert "Event Details:" in result
assert "Test Event" in result
def test_get_wellness_data(monkeypatch):
"""
Test get_wellness_data returns a formatted string containing wellness data for a given athlete.
"""
wellness = {
"2024-01-01": {
"id": "2024-01-01",
"ctl": 75,
"sleepSecs": 28800,
}
}
async def fake_request(*_args, **_kwargs):
return wellness
# Patch in both api.client and tools modules to ensure it works
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr("intervals_mcp_server.tools.wellness.make_intervals_request", fake_request)
result = asyncio.run(get_wellness_data())
assert "Wellness Data:" in result
assert "2024-01-01" in result
def test_get_wellness_data_renders_macros(monkeypatch):
"""
Integration test: native nutrition macros (carbohydrates, protein,
fatTotal) flow from the API response through get_wellness_data into the
formatted output.
"""
wellness = [
{
"id": "2026-04-08",
"carbohydrates": 310,
"protein": 145,
"fatTotal": 72,
}
]
async def fake_request(*_args, **_kwargs):
return wellness
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr("intervals_mcp_server.tools.wellness.make_intervals_request", fake_request)
result = asyncio.run(get_wellness_data())
assert "Wellness Data:" in result
assert "2026-04-08" in result
assert "Nutrition & Hydration:" in result
assert "- Carbohydrates: 310 g" in result
assert "- Protein: 145 g" in result
assert "- Fat: 72 g" in result
def test_get_wellness_data_include_all_fields(monkeypatch):
"""
Test get_wellness_data with include_all_fields=True returns a formatted string including additional fields.
"""
wellness = [
{
"id": "2024-01-01",
"ctl": 75,
"sleepSecs": 28800,
"customField": "custom_value",
}
]
async def fake_request(*_args, **_kwargs):
return wellness
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr("intervals_mcp_server.tools.wellness.make_intervals_request", fake_request)
result = asyncio.run(get_wellness_data(include_all_fields=True))
assert "Wellness Data:" in result
assert "2024-01-01" in result
assert "Fitness (CTL): 75" in result
assert "Other Fields:" in result
assert "customField: custom_value" in result
def test_get_activity_intervals(monkeypatch):
"""
Test get_activity_intervals returns a formatted string with interval analysis for a given activity.
"""
async def fake_request(*_args, **_kwargs):
return INTERVALS_DATA
# Patch in both api.client and tools modules to ensure it works
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(get_activity_intervals("123"))
assert "Intervals Analysis:" in result
assert "Rep 1" in result
def test_get_activity_streams(monkeypatch):
"""
Test get_activity_streams returns a formatted string with stream data for a given activity.
"""
sample_streams = [
{
"type": "time",
"name": "time",
"data": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
"data2": [],
"valueType": "time_units",
"valueTypeIsArray": False,
"anomalies": None,
"custom": False,
},
{
"type": "watts",
"name": "watts",
"data": [150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200],
"data2": [],
"valueType": "power_units",
"valueTypeIsArray": False,
"anomalies": None,
"custom": False,
},
{
"type": "heartrate",
"name": "heartrate",
"data": [120, 125, 130, 135, 140, 145, 150, 155, 160, 165, 170],
"data2": [],
"valueType": "hr_units",
"valueTypeIsArray": False,
"anomalies": None,
"custom": False,
},
]
async def fake_request(*_args, **_kwargs):
return sample_streams
# Patch in both api.client and tools modules to ensure it works
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(get_activity_streams("i107537962"))
assert "Activity Streams" in result
assert "time" in result
assert "watts" in result
assert "heartrate" in result
assert "Data Points: 11" in result
def test_add_or_update_event(monkeypatch):
"""
Test add_or_update_event successfully posts an event and returns the response data.
"""
expected_response = {
"id": "e123",
"start_date_local": "2024-01-15T00:00:00",
"category": "WORKOUT",
"name": "Test Workout",
"type": "Ride",
}
async def fake_post_request(*_args, **_kwargs):
return expected_response
# Patch in both api.client and tools modules to ensure it works
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_post_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.events.make_intervals_request", fake_post_request
)
result = asyncio.run(
add_or_update_event(
start_date="2024-01-15", name="Test Workout", workout_type="Ride"
)
)
assert "Successfully created event id:" in result
assert "e123" in result
def test_get_activity_messages(monkeypatch):
"""Test get_activity_messages returns formatted messages for an activity."""
sample_messages = [
{
"id": 1,
"name": "Niko",
"created": "2024-06-15T10:30:00Z",
"type": "NOTE",
"content": "Legs felt heavy today",
},
{
"id": 2,
"name": "Coach",
"created": "2024-06-15T11:00:00Z",
"type": "TEXT",
"content": "Good effort despite that!",
},
]
async def fake_request(*_args, **_kwargs):
return sample_messages
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(get_activity_messages(activity_id="i123"))
assert "Legs felt heavy today" in result
assert "Good effort despite that!" in result
assert "Niko" in result
assert "Coach" in result
def test_get_activity_messages_error(monkeypatch):
"""Test get_activity_messages handles API errors gracefully."""
async def fake_request(*_args, **_kwargs):
return {"error": True, "message": "Activity not found"}
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(get_activity_messages(activity_id="i999"))
assert "Error fetching activity messages" in result
assert "Activity not found" in result
def test_get_activity_messages_empty(monkeypatch):
"""Test get_activity_messages returns appropriate message when no messages exist."""
async def fake_request(*_args, **_kwargs):
return []
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(get_activity_messages(activity_id="i123"))
assert "No messages found" in result
def test_add_activity_message(monkeypatch):
"""Test add_activity_message posts a message and returns confirmation."""
async def fake_request(*_args, **kwargs):
assert kwargs.get("method") == "POST"
assert kwargs.get("data") == {"content": "Great run!"}
return {"id": 42, "new_chat": None}
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(add_activity_message(activity_id="i123", content="Great run!"))
assert "Successfully added message" in result
assert "42" in result
def test_add_activity_message_missing_id(monkeypatch):
"""Test add_activity_message warns when response has no ID."""
async def fake_request(*_args, **_kwargs):
return {"new_chat": None}
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(add_activity_message(activity_id="i123", content="Hello"))
assert "appears to have been added" in result
assert "verify manually" in result
def test_add_activity_message_unexpected_response(monkeypatch):
"""Test add_activity_message handles unexpected non-dict response."""
async def fake_request(*_args, **_kwargs):
return None
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(add_activity_message(activity_id="i123", content="Hello"))
assert "Unexpected response" in result
def test_add_activity_message_error(monkeypatch):
"""Test add_activity_message handles API errors."""
async def fake_request(*_args, **_kwargs):
return {"error": True, "message": "Not found"}
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
result = asyncio.run(add_activity_message(activity_id="i999", content="Hello"))
assert "Error adding message" in result
def test_get_athlete_power_curves(monkeypatch):
"""
Test get_athlete_power_curves returns formatted power curve data with both seasons.
"""
async def fake_request(*_args, **_kwargs):
return POWER_CURVES_DATA
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.power_curves.make_intervals_request", fake_request
)
result = asyncio.run(
get_athlete_power_curves(
activity_type="Ride",
)
)
assert "Power Curves (Ride):" in result
assert "This season" in result
assert "Last season" in result
assert "5s:" in result
assert "W/kg" in result
assert "i100" in result
def test_get_athlete_power_curves_custom_durations(monkeypatch):
"""
Test get_athlete_power_curves with custom durations returns only those durations.
"""
async def fake_request(*_args, **_kwargs):
return POWER_CURVES_DATA
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.power_curves.make_intervals_request", fake_request
)
result = asyncio.run(
get_athlete_power_curves(
activity_type="Ride",
durations=[5, 60],
)
)
assert "5s:" in result
assert "1m:" in result
# Should not contain durations we didn't request
assert "15s:" not in result
assert "10m:" not in result
def test_get_athlete_power_curves_without_normalised(monkeypatch):
"""
Test get_athlete_power_curves without normalised data excludes W/kg values.
"""
async def fake_request(*_args, **_kwargs):
return POWER_CURVES_DATA
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.power_curves.make_intervals_request", fake_request
)
result = asyncio.run(
get_athlete_power_curves(
activity_type="Ride",
include_normalised=False,
)
)
assert "W/kg" not in result
assert "780W" in result
def test_get_athlete_power_curves_date_validation(monkeypatch):
"""
Test get_athlete_power_curves validates date parameters.
"""
async def fake_request(*_args, **_kwargs):
return POWER_CURVES_DATA
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.power_curves.make_intervals_request", fake_request
)
# Only start_date without end_date should fail
result = asyncio.run(
get_athlete_power_curves(
activity_type="Ride",
start_date="2026-01-01",
)
)
assert "Error" in result
assert "start_date and end_date must be provided together" in result
def test_get_athlete_power_curves_no_curves_selected(monkeypatch):
"""
Test get_athlete_power_curves returns error when no curves selected.
"""
async def fake_request(*_args, **_kwargs):
return POWER_CURVES_DATA
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.power_curves.make_intervals_request", fake_request
)
result = asyncio.run(
get_athlete_power_curves(
activity_type="Ride",
this_season=False,
last_season=False,
)
)
assert "Error" in result
assert "At least one curve must be selected" in result
def test_get_custom_items(monkeypatch):
"""
Test get_custom_items returns a formatted string containing custom item details.
"""
custom_items = [
{"id": 1, "name": "HR Zones", "type": "ZONES", "description": "Heart rate zones"},
{"id": 2, "name": "Power Chart", "type": "FITNESS_CHART", "description": None},
]
async def fake_request(*_args, **_kwargs):
return custom_items
# Patch in both api.client and tools modules to ensure it works
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
)
result = asyncio.run(get_custom_items())
assert "Custom Items:" in result
assert "HR Zones" in result
assert "ZONES" in result
assert "Power Chart" in result
def test_get_custom_item_by_id(monkeypatch):
"""
Test get_custom_item_by_id returns formatted details of a single custom item.
"""
custom_item = {
"id": 1,
"name": "HR Zones",
"type": "ZONES",
"description": "Heart rate zones",
"visibility": "PRIVATE",
"index": 0,
}
async def fake_request(*_args, **_kwargs):
return custom_item
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
)
result = asyncio.run(get_custom_item_by_id(item_id=1))
assert "Custom Item Details:" in result
assert "HR Zones" in result
assert "ZONES" in result
assert "Heart rate zones" in result
assert "PRIVATE" in result
def test_create_custom_item(monkeypatch):
"""
Test create_custom_item returns a success message with formatted item details.
"""
created_item = {
"id": 10,
"name": "New Chart",
"type": "FITNESS_CHART",
"description": "A new fitness chart",
"visibility": "PRIVATE",
}
async def fake_request(*_args, **_kwargs):
return created_item
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
)
result = asyncio.run(
create_custom_item(name="New Chart", item_type="FITNESS_CHART")
)
assert "Successfully created custom item:" in result
assert "New Chart" in result
assert "FITNESS_CHART" in result
def test_create_custom_item_with_string_content(monkeypatch):
"""
Test create_custom_item correctly parses content when passed as a JSON string.
"""
captured: dict = {}
async def fake_request(*_args, **kwargs):
captured["data"] = kwargs.get("data")
return {
"id": 11,
"name": "Activity Field",
"type": "ACTIVITY_FIELD",
"content": {"expression": "icu_training_load"},
}
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
)
result = asyncio.run(
create_custom_item(
name="Activity Field",
item_type="ACTIVITY_FIELD",
content='{"expression": "icu_training_load"}', # type: ignore[arg-type]
)
)
assert "Successfully created custom item:" in result
# Verify the content was parsed from string to dict before being sent
assert isinstance(captured["data"]["content"], dict)
assert captured["data"]["content"]["expression"] == "icu_training_load"
def test_update_custom_item(monkeypatch):
"""
Test update_custom_item returns a success message with formatted item details.
"""
updated_item = {
"id": 1,
"name": "Updated Chart",
"type": "FITNESS_CHART",
"description": "Updated description",
"visibility": "PUBLIC",
}
async def fake_request(*_args, **_kwargs):
return updated_item
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
)
result = asyncio.run(
update_custom_item(item_id=1, name="Updated Chart")
)
assert "Successfully updated custom item:" in result
assert "Updated Chart" in result
assert "PUBLIC" in result
def test_delete_custom_item(monkeypatch):
"""
Test delete_custom_item returns the API response.
"""
async def fake_request(*_args, **_kwargs):
return {}
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
)
result = asyncio.run(delete_custom_item(item_id=1))
assert "Successfully deleted" in result
def test_create_custom_item_with_invalid_json_content(monkeypatch):
"""
Test create_custom_item returns an error message when content is an invalid JSON string.
"""
async def fake_request(*_args, **_kwargs):
return {}
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.custom_items.make_intervals_request", fake_request
)
result = asyncio.run(
create_custom_item(
name="Bad Item",
item_type="FITNESS_CHART",
content="not valid json", # type: ignore[arg-type]
)
)
assert "Error: content must be valid JSON when passed as a string." in result
# ---------------------------------------------------------------------------
# Gear tools
# ---------------------------------------------------------------------------
def test_get_gear_list(monkeypatch):
"""
Test get_gear_list returns a formatted catalog with id, type, name and stats.
"""
_reset_gear_cache()
sample_gear = [
{
"id": "b1",
"type": "Bike",
"name": "Litening Air",
"default_for_type": "Ride",
"activities": 100,
"distance": 4_155_700,
"retired": False,
},
{
"id": "b2",
"type": "Bike",
"name": "Retired bike",
"activities": 50,
"distance": 2_000_000,
"retired": True,
},
]
async def fake_request(*_args, **_kwargs):
return sample_gear
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.gear.make_intervals_request", fake_request
)
result = asyncio.run(get_gear_list())
assert "Gear catalog for athlete i1:" in result
assert "Litening Air" in result
assert "b1" in result
assert "Retired bike" in result
assert "yes" in result # retired flag rendered
assert "Ride" in result # default_for_type rendered
def test_get_gear_list_empty(monkeypatch):
"""
Test get_gear_list returns an informative message when no gear is configured.
"""
_reset_gear_cache()
async def fake_request(*_args, **_kwargs):
return []
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.gear.make_intervals_request", fake_request
)
result = asyncio.run(get_gear_list())
assert "No gear found" in result
def test_get_gear_list_cache_and_refresh(monkeypatch):
"""
Test that get_gear_list caches the catalog and that refresh=True busts the cache.
"""
_reset_gear_cache()
call_count = {"n": 0}
sample_gear = [
{
"id": "b1",
"type": "Bike",
"name": "Litening Air",
"activities": 100,
"distance": 4_155_700,
}
]
async def fake_request(*_args, **_kwargs):
call_count["n"] += 1
return sample_gear
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.gear.make_intervals_request", fake_request
)
# First call: cache cold, one API hit expected.
asyncio.run(get_gear_list())
assert call_count["n"] == 1
# Second call: cache warm, no additional API hit.
asyncio.run(get_gear_list())
assert call_count["n"] == 1
# refresh=True busts the cache and triggers a fresh fetch.
asyncio.run(get_gear_list(refresh=True))
assert call_count["n"] == 2
def test_get_activity_details_resolves_gear_name(monkeypatch):
"""
Test get_activity_details injects the resolved gear name into the formatted output
when the activity payload contains a gear_id.
"""
_reset_gear_cache()
activity = {
"name": "Morning Ride",
"id": 123,
"type": "Ride",
"startTime": "2024-01-01T08:00:00Z",
"distance": 1000,
"duration": 3600,
"gear_id": "b1",
}
gear_catalog = [{"id": "b1", "type": "Bike", "name": "Litening Air"}]
async def fake_request(url=None, **_kwargs):
# The activity endpoint and the gear endpoint share the same fake
# request; route by URL pattern.
if url and "/gear" in url:
return gear_catalog
return activity
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
monkeypatch.setattr(
"intervals_mcp_server.tools.gear.make_intervals_request", fake_request
)
# get_activity_details does not accept athlete_id; gear resolution falls
# back to the configured ATHLETE_ID, which is unset under CI. Provide one.
monkeypatch.setattr(gear_module.config, "athlete_id", "1")
result = asyncio.run(get_activity_details(123))
assert "Activity: Morning Ride" in result
assert "Gear:" in result
assert "Name: Litening Air" in result
assert "ID: b1" in result
def test_get_activities_resolves_gear_name(monkeypatch):
"""
Test get_activities injects resolved gear names for each activity in the list.
"""
_reset_gear_cache()
activities = [
{
"name": "Ride 1",
"id": 1,
"type": "Ride",
"startTime": "2024-01-01T08:00:00Z",
"distance": 1000,
"duration": 3600,
"gear_id": "b1",
},
{
"name": "Ride 2",
"id": 2,
"type": "Ride",
"startTime": "2024-01-02T08:00:00Z",
"distance": 2000,
"duration": 5400,
"gear_id": "b2",
},
]
gear_catalog = [
{"id": "b1", "type": "Bike", "name": "Litening Air"},
{"id": "b2", "type": "Bike", "name": "S-Works Tarmac SL8"},
]
async def fake_request(url=None, **_kwargs):
if url and "/gear" in url:
return gear_catalog
return activities
monkeypatch.setattr("intervals_mcp_server.api.client.make_intervals_request", fake_request)
monkeypatch.setattr(
"intervals_mcp_server.tools.activities.make_intervals_request", fake_request
)
monkeypatch.setattr(
"intervals_mcp_server.tools.gear.make_intervals_request", fake_request
)
result = asyncio.run(get_activities(limit=2, include_unnamed=True))
assert "Ride 1" in result
assert "Ride 2" in result
assert "Name: Litening Air" in result
assert "Name: S-Works Tarmac SL8" in result