Files
AdrianoDev 4d9db750be chore: ruff py313, conftest unification, audit log, app factory comune
- 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>
2026-04-28 00:27:02 +02:00

217 lines
5.8 KiB
Python

from __future__ import annotations
from unittest.mock import AsyncMock, patch
import pytest
from fastapi.testclient import TestClient
from mcp_common.auth import Principal, TokenStore
from mcp_sentiment.server import create_app
@pytest.fixture
def http():
store = TokenStore(
tokens={
"ct": Principal("core", {"core"}),
"ot": Principal("observer", {"observer"}),
}
)
app = create_app(cryptopanic_key="testkey", token_store=store)
return TestClient(app)
# --- Health ---
def test_health(http):
assert http.get("/health").status_code == 200
# --- get_crypto_news ---
def test_get_crypto_news_core_ok(http):
with patch(
"mcp_sentiment.server.fetch_crypto_news",
new=AsyncMock(return_value={"headlines": []}),
):
r = http.post(
"/tools/get_crypto_news",
headers={"Authorization": "Bearer ct"},
json={"limit": 5},
)
assert r.status_code == 200
def test_get_crypto_news_observer_ok(http):
with patch(
"mcp_sentiment.server.fetch_crypto_news",
new=AsyncMock(return_value={"headlines": []}),
):
r = http.post(
"/tools/get_crypto_news",
headers={"Authorization": "Bearer ot"},
json={},
)
assert r.status_code == 200
def test_get_crypto_news_no_auth_401(http):
r = http.post("/tools/get_crypto_news", json={})
assert r.status_code == 401
# --- get_social_sentiment ---
def test_get_social_sentiment_core_ok(http):
with patch(
"mcp_sentiment.server.fetch_social_sentiment",
new=AsyncMock(return_value={"fear_greed_index": 65, "fear_greed_label": "Greed"}),
):
r = http.post(
"/tools/get_social_sentiment",
headers={"Authorization": "Bearer ct"},
json={},
)
assert r.status_code == 200
assert r.json()["fear_greed_index"] == 65
def test_get_social_sentiment_observer_ok(http):
with patch(
"mcp_sentiment.server.fetch_social_sentiment",
new=AsyncMock(return_value={"fear_greed_index": 65}),
):
r = http.post(
"/tools/get_social_sentiment",
headers={"Authorization": "Bearer ot"},
json={},
)
assert r.status_code == 200
def test_get_social_sentiment_no_auth_401(http):
r = http.post("/tools/get_social_sentiment", json={})
assert r.status_code == 401
# --- get_funding_rates ---
def test_get_funding_rates_core_ok(http):
with patch(
"mcp_sentiment.server.fetch_funding_rates",
new=AsyncMock(return_value={"rates": []}),
):
r = http.post(
"/tools/get_funding_rates",
headers={"Authorization": "Bearer ct"},
json={},
)
assert r.status_code == 200
def test_get_funding_rates_observer_ok(http):
with patch(
"mcp_sentiment.server.fetch_funding_rates",
new=AsyncMock(return_value={"rates": []}),
):
r = http.post(
"/tools/get_funding_rates",
headers={"Authorization": "Bearer ot"},
json={},
)
assert r.status_code == 200
def test_get_funding_rates_no_auth_401(http):
r = http.post("/tools/get_funding_rates", json={})
assert r.status_code == 401
# --- get_world_news ---
def test_get_world_news_core_ok(http):
with patch(
"mcp_sentiment.server.fetch_world_news",
new=AsyncMock(return_value={"articles": [], "count": 0}),
):
r = http.post(
"/tools/get_world_news",
headers={"Authorization": "Bearer ct"},
json={},
)
assert r.status_code == 200
def test_get_world_news_observer_ok(http):
with patch(
"mcp_sentiment.server.fetch_world_news",
new=AsyncMock(return_value={"articles": [], "count": 0}),
):
r = http.post(
"/tools/get_world_news",
headers={"Authorization": "Bearer ot"},
json={},
)
assert r.status_code == 200
def test_get_world_news_no_auth_401(http):
r = http.post("/tools/get_world_news", json={})
assert r.status_code == 401
# --- New indicators: funding_arb_spread, liquidation_heatmap, cointegration_pairs ---
def test_get_funding_arb_spread_ok(http):
with patch(
"mcp_sentiment.server.fetch_funding_arb_spread",
new=AsyncMock(return_value={"opportunities": []}),
):
r = http.post(
"/tools/get_funding_arb_spread",
headers={"Authorization": "Bearer ot"},
json={},
)
assert r.status_code == 200
def test_get_funding_arb_spread_no_auth_401(http):
r = http.post("/tools/get_funding_arb_spread", json={})
assert r.status_code == 401
def test_get_liquidation_heatmap_ok(http):
with patch(
"mcp_sentiment.server.fetch_liquidation_heatmap",
new=AsyncMock(return_value={"asset": "BTC", "long_squeeze_risk": "low"}),
):
r = http.post(
"/tools/get_liquidation_heatmap",
headers={"Authorization": "Bearer ct"},
json={"asset": "BTC"},
)
assert r.status_code == 200
def test_get_liquidation_heatmap_no_auth_401(http):
r = http.post("/tools/get_liquidation_heatmap", json={"asset": "BTC"})
assert r.status_code == 401
def test_get_cointegration_pairs_ok(http):
with patch(
"mcp_sentiment.server.fetch_cointegration_pairs",
new=AsyncMock(return_value={"results": []}),
):
r = http.post(
"/tools/get_cointegration_pairs",
headers={"Authorization": "Bearer ot"},
json={"pairs": [["BTC", "ETH"]]},
)
assert r.status_code == 200
def test_get_cointegration_pairs_no_auth_401(http):
r = http.post("/tools/get_cointegration_pairs", json={})
assert r.status_code == 401