Files
intervalsicu-mcp/tests/test_make_intervals_request.py
Chris Farhood 935abf86d4
build-image / build (push) Failing after 18s
Fork intervals-mcp-server: native OAuth + streamable-HTTP, no monkeypatch
- Bump mcp[cli] 1.22 -> 1.28.1 (negotiates MCP protocol 2025-11-25, matching
  current Claude clients; the old 2025-06-18 server never got a tools/list on
  the connector surface).
- Bake transport config into code: stateless_http + json_response for HTTP
  (single JSON body instead of a 34KB SSE stream, which the connector pipeline
  handles far more reliably).
- Bake Authentik OAuth (AuthSettings + JWT TokenVerifier) into intervals_mcp_server.auth,
  configured from MCP_ISSUER/MCP_RESOURCE/MCP_JWKS_URI/MCP_CLIENT_ID — removes the
  runtime FastMCP.__init__ monkeypatch from the k8s deployment command.
- Accept token audience with/without trailing slash (RFC 8707 clients use the
  slash-normalised resource metadata value).
- Dockerfile CMD runs the module (transport via MCP_TRANSPORT); add .gitea CI to
  build+push the image to git.farh.net/farhoodlabs/intervalsicu-mcp.

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

102 lines
3.3 KiB
Python

"""
Unit tests for the make_intervals_request function in intervals_mcp_server.server.
These tests focus on error handling, particularly the scenario where the API returns invalid JSON.
Mock classes are used to simulate httpx responses and client behavior.
"""
import asyncio
import logging
import os
import pathlib
import sys
from json import JSONDecodeError
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 import server # pylint: disable=wrong-import-position
from intervals_mcp_server.api import client as api_client # pylint: disable=wrong-import-position
from intervals_mcp_server.config import Config # pylint: disable=wrong-import-position
class MockBadJSONResponse:
"""
Simulates an httpx response object that returns invalid JSON content.
Used to test error handling for JSONDecodeError in make_intervals_request.
"""
def __init__(self):
self.content = b"bad"
self.status_code = 200
def raise_for_status(self):
"""Mock raise_for_status that does nothing."""
return None
def json(self):
"""Raise JSONDecodeError to simulate invalid JSON."""
raise JSONDecodeError("Expecting value", "bad", 0)
class MockAsyncClient:
"""
Simulates an httpx.AsyncClient for use in monkeypatching.
Always returns a MockBadJSONResponse from get().
"""
def __init__(self, *_args, **_kwargs):
# Accept any arguments to match httpx.AsyncClient's interface
self.is_closed = False
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc, tb):
pass
async def get(self, _url, **_kwargs):
"""Mock get method that returns MockBadJSONResponse."""
return MockBadJSONResponse()
async def request(self, *_args, **_kwargs):
"""Mock request method that returns MockBadJSONResponse."""
return MockBadJSONResponse()
async def aclose(self):
"""Simulate closing the AsyncClient."""
self.is_closed = True
def test_make_intervals_request_bad_json(monkeypatch, caplog):
"""
Test that make_intervals_request returns an error dict when the response contains invalid JSON.
Ensures proper logging and error message content.
"""
monkeypatch.setenv("API_KEY", "test")
monkeypatch.setenv("ATHLETE_ID", "i1")
# Reset the singleton so config picks up the monkeypatched env vars
monkeypatch.setattr("intervals_mcp_server.config._config_instance", None)
monkeypatch.setattr(server, "httpx_client", MockAsyncClient())
monkeypatch.setattr(
api_client,
"get_config",
lambda: Config(
api_key="test",
athlete_id="i1",
intervals_api_base_url="https://intervals.icu/api/v1",
user_agent="test-agent",
),
)
# Ensure the config singleton has an API key, regardless of test execution order
from intervals_mcp_server.config import get_config # pylint: disable=import-outside-toplevel
monkeypatch.setattr(get_config(), "api_key", "test")
with caplog.at_level(logging.ERROR):
result = asyncio.run(server.make_intervals_request("/bad"))
assert result["error"] is True
assert "Invalid JSON in response" in result["message"]