Files
Cerbero-mcp/tests/integration/test_env_routing.py
T
2026-05-01 08:51:40 +02:00

244 lines
9.4 KiB
Python

"""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 contextlib
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)
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", "X-Bot-Tag": "test-bot"}
def _bearer_live():
return {"Authorization": "Bearer t_live_456", "X-Bot-Tag": "test-bot"}
# ── 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
# Se il costruttore fallisce (network, SDK unavailable) non importa:
# la capture è già avvenuta.
with contextlib.suppress(Exception):
real_init(self, *args, **kwargs)
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}"