4d9db750be
- pyproject.toml: ruff target-version py311 → py313 (auto-fix 42 lint warnings via UP rules); aggiunto consider_namespace_packages = true che risolve la collisione conftest tra servizi e permette di lanciare pytest sull'intera suite cross-servizio. - mcp_common.audit: nuovo helper audit_write_op() con logger dedicato mcp.audit. Wirato su tutti i write endpoint di deribit, bybit, alpaca e hyperliquid (place_order, place_combo_order, cancel_*, set_*, close_*, transfer_*, switch_*, amend_*) con principal + target + payload non-sensibile + result summarizzato. - mcp_common.app_factory: ExchangeAppSpec + run_exchange_main() centralizza il boilerplate dei __main__.py (configure_root_logging, fail_fast_if_missing, summarize, load creds, resolve_environment, load token store, uvicorn). I 4 __main__.py exchange ridotti da ~60 LOC ognuno a ~25 LOC dichiarativi. mcp_common.env_validation promosso da mcp_deribit (mantenuto re-export shim per back-compat test_env_validation). - 8 test nuovi (4 audit + 4 app_factory). Suite full: 450/450 verdi. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from mcp_common.app_factory import ExchangeAppSpec, run_exchange_main
|
|
from mcp_common.environment import EnvironmentInfo
|
|
|
|
|
|
def _make_spec(build_client=None, build_app=None) -> ExchangeAppSpec:
|
|
return ExchangeAppSpec(
|
|
exchange="testex",
|
|
creds_env_var="TESTEX_CREDENTIALS_FILE",
|
|
env_var="TESTEX_TESTNET",
|
|
flag_key="testnet",
|
|
default_base_url_live="https://api.testex.com",
|
|
default_base_url_testnet="https://test.testex.com",
|
|
default_port=9999,
|
|
build_client=build_client or (lambda creds, env_info: MagicMock(name="client")),
|
|
build_app=build_app or (lambda **kwargs: MagicMock(name="app")),
|
|
)
|
|
|
|
|
|
def test_run_exchange_main_loads_creds_and_resolves_env(tmp_path, monkeypatch):
|
|
creds_file = tmp_path / "creds.json"
|
|
creds_file.write_text(json.dumps({"api_key": "k", "api_secret": "s"}))
|
|
monkeypatch.setenv("TESTEX_CREDENTIALS_FILE", str(creds_file))
|
|
monkeypatch.setenv("PORT", "10000")
|
|
monkeypatch.delenv("TESTEX_TESTNET", raising=False)
|
|
|
|
captured: dict = {}
|
|
|
|
def build_client(creds, env_info):
|
|
captured["creds"] = creds
|
|
captured["env_info"] = env_info
|
|
return MagicMock()
|
|
|
|
def build_app(**kwargs):
|
|
captured["app_kwargs"] = kwargs
|
|
return MagicMock()
|
|
|
|
spec = _make_spec(build_client=build_client, build_app=build_app)
|
|
|
|
with patch("mcp_common.app_factory.uvicorn.run") as mock_run:
|
|
run_exchange_main(spec)
|
|
|
|
assert captured["creds"]["api_key"] == "k"
|
|
assert captured["creds"]["base_url_live"] == "https://api.testex.com"
|
|
assert captured["creds"]["base_url_testnet"] == "https://test.testex.com"
|
|
assert isinstance(captured["env_info"], EnvironmentInfo)
|
|
assert captured["env_info"].environment == "testnet"
|
|
assert captured["env_info"].exchange == "testex"
|
|
|
|
assert "client" in captured["app_kwargs"]
|
|
assert "token_store" in captured["app_kwargs"]
|
|
assert "creds" in captured["app_kwargs"]
|
|
assert "env_info" in captured["app_kwargs"]
|
|
|
|
call_kwargs = mock_run.call_args.kwargs
|
|
assert call_kwargs["port"] == 10000 # PORT override
|
|
|
|
|
|
def test_run_exchange_main_uses_default_port(tmp_path, monkeypatch):
|
|
creds_file = tmp_path / "creds.json"
|
|
creds_file.write_text(json.dumps({}))
|
|
monkeypatch.setenv("TESTEX_CREDENTIALS_FILE", str(creds_file))
|
|
monkeypatch.delenv("PORT", raising=False)
|
|
|
|
spec = _make_spec()
|
|
with patch("mcp_common.app_factory.uvicorn.run") as mock_run:
|
|
run_exchange_main(spec)
|
|
|
|
assert mock_run.call_args.kwargs["port"] == 9999
|
|
|
|
|
|
def test_run_exchange_main_env_var_overrides_creds(tmp_path, monkeypatch):
|
|
creds_file = tmp_path / "creds.json"
|
|
creds_file.write_text(json.dumps({"testnet": True}))
|
|
monkeypatch.setenv("TESTEX_CREDENTIALS_FILE", str(creds_file))
|
|
monkeypatch.setenv("TESTEX_TESTNET", "false")
|
|
|
|
captured: dict = {}
|
|
|
|
def build_client(creds, env_info):
|
|
captured["env_info"] = env_info
|
|
return MagicMock()
|
|
|
|
spec = _make_spec(build_client=build_client)
|
|
|
|
with patch("mcp_common.app_factory.uvicorn.run"):
|
|
run_exchange_main(spec)
|
|
|
|
# env var "false" overrides creds.testnet=True → mainnet
|
|
assert captured["env_info"].environment == "mainnet"
|
|
assert captured["env_info"].source == "env"
|
|
|
|
|
|
def test_run_exchange_main_missing_creds_file_exits(monkeypatch):
|
|
monkeypatch.delenv("TESTEX_CREDENTIALS_FILE", raising=False)
|
|
|
|
spec = _make_spec()
|
|
import pytest
|
|
with pytest.raises(SystemExit) as exc_info:
|
|
run_exchange_main(spec)
|
|
assert exc_info.value.code == 2
|