diff --git a/services/common/src/mcp_common/environment.py b/services/common/src/mcp_common/environment.py new file mode 100644 index 0000000..d09d014 --- /dev/null +++ b/services/common/src/mcp_common/environment.py @@ -0,0 +1,59 @@ +"""Resolver di ambiente (testnet/mainnet) per MCP exchange. + +Precedenza: env var > campo secret > default True (testnet). +""" +from __future__ import annotations + +import os +from dataclasses import dataclass +from typing import Literal + +Environment = Literal["testnet", "mainnet"] +Source = Literal["env", "credentials", "default"] + +TRUTHY = {"1", "true", "yes", "on"} + + +@dataclass(frozen=True) +class EnvironmentInfo: + exchange: str + environment: Environment + source: Source + env_value: str | None + base_url: str + + +def resolve_environment( + creds: dict, + *, + env_var: str, + flag_key: str, + exchange: str, +) -> EnvironmentInfo: + """Risolvi l'ambiente per un MCP exchange. + + creds: dict letto dal secret JSON. Deve contenere base_url_live e base_url_testnet. + env_var: nome della env var di override (es. DERIBIT_TESTNET). + flag_key: chiave booleana nel secret JSON (es. "testnet" o "paper" per alpaca). + exchange: nome exchange per logging/info. + """ + env_value = os.environ.get(env_var) + if env_value is not None: + is_test = env_value.strip().lower() in TRUTHY + environment: Environment = "testnet" if is_test else "mainnet" + source: Source = "env" + elif flag_key in creds: + environment = "testnet" if bool(creds[flag_key]) else "mainnet" + source = "credentials" + else: + environment = "testnet" + source = "default" + + base_url = creds["base_url_testnet"] if environment == "testnet" else creds["base_url_live"] + return EnvironmentInfo( + exchange=exchange, + environment=environment, + source=source, + env_value=env_value, + base_url=base_url, + ) diff --git a/services/common/tests/test_environment.py b/services/common/tests/test_environment.py new file mode 100644 index 0000000..e7defb0 --- /dev/null +++ b/services/common/tests/test_environment.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import json + +import pytest + +from mcp_common.environment import EnvironmentInfo, resolve_environment + + +def test_env_var_overrides_secret(monkeypatch): + monkeypatch.setenv("DERIBIT_TESTNET", "false") + creds = {"testnet": True, "base_url_live": "L", "base_url_testnet": "T"} + info = resolve_environment( + creds, + env_var="DERIBIT_TESTNET", + flag_key="testnet", + exchange="deribit", + ) + assert info.environment == "mainnet" + assert info.source == "env" + assert info.env_value == "false" + assert info.base_url == "L" + + +def test_secret_used_when_env_missing(monkeypatch): + monkeypatch.delenv("DERIBIT_TESTNET", raising=False) + creds = {"testnet": True, "base_url_live": "L", "base_url_testnet": "T"} + info = resolve_environment( + creds, + env_var="DERIBIT_TESTNET", + flag_key="testnet", + exchange="deribit", + ) + assert info.environment == "testnet" + assert info.source == "credentials" + assert info.env_value is None + assert info.base_url == "T" + + +def test_default_when_both_missing(monkeypatch): + monkeypatch.delenv("FOO_TESTNET", raising=False) + creds = {"base_url_live": "L", "base_url_testnet": "T"} + info = resolve_environment( + creds, + env_var="FOO_TESTNET", + flag_key="testnet", + exchange="foo", + ) + assert info.environment == "testnet" + assert info.source == "default" + assert info.env_value is None + + +@pytest.mark.parametrize("raw,expected", [ + ("1", "testnet"), + ("true", "testnet"), + ("yes", "testnet"), + ("on", "testnet"), + ("TRUE", "testnet"), + ("0", "mainnet"), + ("false", "mainnet"), + ("no", "mainnet"), + ("off", "mainnet"), + ("garbage", "mainnet"), +]) +def test_env_value_truthy_parsing(monkeypatch, raw, expected): + monkeypatch.setenv("X_TESTNET", raw) + info = resolve_environment( + {"base_url_live": "L", "base_url_testnet": "T"}, + env_var="X_TESTNET", + flag_key="testnet", + exchange="x", + ) + assert info.environment == expected + + +def test_alpaca_paper_flag_key(monkeypatch): + """Alpaca usa 'paper' invece di 'testnet' nel secret.""" + monkeypatch.delenv("ALPACA_PAPER", raising=False) + creds = {"paper": False, "base_url_live": "L", "base_url_testnet": "T"} + info = resolve_environment( + creds, + env_var="ALPACA_PAPER", + flag_key="paper", + exchange="alpaca", + ) + assert info.environment == "mainnet" + assert info.source == "credentials"