218 lines
6.2 KiB
Python
218 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from mcp_hyperliquid.server import create_app
|
|
from option_mcp_common.auth import Principal, TokenStore
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_client():
|
|
c = MagicMock()
|
|
c.get_markets = AsyncMock(return_value=[{"asset": "BTC", "mark_price": 50000}])
|
|
c.get_ticker = AsyncMock(return_value={"asset": "BTC", "mark_price": 50000})
|
|
c.get_orderbook = AsyncMock(return_value={"bids": [], "asks": []})
|
|
c.get_positions = AsyncMock(return_value=[])
|
|
c.get_account_summary = AsyncMock(return_value={"equity": 1500, "perps_equity": 1000})
|
|
c.get_trade_history = AsyncMock(return_value=[])
|
|
c.get_historical = AsyncMock(return_value={"candles": []})
|
|
c.get_open_orders = AsyncMock(return_value=[])
|
|
c.get_funding_rate = AsyncMock(return_value={"asset": "BTC", "current_funding_rate": 0.0001})
|
|
c.get_indicators = AsyncMock(return_value={"rsi": 55.0})
|
|
c.place_order = AsyncMock(return_value={"order_id": "x", "status": "ok"})
|
|
c.cancel_order = AsyncMock(return_value={"order_id": "x", "status": "ok"})
|
|
c.set_stop_loss = AsyncMock(return_value={"order_id": "x", "status": "ok"})
|
|
c.set_take_profit = AsyncMock(return_value={"order_id": "x", "status": "ok"})
|
|
c.close_position = AsyncMock(return_value={"status": "ok", "asset": "BTC"})
|
|
return c
|
|
|
|
|
|
@pytest.fixture
|
|
def http(mock_client):
|
|
store = TokenStore(
|
|
tokens={
|
|
"ct": Principal("core", {"core"}),
|
|
"ot": Principal("observer", {"observer"}),
|
|
}
|
|
)
|
|
app = create_app(client=mock_client, token_store=store)
|
|
return TestClient(app)
|
|
|
|
|
|
# --- Health ---
|
|
|
|
def test_health(http):
|
|
assert http.get("/health").status_code == 200
|
|
|
|
|
|
# --- Read tools: both core and observer allowed ---
|
|
|
|
def test_get_markets_core_ok(http):
|
|
r = http.post("/tools/get_markets", headers={"Authorization": "Bearer ct"}, json={})
|
|
assert r.status_code == 200
|
|
|
|
|
|
def test_get_markets_observer_ok(http):
|
|
r = http.post("/tools/get_markets", headers={"Authorization": "Bearer ot"}, json={})
|
|
assert r.status_code == 200
|
|
|
|
|
|
def test_get_ticker_core_ok(http):
|
|
r = http.post(
|
|
"/tools/get_ticker",
|
|
headers={"Authorization": "Bearer ct"},
|
|
json={"instrument": "BTC"},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["mark_price"] == 50000
|
|
|
|
|
|
def test_get_ticker_observer_ok(http):
|
|
r = http.post(
|
|
"/tools/get_ticker",
|
|
headers={"Authorization": "Bearer ot"},
|
|
json={"instrument": "BTC"},
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
|
|
def test_get_ticker_no_auth_401(http):
|
|
r = http.post("/tools/get_ticker", json={"instrument": "BTC"})
|
|
assert r.status_code == 401
|
|
|
|
|
|
def test_get_account_summary_observer_ok(http):
|
|
r = http.post(
|
|
"/tools/get_account_summary",
|
|
headers={"Authorization": "Bearer ot"},
|
|
json={},
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["equity"] == 1500
|
|
|
|
|
|
def test_get_funding_rate_observer_ok(http):
|
|
r = http.post(
|
|
"/tools/get_funding_rate",
|
|
headers={"Authorization": "Bearer ot"},
|
|
json={"instrument": "BTC"},
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
|
|
def test_get_positions_no_auth_401(http):
|
|
r = http.post("/tools/get_positions", json={})
|
|
assert r.status_code == 401
|
|
|
|
|
|
# --- Write tools: core only ---
|
|
|
|
def test_place_order_core_ok(http):
|
|
# CER-016: amount * price = 150 < cap 200
|
|
r = http.post(
|
|
"/tools/place_order",
|
|
headers={"Authorization": "Bearer ct"},
|
|
json={"instrument": "BTC", "side": "buy", "amount": 0.003, "price": 50000},
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
|
|
def test_place_order_observer_forbidden(http):
|
|
r = http.post(
|
|
"/tools/place_order",
|
|
headers={"Authorization": "Bearer ot"},
|
|
json={"instrument": "BTC", "side": "buy", "amount": 0.001, "price": 50000},
|
|
)
|
|
assert r.status_code == 403
|
|
|
|
|
|
def test_place_order_notional_cap_enforced(http):
|
|
"""CER-016: HL reject amount*price > 200."""
|
|
r = http.post(
|
|
"/tools/place_order",
|
|
headers={"Authorization": "Bearer ct"},
|
|
json={"instrument": "ETH", "side": "buy", "amount": 0.1, "price": 3350},
|
|
)
|
|
assert r.status_code == 403
|
|
assert r.json()["error"]["code"] == "HARD_PROHIBITION"
|
|
|
|
|
|
def test_place_order_leverage_cap_enforced_hl(http):
|
|
r = http.post(
|
|
"/tools/place_order",
|
|
headers={"Authorization": "Bearer ct"},
|
|
json={
|
|
"instrument": "BTC",
|
|
"side": "buy",
|
|
"amount": 0.001,
|
|
"price": 50000,
|
|
"leverage": 10,
|
|
},
|
|
)
|
|
assert r.status_code == 403
|
|
|
|
|
|
def test_cancel_order_core_ok(http):
|
|
r = http.post(
|
|
"/tools/cancel_order",
|
|
headers={"Authorization": "Bearer ct"},
|
|
json={"order_id": "123", "instrument": "BTC"},
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
|
|
def test_cancel_order_observer_forbidden(http):
|
|
r = http.post(
|
|
"/tools/cancel_order",
|
|
headers={"Authorization": "Bearer ot"},
|
|
json={"order_id": "123", "instrument": "BTC"},
|
|
)
|
|
assert r.status_code == 403
|
|
|
|
|
|
def test_set_stop_loss_core_ok(http):
|
|
r = http.post(
|
|
"/tools/set_stop_loss",
|
|
headers={"Authorization": "Bearer ct"},
|
|
json={"instrument": "BTC", "stop_price": 45000.0, "size": 0.1},
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
|
|
def test_set_stop_loss_observer_forbidden(http):
|
|
r = http.post(
|
|
"/tools/set_stop_loss",
|
|
headers={"Authorization": "Bearer ot"},
|
|
json={"instrument": "BTC", "stop_price": 45000.0, "size": 0.1},
|
|
)
|
|
assert r.status_code == 403
|
|
|
|
|
|
def test_set_take_profit_observer_forbidden(http):
|
|
r = http.post(
|
|
"/tools/set_take_profit",
|
|
headers={"Authorization": "Bearer ot"},
|
|
json={"instrument": "BTC", "tp_price": 55000.0, "size": 0.1},
|
|
)
|
|
assert r.status_code == 403
|
|
|
|
|
|
def test_close_position_core_ok(http):
|
|
r = http.post(
|
|
"/tools/close_position",
|
|
headers={"Authorization": "Bearer ct"},
|
|
json={"instrument": "BTC"},
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
|
|
def test_close_position_observer_forbidden(http):
|
|
r = http.post(
|
|
"/tools/close_position",
|
|
headers={"Authorization": "Bearer ot"},
|
|
json={"instrument": "BTC"},
|
|
)
|
|
assert r.status_code == 403
|