8ecc1a24a9
- /health/ready: ping di tutti i client (exchange, env) cached con timeout 2s, status ready|degraded|not_ready, opt-in 503 via READY_FAILS_ON_DEGRADED. - Middleware mcp.request: 1 riga JSON per HTTP request con request_id, method, path, status_code, duration_ms, actor, bot_tag, exchange, tool, client_ip, user_agent. - request_id propagato in request.state, audit log e error envelope per correlazione cross-cutting. - Aggiunto async health() come probe minimo a bybit/alpaca/macro/ sentiment/deribit (hyperliquid lo aveva già). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
183 lines
5.0 KiB
Python
183 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
from cerbero_mcp.server import build_app
|
|
return build_app(
|
|
testnet_token="tk_test",
|
|
mainnet_token="tk_live",
|
|
title="Test",
|
|
version="2.0.0",
|
|
)
|
|
|
|
|
|
def test_apidocs_served(app):
|
|
c = TestClient(app)
|
|
r = c.get("/apidocs")
|
|
assert r.status_code == 200
|
|
assert "swagger" in r.text.lower()
|
|
|
|
|
|
def test_openapi_json_served(app):
|
|
c = TestClient(app)
|
|
r = c.get("/openapi.json")
|
|
assert r.status_code == 200
|
|
spec = r.json()
|
|
assert spec["info"]["title"] == "Test"
|
|
# securityScheme BearerAuth presente
|
|
assert "BearerAuth" in spec["components"]["securitySchemes"]
|
|
assert spec["components"]["securitySchemes"]["BearerAuth"]["scheme"] == "bearer"
|
|
|
|
|
|
def test_redoc_disabled(app):
|
|
c = TestClient(app)
|
|
r = c.get("/redoc")
|
|
assert r.status_code == 404
|
|
|
|
|
|
def test_default_docs_path_disabled(app):
|
|
c = TestClient(app)
|
|
r = c.get("/docs")
|
|
assert r.status_code == 404
|
|
|
|
|
|
def test_health_endpoint(app):
|
|
c = TestClient(app)
|
|
r = c.get("/health")
|
|
assert r.status_code == 200
|
|
j = r.json()
|
|
assert j["status"] == "healthy"
|
|
assert j["version"] == "2.0.0"
|
|
assert "uptime_seconds" in j
|
|
assert "data_timestamp" in j
|
|
|
|
|
|
def test_x_duration_ms_header(app):
|
|
c = TestClient(app)
|
|
r = c.get("/health")
|
|
assert "X-Duration-Ms" in r.headers
|
|
|
|
|
|
def test_health_ready_empty_registry(app):
|
|
"""Senza registry il readiness ritorna not_ready ma HTTP 200."""
|
|
c = TestClient(app)
|
|
r = c.get("/health/ready")
|
|
assert r.status_code == 200
|
|
j = r.json()
|
|
assert j["status"] == "not_ready"
|
|
assert j["clients"] == []
|
|
assert j["version"] == "2.0.0"
|
|
|
|
|
|
def test_health_ready_all_healthy(app):
|
|
"""Registry con stub client healthy → status=ready."""
|
|
from cerbero_mcp.client_registry import ClientRegistry
|
|
|
|
class _StubOk:
|
|
async def health(self):
|
|
return {"status": "ok"}
|
|
|
|
async def _builder(exchange, env): # pragma: no cover - non chiamato
|
|
return _StubOk()
|
|
|
|
reg = ClientRegistry(builder=_builder)
|
|
reg._clients[("deribit", "testnet")] = _StubOk()
|
|
reg._clients[("bybit", "mainnet")] = _StubOk()
|
|
app.state.registry = reg
|
|
|
|
c = TestClient(app)
|
|
r = c.get("/health/ready")
|
|
assert r.status_code == 200
|
|
j = r.json()
|
|
assert j["status"] == "ready"
|
|
assert len(j["clients"]) == 2
|
|
for entry in j["clients"]:
|
|
assert entry["healthy"] is True
|
|
assert "duration_ms" in entry
|
|
|
|
|
|
def test_health_ready_degraded_on_error(app):
|
|
"""Registry con almeno un client che fa raise → status=degraded."""
|
|
from cerbero_mcp.client_registry import ClientRegistry
|
|
|
|
class _StubOk:
|
|
async def health(self):
|
|
return {"status": "ok"}
|
|
|
|
class _StubFail:
|
|
async def health(self):
|
|
raise RuntimeError("boom")
|
|
|
|
async def _builder(exchange, env): # pragma: no cover - non chiamato
|
|
return _StubOk()
|
|
|
|
reg = ClientRegistry(builder=_builder)
|
|
reg._clients[("deribit", "testnet")] = _StubOk()
|
|
reg._clients[("bybit", "mainnet")] = _StubFail()
|
|
app.state.registry = reg
|
|
|
|
c = TestClient(app)
|
|
r = c.get("/health/ready")
|
|
assert r.status_code == 200
|
|
j = r.json()
|
|
assert j["status"] == "degraded"
|
|
fail = next(c for c in j["clients"] if c["exchange"] == "bybit")
|
|
assert fail["healthy"] is False
|
|
assert "RuntimeError" in fail["error"]
|
|
|
|
|
|
def test_health_ready_503_when_fail_on_degraded(app, monkeypatch):
|
|
"""READY_FAILS_ON_DEGRADED=true → HTTP 503 quando degraded."""
|
|
from cerbero_mcp.client_registry import ClientRegistry
|
|
|
|
class _StubFail:
|
|
async def health(self):
|
|
raise RuntimeError("boom")
|
|
|
|
async def _builder(exchange, env): # pragma: no cover - non chiamato
|
|
return _StubFail()
|
|
|
|
reg = ClientRegistry(builder=_builder)
|
|
reg._clients[("deribit", "testnet")] = _StubFail()
|
|
app.state.registry = reg
|
|
|
|
monkeypatch.setenv("READY_FAILS_ON_DEGRADED", "true")
|
|
c = TestClient(app)
|
|
r = c.get("/health/ready")
|
|
assert r.status_code == 503
|
|
assert r.json()["status"] == "degraded"
|
|
|
|
|
|
def test_health_ready_no_probe_method(app):
|
|
"""Client senza health/is_testnet → marcato healthy con note."""
|
|
from cerbero_mcp.client_registry import ClientRegistry
|
|
|
|
class _StubBare:
|
|
pass
|
|
|
|
async def _builder(exchange, env): # pragma: no cover - non chiamato
|
|
return _StubBare()
|
|
|
|
reg = ClientRegistry(builder=_builder)
|
|
reg._clients[("foo", "testnet")] = _StubBare()
|
|
app.state.registry = reg
|
|
|
|
c = TestClient(app)
|
|
r = c.get("/health/ready")
|
|
assert r.status_code == 200
|
|
j = r.json()
|
|
assert j["status"] == "ready"
|
|
assert j["clients"][0]["note"] == "no probe method"
|
|
|
|
|
|
def test_health_ready_in_whitelist_no_auth(app):
|
|
"""/health/ready non richiede bearer."""
|
|
c = TestClient(app)
|
|
# Nessun Authorization header → 200 (whitelist)
|
|
r = c.get("/health/ready")
|
|
assert r.status_code == 200
|