trymemcp/tests/conftest.py

145 lines
4.8 KiB
Python
Raw Normal View History

2026-11-12 03:23:16 +00:00
"""Shared pytest fixtures for testing the TryMe FastMCP HTTP server."""
from __future__ import annotations
import json
from dataclasses import dataclass, field
from typing import Any
import httpx
import pytest
from app.client import TryMeClient
from app.config import Settings
from app.main import get_client, set_client
@dataclass
class RecordedCall:
"""Captured upstream HTTP request made by TryMeClient."""
method: str
path: str
query: dict[str, str]
headers: dict[str, str]
body: Any
@dataclass
class UpstreamMockState:
"""Stateful recorder and configurable responder for MockTransport."""
calls: list[RecordedCall] = field(default_factory=list)
next_status: int | None = None
next_json: Any = None
next_headers: dict[str, str] = field(default_factory=dict)
@pytest.fixture
def mock_upstream() -> Any:
"""Install a MockTransport-backed TryMeClient and restore original client afterward."""
state = UpstreamMockState()
original_client = get_client()
async def handler(request: httpx.Request) -> httpx.Response:
raw_body = request.content.decode("utf-8") if request.content else ""
parsed_body: Any = None
if raw_body:
try:
parsed_body = json.loads(raw_body)
except Exception:
parsed_body = raw_body
state.calls.append(
RecordedCall(
method=request.method,
path=request.url.path,
query=dict(request.url.params),
headers={k.lower(): v for k, v in request.headers.items()},
body=parsed_body,
)
)
if state.next_status is not None:
status = state.next_status
payload = state.next_json
resp_headers = dict(state.next_headers)
state.next_status = None
state.next_json = None
state.next_headers.clear()
if status == 204 or (300 <= status < 400 and payload is None):
return httpx.Response(status_code=status, headers=resp_headers)
return httpx.Response(status_code=status, json=payload, headers=resp_headers)
# Default realistic responses by method + path
path = request.url.path
method = request.method.upper()
if path == "/health":
return httpx.Response(200, json={"status": "ok", "service": "trymeapp", "version": "0.2"})
if path == "/api/v1/auth/config":
return httpx.Response(
200,
json={
"auth0_enabled": True,
"auth0_domain": "example.us.auth0.com",
"auth0_client_id": "client123",
"auth0_audience": "",
"dev_auth_enabled": True,
"admin_unclaimed": False,
},
)
if path in ("/api/v1/auth/login", "/api/v1/auth/callback"):
return httpx.Response(
302,
headers={"location": "https://example.us.auth0.com/authorize"},
)
if path == "/api/v1/auth/dev-login":
return httpx.Response(
200,
json={
"access_token": "dev-jwt-token",
"token_type": "bearer",
"user": {"id": 1, "nickname": parsed_body.get("nickname", "tester")},
},
)
if method in ("DELETE", "POST") and path in (
"/api/v1/auth/logout",
):
return httpx.Response(204)
if method == "DELETE" and (
path.startswith("/api/v1/quizzes/") or path.startswith("/api/v1/admin/quizzes/")
):
return httpx.Response(204)
if path in (
"/api/v1/users/me/scores",
"/api/v1/users/me/api-keys",
"/api/v1/quizzes",
"/api/v1/favourites",
"/api/v1/admin/users",
"/api/v1/admin/quizzes",
) and method == "GET":
return httpx.Response(200, json=[{"ok": True, "path": path}])
if path.startswith("/api/v1/favourites") and method in ("POST", "PUT", "DELETE"):
return httpx.Response(200, json=[{"favourite_id": 1, "position": 0, "quiz": {"id": 10}}])
return httpx.Response(200, json={"ok": True, "method": method, "path": path, "body": parsed_body})
test_settings = Settings(
version="0.1",
mcp_host="0.0.0.0",
mcp_port=8000,
mcp_path="/mcp",
tryme_base_url="https://tryme.tpk.pw",
tryme_timeout_seconds=10.0,
tryme_api_key="env-default-key",
tryme_bearer_token=None,
tryme_session_cookie=None,
)
client = TryMeClient(config=test_settings, transport=httpx.MockTransport(handler))
set_client(client)
try:
yield state
finally:
set_client(original_client)