Files
Cerbero-mcp/services/common/tests/test_app_factory.py
T
AdrianoDev a1110c8ecb
ci / ruff lint (push) Failing after 12s
ci / mypy mcp_common (push) Successful in 25s
ci / pytest (push) Successful in 35s
ci / validate compose + Caddyfile (push) Successful in 2m3s
ci / build & push to registry (push) Has been skipped
feat(safety+audit+deploy): consistency_check + audit log file sink + deploy script
#2 Env switch safety:
- mcp_common/environment.py: nuova consistency_check() che previene
  switch accidentali a mainnet. Solleva EnvironmentMismatchError se
  resolved=mainnet senza creds["environment"]="mainnet" esplicito,
  o se declared/resolved mismatch. Override via STRICT_MAINNET=false.
- Wirato in app_factory.run_exchange_main al boot.
- 6 nuovi test consistency.

#3 Audit log persistence:
- mcp_common/audit.py: TimedRotatingFileHandler aggiuntivo se env
  AUDIT_LOG_FILE settato. Rotation midnight UTC, retention 30gg
  default (AUDIT_LOG_BACKUP_DAYS). Format JSONL con SecretsFilter.
- docker-compose.prod.yml: bind mount /var/log/cerbero-mcp + env
  AUDIT_LOG_FILE per i 4 servizi exchange (write endpoints).
- 2 nuovi test file sink.

#1 Deploy script:
- scripts/deploy.sh: idempotente, fa docker login + clone/pull repo +
  copia secrets chmod 600 + crea .env + setup audit dir + pull image
  + up + smoke test pubblico HTTPS.
- DEPLOYMENT.md aggiornato: sezioni 2 (script), 3 (safety mainnet),
  4 (audit log query), renumber sezioni successive.

Test: 488/488 verdi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 09:29:04 +02:00

137 lines
5.0 KiB
Python

from __future__ import annotations
import json
from unittest.mock import MagicMock, patch
import pytest
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"
# `environment: mainnet` esplicito perché env var override → mainnet
# e consistency_check richiede conferma per evitare switch accidentale.
creds_file.write_text(json.dumps({"testnet": True, "environment": "mainnet"}))
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_aborts_on_mainnet_without_confirmation(tmp_path, monkeypatch):
"""Mainnet senza creds['environment']='mainnet' → boot abort fail-fast."""
from mcp_common.environment import EnvironmentMismatchError
creds_file = tmp_path / "creds.json"
creds_file.write_text(json.dumps({"testnet": False}))
monkeypatch.setenv("TESTEX_CREDENTIALS_FILE", str(creds_file))
monkeypatch.delenv("TESTEX_TESTNET", raising=False)
monkeypatch.delenv("STRICT_MAINNET", raising=False)
spec = _make_spec()
with pytest.raises(EnvironmentMismatchError):
with patch("mcp_common.app_factory.uvicorn.run"):
run_exchange_main(spec)
def test_run_exchange_main_strict_mainnet_disabled_via_env(tmp_path, monkeypatch):
"""STRICT_MAINNET=false permette mainnet senza conferma (warning soltanto)."""
creds_file = tmp_path / "creds.json"
creds_file.write_text(json.dumps({"testnet": False}))
monkeypatch.setenv("TESTEX_CREDENTIALS_FILE", str(creds_file))
monkeypatch.setenv("STRICT_MAINNET", "false")
spec = _make_spec()
with patch("mcp_common.app_factory.uvicorn.run"):
run_exchange_main(spec) # non solleva
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