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