""" Tests for Binance exchange connector. """ import pytest import asyncio from unittest.mock import AsyncMock, MagicMock, patch from datetime import datetime, timezone from ..connectors.binance_connector import BinanceConnector from ..models.core import OrderBookSnapshot, TradeEvent, PriceLevel @pytest.fixture def binance_connector(): """Create Binance connector for testing""" return BinanceConnector() @pytest.fixture def sample_binance_orderbook_data(): """Sample Binance order book data""" return { "lastUpdateId": 1027024, "bids": [ ["4.00000000", "431.00000000"], ["3.99000000", "9.00000000"] ], "asks": [ ["4.00000200", "12.00000000"], ["4.01000000", "18.00000000"] ] } @pytest.fixture def sample_binance_depth_update(): """Sample Binance depth update message""" return { "e": "depthUpdate", "E": 1672515782136, "s": "BTCUSDT", "U": 157, "u": 160, "b": [ ["50000.00", "0.25"], ["49999.00", "0.50"] ], "a": [ ["50001.00", "0.30"], ["50002.00", "0.40"] ] } @pytest.fixture def sample_binance_trade_update(): """Sample Binance trade update message""" return { "e": "trade", "E": 1672515782136, "s": "BTCUSDT", "t": 12345, "p": "50000.50", "q": "0.10", "b": 88, "a": 50, "T": 1672515782134, "m": False, "M": True } class TestBinanceConnector: """Test cases for BinanceConnector""" def test_initialization(self, binance_connector): """Test connector initialization""" assert binance_connector.exchange_name == "binance" assert binance_connector.websocket_url == BinanceConnector.WEBSOCKET_URL assert len(binance_connector.message_handlers) >= 3 assert binance_connector.stream_id == 1 assert binance_connector.active_streams == [] def test_normalize_symbol(self, binance_connector): """Test symbol normalization""" # Test standard format assert binance_connector.normalize_symbol("BTCUSDT") == "BTCUSDT" # Test with separators assert binance_connector.normalize_symbol("BTC-USDT") == "BTCUSDT" assert binance_connector.normalize_symbol("BTC/USDT") == "BTCUSDT" # Test lowercase assert binance_connector.normalize_symbol("btcusdt") == "BTCUSDT" # Test invalid symbol with pytest.raises(Exception): binance_connector.normalize_symbol("") def test_get_message_type(self, binance_connector): """Test message type detection""" # Test depth update depth_msg = {"e": "depthUpdate", "s": "BTCUSDT"} assert binance_connector._get_message_type(depth_msg) == "depthUpdate" # Test trade update trade_msg = {"e": "trade", "s": "BTCUSDT"} assert binance_connector._get_message_type(trade_msg) == "trade" # Test error message error_msg = {"error": {"code": -1121, "msg": "Invalid symbol"}} assert binance_connector._get_message_type(error_msg) == "error" # Test unknown message unknown_msg = {"data": "something"} assert binance_connector._get_message_type(unknown_msg) == "unknown" def test_parse_orderbook_snapshot(self, binance_connector, sample_binance_orderbook_data): """Test order book snapshot parsing""" orderbook = binance_connector._parse_orderbook_snapshot( sample_binance_orderbook_data, "BTCUSDT" ) assert isinstance(orderbook, OrderBookSnapshot) assert orderbook.symbol == "BTCUSDT" assert orderbook.exchange == "binance" assert len(orderbook.bids) == 2 assert len(orderbook.asks) == 2 assert orderbook.sequence_id == 1027024 # Check bid data assert orderbook.bids[0].price == 4.0 assert orderbook.bids[0].size == 431.0 # Check ask data assert orderbook.asks[0].price == 4.000002 assert orderbook.asks[0].size == 12.0 @pytest.mark.asyncio async def test_handle_orderbook_update(self, binance_connector, sample_binance_depth_update): """Test order book update handling""" # Mock callback callback_called = False received_data = None def mock_callback(data): nonlocal callback_called, received_data callback_called = True received_data = data binance_connector.add_data_callback(mock_callback) # Handle update await binance_connector._handle_orderbook_update(sample_binance_depth_update) # Verify callback was called assert callback_called assert isinstance(received_data, OrderBookSnapshot) assert received_data.symbol == "BTCUSDT" assert received_data.exchange == "binance" assert len(received_data.bids) == 2 assert len(received_data.asks) == 2 @pytest.mark.asyncio async def test_handle_trade_update(self, binance_connector, sample_binance_trade_update): """Test trade update handling""" # Mock callback callback_called = False received_data = None def mock_callback(data): nonlocal callback_called, received_data callback_called = True received_data = data binance_connector.add_data_callback(mock_callback) # Handle update await binance_connector._handle_trade_update(sample_binance_trade_update) # Verify callback was called assert callback_called assert isinstance(received_data, TradeEvent) assert received_data.symbol == "BTCUSDT" assert received_data.exchange == "binance" assert received_data.price == 50000.50 assert received_data.size == 0.10 assert received_data.side == "buy" # m=False means buyer is not maker assert received_data.trade_id == "12345" @pytest.mark.asyncio async def test_subscribe_orderbook(self, binance_connector): """Test order book subscription""" # Mock WebSocket send binance_connector._send_message = AsyncMock(return_value=True) # Subscribe await binance_connector.subscribe_orderbook("BTCUSDT") # Verify subscription was sent binance_connector._send_message.assert_called_once() call_args = binance_connector._send_message.call_args[0][0] assert call_args["method"] == "SUBSCRIBE" assert "btcusdt@depth@100ms" in call_args["params"] assert call_args["id"] == 1 # Verify tracking assert "BTCUSDT" in binance_connector.subscriptions assert "orderbook" in binance_connector.subscriptions["BTCUSDT"] assert "btcusdt@depth@100ms" in binance_connector.active_streams assert binance_connector.stream_id == 2 @pytest.mark.asyncio async def test_subscribe_trades(self, binance_connector): """Test trade subscription""" # Mock WebSocket send binance_connector._send_message = AsyncMock(return_value=True) # Subscribe await binance_connector.subscribe_trades("ETHUSDT") # Verify subscription was sent binance_connector._send_message.assert_called_once() call_args = binance_connector._send_message.call_args[0][0] assert call_args["method"] == "SUBSCRIBE" assert "ethusdt@trade" in call_args["params"] assert call_args["id"] == 1 # Verify tracking assert "ETHUSDT" in binance_connector.subscriptions assert "trades" in binance_connector.subscriptions["ETHUSDT"] assert "ethusdt@trade" in binance_connector.active_streams @pytest.mark.asyncio async def test_unsubscribe_orderbook(self, binance_connector): """Test order book unsubscription""" # Setup initial subscription binance_connector.subscriptions["BTCUSDT"] = ["orderbook"] binance_connector.active_streams.append("btcusdt@depth@100ms") # Mock WebSocket send binance_connector._send_message = AsyncMock(return_value=True) # Unsubscribe await binance_connector.unsubscribe_orderbook("BTCUSDT") # Verify unsubscription was sent binance_connector._send_message.assert_called_once() call_args = binance_connector._send_message.call_args[0][0] assert call_args["method"] == "UNSUBSCRIBE" assert "btcusdt@depth@100ms" in call_args["params"] # Verify tracking removal assert "BTCUSDT" not in binance_connector.subscriptions assert "btcusdt@depth@100ms" not in binance_connector.active_streams @pytest.mark.asyncio @patch('aiohttp.ClientSession.get') async def test_get_symbols(self, mock_get, binance_connector): """Test getting available symbols""" # Mock API response mock_response = AsyncMock() mock_response.status = 200 mock_response.json = AsyncMock(return_value={ "symbols": [ {"symbol": "BTCUSDT", "status": "TRADING"}, {"symbol": "ETHUSDT", "status": "TRADING"}, {"symbol": "ADAUSDT", "status": "BREAK"} # Should be filtered out ] }) mock_get.return_value.__aenter__.return_value = mock_response # Get symbols symbols = await binance_connector.get_symbols() # Verify results assert len(symbols) == 2 assert "BTCUSDT" in symbols assert "ETHUSDT" in symbols assert "ADAUSDT" not in symbols # Filtered out due to status @pytest.mark.asyncio @patch('aiohttp.ClientSession.get') async def test_get_orderbook_snapshot(self, mock_get, binance_connector, sample_binance_orderbook_data): """Test getting order book snapshot""" # Mock API response mock_response = AsyncMock() mock_response.status = 200 mock_response.json = AsyncMock(return_value=sample_binance_orderbook_data) mock_get.return_value.__aenter__.return_value = mock_response # Get order book snapshot orderbook = await binance_connector.get_orderbook_snapshot("BTCUSDT", depth=20) # Verify results assert isinstance(orderbook, OrderBookSnapshot) assert orderbook.symbol == "BTCUSDT" assert orderbook.exchange == "binance" assert len(orderbook.bids) == 2 assert len(orderbook.asks) == 2 def test_get_binance_stats(self, binance_connector): """Test getting Binance-specific statistics""" # Add some test data binance_connector.active_streams = ["btcusdt@depth@100ms", "ethusdt@trade"] binance_connector.stream_id = 5 stats = binance_connector.get_binance_stats() # Verify Binance-specific stats assert stats['active_streams'] == 2 assert len(stats['stream_list']) == 2 assert stats['next_stream_id'] == 5 # Verify base stats are included assert 'exchange' in stats assert 'connection_status' in stats assert 'message_count' in stats if __name__ == "__main__": # Run a simple test async def simple_test(): connector = BinanceConnector() # Test symbol normalization normalized = connector.normalize_symbol("BTC-USDT") print(f"Symbol normalization: BTC-USDT -> {normalized}") # Test message type detection msg_type = connector._get_message_type({"e": "depthUpdate"}) print(f"Message type detection: {msg_type}") print("Simple Binance connector test completed") asyncio.run(simple_test())