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