From 1ca1687c9b325ff0d4af7106ce99fad6a091a603 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 1 May 2026 14:46:47 +0000 Subject: [PATCH] feat(V2): Deribit credenziali per env (CLIENT_ID/SECRET _TESTNET / _LIVE) DeribitSettings ora supporta coppie credenziali distinte per testnet e mainnet via DERIBIT_CLIENT_ID_TESTNET/_LIVE e DERIBIT_CLIENT_SECRET_TESTNET/_LIVE. Le coppie env-specifiche prevalgono sulla coppia base DERIBIT_CLIENT_ID/DERIBIT_CLIENT_SECRET (mantenuta per backward compat). build_client risolve la coppia giusta tramite settings.deribit.credentials(env); ValueError esplicito se nessuna coppia configurata per l'env richiesto. +4 test (legacy single, per-env, override, missing). Fix anche isolation da .env reale via monkeypatch.chdir(tmp_path). Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 6 +++ src/cerbero_mcp/exchanges/__init__.py | 5 +- src/cerbero_mcp/settings.py | 25 ++++++++- tests/unit/test_settings.py | 73 ++++++++++++++++++++++++++- 4 files changed, 104 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 6d76d57..2b6efab 100644 --- a/.env.example +++ b/.env.example @@ -17,8 +17,14 @@ TESTNET_TOKEN= MAINNET_TOKEN= # ─── EXCHANGE — DERIBIT ─────────────────────────────────── +# Coppia singola (usata sia per testnet sia per mainnet): DERIBIT_CLIENT_ID= DERIBIT_CLIENT_SECRET= +# Oppure coppie distinte per env (prevalgono se valorizzate): +# DERIBIT_CLIENT_ID_TESTNET= +# DERIBIT_CLIENT_SECRET_TESTNET= +# DERIBIT_CLIENT_ID_LIVE= +# DERIBIT_CLIENT_SECRET_LIVE= DERIBIT_URL_LIVE=https://www.deribit.com/api/v2 DERIBIT_URL_TESTNET=https://test.deribit.com/api/v2 DERIBIT_MAX_LEVERAGE=3 diff --git a/src/cerbero_mcp/exchanges/__init__.py b/src/cerbero_mcp/exchanges/__init__.py index 6391959..f1e9472 100644 --- a/src/cerbero_mcp/exchanges/__init__.py +++ b/src/cerbero_mcp/exchanges/__init__.py @@ -15,9 +15,10 @@ async def build_client( from cerbero_mcp.exchanges.deribit.client import DeribitClient url = settings.deribit.url_testnet if env == "testnet" else settings.deribit.url_live + cid, csec = settings.deribit.credentials(env) return DeribitClient( - client_id=settings.deribit.client_id, - client_secret=settings.deribit.client_secret.get_secret_value(), + client_id=cid, + client_secret=csec, testnet=(env == "testnet"), base_url_override=url, ) diff --git a/src/cerbero_mcp/settings.py b/src/cerbero_mcp/settings.py index 6f60283..463b36c 100644 --- a/src/cerbero_mcp/settings.py +++ b/src/cerbero_mcp/settings.py @@ -21,12 +21,33 @@ class DeribitSettings(_Sub): env_prefix="DERIBIT_", extra="ignore", ) - client_id: str - client_secret: SecretStr + client_id: str | None = None + client_secret: SecretStr | None = None + client_id_testnet: str | None = None + client_secret_testnet: SecretStr | None = None + client_id_live: str | None = None + client_secret_live: SecretStr | None = None url_live: str url_testnet: str max_leverage: int = 3 + def credentials(self, env: str) -> tuple[str, str]: + """Return (client_id, client_secret) for the given env. + Prefers env-specific (_TESTNET / _LIVE) pair; falls back to base + (DERIBIT_CLIENT_ID / DERIBIT_CLIENT_SECRET) for legacy single-pair setups. + """ + if env == "testnet": + cid = self.client_id_testnet or self.client_id + csec = self.client_secret_testnet or self.client_secret + elif env == "mainnet": + cid = self.client_id_live or self.client_id + csec = self.client_secret_live or self.client_secret + else: + raise ValueError(f"unknown deribit env: {env}") + if not cid or csec is None: + raise ValueError(f"Deribit credentials not configured for env={env}") + return cid, csec.get_secret_value() + class BybitSettings(_Sub): model_config = SettingsConfigDict( diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index 2ee49c9..c2d38ff 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -51,7 +51,8 @@ def test_settings_load_minimal(monkeypatch): assert s.alpaca.max_leverage == 1 -def test_settings_missing_token_fails(monkeypatch): +def test_settings_missing_token_fails(monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) # isola dal .env reale del working dir env = _minimal_env() env.pop("TESTNET_TOKEN") for k, v in env.items(): @@ -84,3 +85,73 @@ def test_settings_secret_str_no_leak(monkeypatch): s = Settings() assert "t_test_123" not in repr(s) assert "t_live_456" not in repr(s) + + +def _isolated(monkeypatch, tmp_path, env: dict) -> None: + """Isola Settings dal .env reale in working dir e setta solo env passato.""" + monkeypatch.chdir(tmp_path) + for k in ( + "DERIBIT_CLIENT_ID", "DERIBIT_CLIENT_SECRET", + "DERIBIT_CLIENT_ID_TESTNET", "DERIBIT_CLIENT_SECRET_TESTNET", + "DERIBIT_CLIENT_ID_LIVE", "DERIBIT_CLIENT_SECRET_LIVE", + ): + monkeypatch.delenv(k, raising=False) + for k, v in env.items(): + monkeypatch.setenv(k, v) + + +def test_deribit_credentials_legacy_single_pair(monkeypatch, tmp_path): + """Solo DERIBIT_CLIENT_ID/SECRET → entrambi gli env usano la stessa coppia.""" + _isolated(monkeypatch, tmp_path, _minimal_env()) + + from cerbero_mcp.settings import Settings + + s = Settings() + assert s.deribit.credentials("testnet") == ("id", "secret") + assert s.deribit.credentials("mainnet") == ("id", "secret") + + +def test_deribit_credentials_per_env_pairs(monkeypatch, tmp_path): + """Coppie _TESTNET e _LIVE → ognuna serve l'env corrispondente.""" + env = _minimal_env() + env.pop("DERIBIT_CLIENT_ID") + env.pop("DERIBIT_CLIENT_SECRET") + env["DERIBIT_CLIENT_ID_TESTNET"] = "tid" + env["DERIBIT_CLIENT_SECRET_TESTNET"] = "tsec" + env["DERIBIT_CLIENT_ID_LIVE"] = "lid" + env["DERIBIT_CLIENT_SECRET_LIVE"] = "lsec" + _isolated(monkeypatch, tmp_path, env) + + from cerbero_mcp.settings import Settings + + s = Settings() + assert s.deribit.credentials("testnet") == ("tid", "tsec") + assert s.deribit.credentials("mainnet") == ("lid", "lsec") + + +def test_deribit_credentials_env_specific_overrides_fallback(monkeypatch, tmp_path): + """_LIVE presente prevale sulla coppia base anche se entrambe configurate.""" + env = _minimal_env() + env["DERIBIT_CLIENT_ID_LIVE"] = "lid" + env["DERIBIT_CLIENT_SECRET_LIVE"] = "lsec" + _isolated(monkeypatch, tmp_path, env) + + from cerbero_mcp.settings import Settings + + s = Settings() + assert s.deribit.credentials("mainnet") == ("lid", "lsec") + assert s.deribit.credentials("testnet") == ("id", "secret") # fallback + + +def test_deribit_credentials_missing_raises(monkeypatch, tmp_path): + """Nessuna coppia configurata → ValueError esplicito.""" + env = _minimal_env() + env.pop("DERIBIT_CLIENT_ID") + env.pop("DERIBIT_CLIENT_SECRET") + _isolated(monkeypatch, tmp_path, env) + + from cerbero_mcp.settings import Settings + + s = Settings() + with pytest.raises(ValueError, match="not configured for env=mainnet"): + s.deribit.credentials("mainnet")