Fork intervals-mcp-server: native OAuth + streamable-HTTP, no monkeypatch
build-image / build (push) Failing after 18s
build-image / build (push) Failing after 18s
- 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>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
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"]
|
||||
Reference in New Issue
Block a user