f067f9639a
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>
77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
"""
|
|
Tests for the per-request credential resolver.
|
|
|
|
Identity comes from the OAuth token; credentials from the enabled store record;
|
|
with a fallback to env config only when there is no auth context (local/stdio).
|
|
"""
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
from intervals_mcp_server import credentials
|
|
from intervals_mcp_server.config import Config
|
|
from intervals_mcp_server.credentials import CredentialError, resolve_caller_credentials
|
|
|
|
|
|
def _token(sub):
|
|
return SimpleNamespace(subject=sub)
|
|
|
|
|
|
def test_uses_authenticated_users_stored_creds(monkeypatch):
|
|
monkeypatch.setattr(credentials, "get_access_token", lambda: _token("sub1"))
|
|
|
|
async def _creds(sub):
|
|
assert sub == "sub1"
|
|
return ("i123", "user-key")
|
|
|
|
monkeypatch.setattr(credentials.store, "get_active_credentials", _creds)
|
|
assert asyncio.run(resolve_caller_credentials()) == ("i123", "user-key")
|
|
|
|
|
|
def test_authenticated_but_not_set_up_raises(monkeypatch):
|
|
monkeypatch.setattr(credentials, "get_access_token", lambda: _token("sub1"))
|
|
|
|
async def _none(_sub):
|
|
return None
|
|
|
|
monkeypatch.setattr(credentials.store, "get_active_credentials", _none)
|
|
with pytest.raises(CredentialError, match="isn't ready yet"):
|
|
asyncio.run(resolve_caller_credentials())
|
|
|
|
|
|
def test_no_token_falls_back_to_env_config(monkeypatch):
|
|
monkeypatch.setattr(credentials, "get_access_token", lambda: None)
|
|
monkeypatch.setattr(
|
|
credentials,
|
|
"get_config",
|
|
lambda: Config(api_key="envkey", athlete_id="i999", intervals_api_base_url="x", user_agent="t"),
|
|
)
|
|
assert asyncio.run(resolve_caller_credentials()) == ("i999", "envkey")
|
|
|
|
|
|
def test_get_access_token_raising_is_treated_as_no_context(monkeypatch):
|
|
# outside a request the SDK accessor may raise; that must fall back to env config
|
|
def _boom():
|
|
raise RuntimeError("no request context")
|
|
|
|
monkeypatch.setattr(credentials, "get_access_token", _boom)
|
|
monkeypatch.setattr(
|
|
credentials,
|
|
"get_config",
|
|
lambda: Config(api_key="envkey", athlete_id="i999", intervals_api_base_url="x", user_agent="t"),
|
|
)
|
|
assert asyncio.run(resolve_caller_credentials()) == ("i999", "envkey")
|
|
|
|
|
|
def test_no_token_no_env_raises(monkeypatch):
|
|
monkeypatch.setattr(credentials, "get_access_token", lambda: None)
|
|
monkeypatch.setattr(
|
|
credentials,
|
|
"get_config",
|
|
lambda: Config(api_key="", athlete_id="", intervals_api_base_url="x", user_agent="t"),
|
|
)
|
|
with pytest.raises(CredentialError, match="Not authenticated"):
|
|
asyncio.run(resolve_caller_credentials())
|