from __future__ import annotations from unittest.mock import AsyncMock, MagicMock import pytest from fastapi.testclient import TestClient from mcp_common.auth import Principal, TokenStore from mcp_bybit.server import create_app @pytest.fixture def token_store(): return TokenStore( tokens={ "core-tok": Principal("core", {"core"}), "obs-tok": Principal("observer", {"observer"}), } ) @pytest.fixture def mock_client(): c = MagicMock() c.get_ticker = AsyncMock(return_value={"symbol": "BTCUSDT"}) c.get_ticker_batch = AsyncMock(return_value={"BTCUSDT": {}}) c.get_orderbook = AsyncMock(return_value={"bids": [], "asks": []}) c.get_historical = AsyncMock(return_value={"candles": []}) c.get_indicators = AsyncMock(return_value={"rsi": 50.0}) c.get_funding_rate = AsyncMock(return_value={"funding_rate": 0.0001}) c.get_funding_history = AsyncMock(return_value={"history": []}) c.get_open_interest = AsyncMock(return_value={"points": []}) c.get_instruments = AsyncMock(return_value={"instruments": []}) c.get_option_chain = AsyncMock(return_value={"options": []}) c.get_positions = AsyncMock(return_value=[]) c.get_account_summary = AsyncMock(return_value={"equity": 0}) c.get_trade_history = AsyncMock(return_value=[]) c.get_open_orders = AsyncMock(return_value=[]) c.get_basis_spot_perp = AsyncMock(return_value={"basis_pct": 0}) c.place_order = AsyncMock(return_value={"order_id": "x"}) c.amend_order = AsyncMock(return_value={"order_id": "x"}) c.cancel_order = AsyncMock(return_value={"status": "cancelled"}) c.cancel_all_orders = AsyncMock(return_value={"cancelled_ids": []}) c.set_stop_loss = AsyncMock(return_value={"status": "stop_loss_set"}) c.set_take_profit = AsyncMock(return_value={"status": "take_profit_set"}) c.close_position = AsyncMock(return_value={"status": "submitted"}) c.set_leverage = AsyncMock(return_value={"status": "leverage_set"}) c.switch_position_mode = AsyncMock(return_value={"status": "mode_switched"}) c.transfer_asset = AsyncMock(return_value={"transfer_id": "tx"}) return c @pytest.fixture def http(mock_client, token_store): app = create_app(client=mock_client, token_store=token_store) return TestClient(app) CORE = {"Authorization": "Bearer core-tok"} OBS = {"Authorization": "Bearer obs-tok"} READ_ENDPOINTS = [ ("/tools/get_ticker", {"symbol": "BTCUSDT"}), ("/tools/get_ticker_batch", {"symbols": ["BTCUSDT"]}), ("/tools/get_orderbook", {"symbol": "BTCUSDT"}), ("/tools/get_historical", {"symbol": "BTCUSDT"}), ("/tools/get_indicators", {"symbol": "BTCUSDT"}), ("/tools/get_funding_rate", {"symbol": "BTCUSDT"}), ("/tools/get_funding_history", {"symbol": "BTCUSDT"}), ("/tools/get_open_interest", {"symbol": "BTCUSDT"}), ("/tools/get_instruments", {}), ("/tools/get_option_chain", {"base_coin": "BTC"}), ("/tools/get_positions", {}), ("/tools/get_account_summary", {}), ("/tools/get_trade_history", {}), ("/tools/get_open_orders", {}), ("/tools/get_basis_spot_perp", {"asset": "BTC"}), ] WRITE_ENDPOINTS = [ ("/tools/place_order", {"category": "linear", "symbol": "BTCUSDT", "side": "Buy", "qty": 0.01}), ("/tools/amend_order", {"category": "linear", "symbol": "BTCUSDT", "order_id": "o1"}), ("/tools/cancel_order", {"category": "linear", "symbol": "BTCUSDT", "order_id": "o1"}), ("/tools/cancel_all_orders", {"category": "linear"}), ("/tools/set_stop_loss", {"category": "linear", "symbol": "BTCUSDT", "stop_loss": 55000}), ("/tools/set_take_profit", {"category": "linear", "symbol": "BTCUSDT", "take_profit": 65000}), ("/tools/close_position", {"category": "linear", "symbol": "BTCUSDT"}), ("/tools/set_leverage", {"category": "linear", "symbol": "BTCUSDT", "leverage": 5}), ("/tools/switch_position_mode", {"category": "linear", "symbol": "BTCUSDT", "mode": "hedge"}), ("/tools/transfer_asset", {"coin": "USDT", "amount": 10.0, "from_type": "UNIFIED", "to_type": "FUND"}), ] @pytest.mark.parametrize("path,payload", READ_ENDPOINTS) def test_read_core_ok(http, path, payload): r = http.post(path, json=payload, headers=CORE) assert r.status_code == 200, (path, r.text) @pytest.mark.parametrize("path,payload", READ_ENDPOINTS) def test_read_observer_ok(http, path, payload): r = http.post(path, json=payload, headers=OBS) assert r.status_code == 200, (path, r.text) @pytest.mark.parametrize("path,payload", READ_ENDPOINTS) def test_read_no_auth_401(http, path, payload): r = http.post(path, json=payload) assert r.status_code == 401, (path, r.text) @pytest.mark.parametrize("path,payload", WRITE_ENDPOINTS) def test_write_core_ok(http, path, payload): r = http.post(path, json=payload, headers=CORE) assert r.status_code == 200, (path, r.text) @pytest.mark.parametrize("path,payload", WRITE_ENDPOINTS) def test_write_observer_403(http, path, payload): r = http.post(path, json=payload, headers=OBS) assert r.status_code == 403, (path, r.text) @pytest.mark.parametrize("path,payload", WRITE_ENDPOINTS) def test_write_no_auth_401(http, path, payload): r = http.post(path, json=payload) assert r.status_code == 401, (path, r.text)