"""Integration test: bearer token → corretto env URL per ogni exchange. Usa il constructor spy pattern: intercetta __init__ del client per catturare i kwargs passati (testnet/paper) e verificare che il bearer token corrisponda all'env atteso. Nota: la risposta HTTP può essere qualsiasi — non ci interessa. Ci interessa solo che il costruttore venga chiamato con i parametri corretti. """ from __future__ import annotations import importlib import pytest from fastapi.testclient import TestClient # ── Fixtures ──────────────────────────────────────────────────────────────── @pytest.fixture def app(monkeypatch): from tests.unit.test_settings import _minimal_env for k, v in _minimal_env().items(): monkeypatch.setenv(k, v) # Re-import fresco per evitare Settings cached con env vuoto import importlib import sys for mod_name in list(sys.modules.keys()): if "cerbero_mcp" in mod_name: sys.modules.pop(mod_name, None) from cerbero_mcp.__main__ import _make_app from cerbero_mcp.settings import Settings return _make_app(Settings()) def _bearer_test(): return {"Authorization": "Bearer t_test_123"} def _bearer_live(): return {"Authorization": "Bearer t_live_456"} # ── Spy helpers ────────────────────────────────────────────────────────────── def _spy_constructor(monkeypatch, module_path: str, cls_name: str, capture: dict): """Sostituisce __init__ con uno spy che cattura gli args/kwargs. Supporta sia dataclass (init generato) sia classi normali. Dopo la cattura, chiama il vero __init__ (fallback no-op se fallisce). """ mod = importlib.import_module(module_path) real_cls = getattr(mod, cls_name) real_init = real_cls.__init__ def spy_init(self, *args, **kwargs): capture["args"] = args capture["kwargs"] = kwargs try: real_init(self, *args, **kwargs) except Exception: # Se il costruttore fallisce (network, SDK unavailable) non importa: # la capture è già avvenuta. pass monkeypatch.setattr(real_cls, "__init__", spy_init) def _force_rebuild(app): """Svuota la cache del registry per forzare rebuild al prossimo request.""" app.state.registry._clients.clear() # ── Deribit ────────────────────────────────────────────────────────────────── def test_deribit_testnet_bearer_constructs_testnet_client(app, monkeypatch): capture = {} _spy_constructor(monkeypatch, "cerbero_mcp.exchanges.deribit.client", "DeribitClient", capture) _force_rebuild(app) c = TestClient(app, raise_server_exceptions=False) c.post("/mcp-deribit/tools/is_testnet", headers=_bearer_test()) assert capture, "DeribitClient constructor non chiamato" assert capture["kwargs"].get("testnet") is True, ( f"atteso testnet=True, kwargs={capture['kwargs']}" ) def test_deribit_mainnet_bearer_constructs_mainnet_client(app, monkeypatch): capture = {} _spy_constructor(monkeypatch, "cerbero_mcp.exchanges.deribit.client", "DeribitClient", capture) _force_rebuild(app) c = TestClient(app, raise_server_exceptions=False) c.post("/mcp-deribit/tools/is_testnet", headers=_bearer_live()) assert capture, "DeribitClient constructor non chiamato" assert capture["kwargs"].get("testnet") is False, ( f"atteso testnet=False, kwargs={capture['kwargs']}" ) # ── Bybit ──────────────────────────────────────────────────────────────────── def test_bybit_testnet_bearer(app, monkeypatch): capture = {} _spy_constructor(monkeypatch, "cerbero_mcp.exchanges.bybit.client", "BybitClient", capture) _force_rebuild(app) c = TestClient(app, raise_server_exceptions=False) c.post("/mcp-bybit/tools/environment_info", headers=_bearer_test()) assert capture, "BybitClient constructor non chiamato" assert capture["kwargs"].get("testnet") is True, ( f"atteso testnet=True, kwargs={capture['kwargs']}" ) def test_bybit_mainnet_bearer(app, monkeypatch): capture = {} _spy_constructor(monkeypatch, "cerbero_mcp.exchanges.bybit.client", "BybitClient", capture) _force_rebuild(app) c = TestClient(app, raise_server_exceptions=False) c.post("/mcp-bybit/tools/environment_info", headers=_bearer_live()) assert capture, "BybitClient constructor non chiamato" assert capture["kwargs"].get("testnet") is False, ( f"atteso testnet=False, kwargs={capture['kwargs']}" ) # ── Hyperliquid ────────────────────────────────────────────────────────────── def test_hyperliquid_testnet_bearer(app, monkeypatch): capture = {} _spy_constructor(monkeypatch, "cerbero_mcp.exchanges.hyperliquid.client", "HyperliquidClient", capture) _force_rebuild(app) c = TestClient(app, raise_server_exceptions=False) c.post("/mcp-hyperliquid/tools/environment_info", headers=_bearer_test()) assert capture, "HyperliquidClient constructor non chiamato" assert capture["kwargs"].get("testnet") is True, ( f"atteso testnet=True, kwargs={capture['kwargs']}" ) def test_hyperliquid_mainnet_bearer(app, monkeypatch): capture = {} _spy_constructor(monkeypatch, "cerbero_mcp.exchanges.hyperliquid.client", "HyperliquidClient", capture) _force_rebuild(app) c = TestClient(app, raise_server_exceptions=False) c.post("/mcp-hyperliquid/tools/environment_info", headers=_bearer_live()) assert capture, "HyperliquidClient constructor non chiamato" assert capture["kwargs"].get("testnet") is False, ( f"atteso testnet=False, kwargs={capture['kwargs']}" ) # ── Alpaca ─────────────────────────────────────────────────────────────────── def test_alpaca_testnet_bearer_uses_paper(app, monkeypatch): """Alpaca usa paper=True al posto di testnet=True.""" capture = {} _spy_constructor(monkeypatch, "cerbero_mcp.exchanges.alpaca.client", "AlpacaClient", capture) _force_rebuild(app) c = TestClient(app, raise_server_exceptions=False) c.post("/mcp-alpaca/tools/environment_info", headers=_bearer_test()) assert capture, "AlpacaClient constructor non chiamato" assert capture["kwargs"].get("paper") is True, ( f"atteso paper=True, kwargs={capture['kwargs']}" ) def test_alpaca_mainnet_bearer_uses_paper_false(app, monkeypatch): capture = {} _spy_constructor(monkeypatch, "cerbero_mcp.exchanges.alpaca.client", "AlpacaClient", capture) _force_rebuild(app) c = TestClient(app, raise_server_exceptions=False) c.post("/mcp-alpaca/tools/environment_info", headers=_bearer_live()) assert capture, "AlpacaClient constructor non chiamato" assert capture["kwargs"].get("paper") is False, ( f"atteso paper=False, kwargs={capture['kwargs']}" ) # ── Auth sanity ────────────────────────────────────────────────────────────── def test_no_bearer_returns_401(app): """Senza bearer header qualsiasi /mcp-*/tools/* deve ritornare 401.""" c = TestClient(app, raise_server_exceptions=False) r = c.post("/mcp-deribit/tools/is_testnet") assert r.status_code == 401, f"atteso 401, got {r.status_code}" def test_invalid_bearer_returns_401(app): c = TestClient(app, raise_server_exceptions=False) r = c.post( "/mcp-deribit/tools/is_testnet", headers={"Authorization": "Bearer wrong_token_xyz"}, ) assert r.status_code == 401, f"atteso 401, got {r.status_code}" # ── Macro (read-only, env-agnostic) ───────────────────────────────────────── def test_macro_works_with_either_bearer(app, monkeypatch): """Macro è env-agnostic. Entrambi i bearer devono costruire il client.""" import cerbero_mcp.exchanges.macro.client as macro_mod captures: list[dict] = [] real_init = macro_mod.MacroClient.__init__ def spy_init(self, *args, **kwargs): captures.append({"args": args, "kwargs": kwargs}) real_init(self, *args, **kwargs) monkeypatch.setattr(macro_mod.MacroClient, "__init__", spy_init) _force_rebuild(app) c = TestClient(app, raise_server_exceptions=False) c.post("/mcp-macro/tools/get_macro_calendar", headers=_bearer_test()) c.post("/mcp-macro/tools/get_macro_calendar", headers=_bearer_live()) # Almeno una costruzione (registry può cachare dopo la prima) assert len(captures) >= 1, f"MacroClient constructor non chiamato, captures={captures}"