Files
Cerbero-mcp/tests/unit/test_request_log.py
AdrianoDev 8ecc1a24a9 feat(V2): /health/ready con ping client + middleware request log strutturato + request_id correlation
- /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>
2026-05-01 09:03:28 +02:00

122 lines
3.9 KiB
Python

from __future__ import annotations
import logging
import pytest
from fastapi.testclient import TestClient
from starlette.requests import Request as StarletteRequest
@pytest.fixture
def app(monkeypatch):
from tests.unit.test_settings import _minimal_env
for k, v in _minimal_env().items():
monkeypatch.setenv(k, v)
from cerbero_mcp.__main__ import _make_app
from cerbero_mcp.settings import Settings
return _make_app(Settings())
@pytest.fixture
def request_log_caplog(caplog):
"""Caplog per logger mcp.request: aggiunge l'handler caplog direttamente
al logger, bypassando ``propagate=False`` settato da common/logging.py.
"""
lg = logging.getLogger("mcp.request")
lg.addHandler(caplog.handler)
lg.setLevel(logging.INFO)
caplog.set_level(logging.INFO, logger="mcp.request")
try:
yield caplog
finally:
lg.removeHandler(caplog.handler)
def _request_records(caplog):
return [rec for rec in caplog.records if rec.name == "mcp.request"]
def test_request_log_emits_for_health(app, request_log_caplog):
c = TestClient(app)
r = c.get("/health")
assert r.status_code == 200
records = _request_records(request_log_caplog)
assert any(getattr(rec, "path", None) == "/health" for rec in records)
def test_request_log_includes_request_id(app, request_log_caplog):
c = TestClient(app)
c.get("/health")
records = _request_records(request_log_caplog)
assert records, "expected at least one mcp.request record"
for rec in records:
rid = getattr(rec, "request_id", None)
assert rid and isinstance(rid, str) and len(rid) >= 16
def test_request_log_includes_method_status_duration(app, request_log_caplog):
c = TestClient(app)
c.get("/health")
records = _request_records(request_log_caplog)
rec = next(rec for rec in records if getattr(rec, "path", None) == "/health")
assert getattr(rec, "method", None) == "GET"
assert getattr(rec, "status_code", None) == 200
assert isinstance(getattr(rec, "duration_ms", None), int | float)
def test_request_log_includes_actor_and_bot_tag_on_protected(
app, request_log_caplog
):
"""Su path autenticato actor/bot_tag/exchange/tool sono propagati."""
c = TestClient(app)
c.post(
"/mcp-deribit/tools/is_testnet",
headers={
"Authorization": "Bearer t_test_123",
"X-Bot-Tag": "scanner-x",
},
json={},
)
records = _request_records(request_log_caplog)
rec = next(
rec
for rec in records
if getattr(rec, "path", None) == "/mcp-deribit/tools/is_testnet"
)
assert getattr(rec, "actor", None) == "testnet"
assert getattr(rec, "bot_tag", None) == "scanner-x"
assert getattr(rec, "exchange", None) == "deribit"
assert getattr(rec, "tool", None) == "is_testnet"
def test_request_log_unauthorized_does_not_have_actor(
app, request_log_caplog
):
"""Senza bearer, request log emette comunque ma senza actor/bot_tag."""
c = TestClient(app)
c.post("/mcp-deribit/tools/is_testnet", json={})
records = _request_records(request_log_caplog)
rec = next(
rec
for rec in records
if getattr(rec, "path", None) == "/mcp-deribit/tools/is_testnet"
)
assert getattr(rec, "status_code", None) == 401
assert getattr(rec, "actor", None) is None
assert getattr(rec, "exchange", None) == "deribit"
def test_request_id_in_state_for_handlers(app):
"""Verifica che request.state.request_id sia disponibile a handler."""
@app.get("/__test_state")
def _state_handler(request: StarletteRequest) -> dict:
return {"rid": request.state.request_id}
c = TestClient(app)
r = c.get(
"/__test_state",
headers={"Authorization": "Bearer t_test_123", "X-Bot-Tag": "x"},
)
assert r.status_code == 200, f"got {r.status_code}: {r.text[:500]}"
assert r.json()["rid"]