"""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)