""" Unit tests for Bybit exchange connector. """ import asyncio import pytest from unittest.mock import Mock, AsyncMock, patch from datetime import datetime, timezone from ..connectors.bybit_connector import BybitConnector from ..models.core import OrderBookSnapshot, TradeEvent, PriceLevel class TestBybitConnector: """Test suite for Bybit connector.""" @pytest.fixture def connector(self): """Create connector instance for testing.""" return BybitConnector(use_testnet=True) def test_initialization(self, connector): """Test connector initializes correctly.""" assert connector.exchange_name == "bybit" assert connector.use_testnet is True assert connector.TESTNET_URL in connector.websocket_url assert 'orderbook' in connector.message_handlers assert 'publicTrade' in connector.message_handlers def test_symbol_normalization(self, connector): """Test symbol normalization to Bybit format.""" # Test standard conversions (Bybit uses same format as Binance) assert connector.normalize_symbol('BTCUSDT') == 'BTCUSDT' assert connector.normalize_symbol('ETHUSDT') == 'ETHUSDT' assert connector.normalize_symbol('btcusdt') == 'BTCUSDT' # Test with separators assert connector.normalize_symbol('BTC-USDT') == 'BTCUSDT' assert connector.normalize_symbol('BTC/USDT') == 'BTCUSDT' def test_message_type_detection(self, connector): """Test message type detection.""" # Test orderbook message orderbook_message = { 'topic': 'orderbook.50.BTCUSDT', 'data': {'b': [], 'a': []} } assert connector._get_message_type(orderbook_message) == 'orderbook' # Test trade message trade_message = { 'topic': 'publicTrade.BTCUSDT', 'data': [] } assert connector._get_message_type(trade_message) == 'publicTrade' # Test operation message op_message = {'op': 'subscribe', 'success': True} assert connector._get_message_type(op_message) == 'subscribe' # Test response message response_message = {'success': True, 'ret_msg': 'OK'} assert connector._get_message_type(response_message) == 'response' @pytest.mark.asyncio async def test_subscription_methods(self, connector): """Test subscription and unsubscription methods.""" # Mock the _send_message method connector._send_message = AsyncMock(return_value=True) # Test order book subscription await connector.subscribe_orderbook('BTCUSDT') # Verify subscription was tracked assert 'BTCUSDT' in connector.subscriptions assert 'orderbook' in connector.subscriptions['BTCUSDT'] assert 'orderbook.50.BTCUSDT' in connector.subscribed_topics # Verify correct message was sent connector._send_message.assert_called() call_args = connector._send_message.call_args[0][0] assert call_args['op'] == 'subscribe' assert 'orderbook.50.BTCUSDT' in call_args['args'] # Test trade subscription await connector.subscribe_trades('ETHUSDT') assert 'ETHUSDT' in connector.subscriptions assert 'trades' in connector.subscriptions['ETHUSDT'] assert 'publicTrade.ETHUSDT' in connector.subscribed_topics # Test unsubscription await connector.unsubscribe_orderbook('BTCUSDT') # Verify unsubscription if 'BTCUSDT' in connector.subscriptions: assert 'orderbook' not in connector.subscriptions['BTCUSDT'] @pytest.mark.asyncio async def test_orderbook_snapshot_parsing(self, connector): """Test parsing order book snapshot data.""" # Mock order book data from Bybit mock_data = { 'u': 12345, 'ts': 1609459200000, 'b': [ ['50000.00', '1.5'], ['49999.00', '2.0'] ], 'a': [ ['50001.00', '1.2'], ['50002.00', '0.8'] ] } # Parse the data orderbook = connector._parse_orderbook_snapshot(mock_data, 'BTCUSDT') # Verify parsing assert isinstance(orderbook, OrderBookSnapshot) assert orderbook.symbol == 'BTCUSDT' assert orderbook.exchange == 'bybit' assert orderbook.sequence_id == 12345 # Verify bids assert len(orderbook.bids) == 2 assert orderbook.bids[0].price == 50000.00 assert orderbook.bids[0].size == 1.5 # Verify asks assert len(orderbook.asks) == 2 assert orderbook.asks[0].price == 50001.00 assert orderbook.asks[0].size == 1.2 @pytest.mark.asyncio async def test_orderbook_update_handling(self, connector): """Test handling order book update messages.""" # Mock callback callback_called = False received_data = None def mock_callback(data): nonlocal callback_called, received_data callback_called = True received_data = data connector.add_data_callback(mock_callback) # Mock Bybit orderbook update message update_message = { 'topic': 'orderbook.50.BTCUSDT', 'ts': 1609459200000, 'data': { 'u': 12345, 'b': [['50000.00', '1.5']], 'a': [['50001.00', '1.2']] } } # Handle the message await connector._handle_orderbook_update(update_message) # Verify callback was called assert callback_called assert isinstance(received_data, OrderBookSnapshot) assert received_data.symbol == 'BTCUSDT' assert received_data.exchange == 'bybit' assert received_data.sequence_id == 12345 @pytest.mark.asyncio async def test_trade_handling(self, connector): """Test handling trade messages.""" # Mock callback callback_called = False received_trades = [] def mock_callback(data): nonlocal callback_called callback_called = True received_trades.append(data) connector.add_data_callback(mock_callback) # Mock Bybit trade message trade_message = { 'topic': 'publicTrade.BTCUSDT', 'ts': 1609459200000, 'data': [ { 'T': 1609459200000, 'p': '50000.50', 'v': '0.1', 'S': 'Buy', 'i': '12345' } ] } # Handle the message await connector._handle_trade_update(trade_message) # Verify callback was called assert callback_called assert len(received_trades) == 1 trade = received_trades[0] assert isinstance(trade, TradeEvent) assert trade.symbol == 'BTCUSDT' assert trade.exchange == 'bybit' assert trade.price == 50000.50 assert trade.size == 0.1 assert trade.side == 'buy' assert trade.trade_id == '12345' @pytest.mark.asyncio async def test_get_symbols(self, connector): """Test getting available symbols.""" # Mock HTTP response mock_response_data = { 'retCode': 0, 'result': { 'list': [ { 'symbol': 'BTCUSDT', 'status': 'Trading' }, { 'symbol': 'ETHUSDT', 'status': 'Trading' }, { 'symbol': 'DISABLEDUSDT', 'status': 'Closed' } ] } } with patch('aiohttp.ClientSession.get') as mock_get: mock_response = AsyncMock() mock_response.status = 200 mock_response.json = AsyncMock(return_value=mock_response_data) mock_get.return_value.__aenter__.return_value = mock_response symbols = await connector.get_symbols() # Should only return trading symbols assert 'BTCUSDT' in symbols assert 'ETHUSDT' in symbols assert 'DISABLEDUSDT' not in symbols @pytest.mark.asyncio async def test_get_orderbook_snapshot(self, connector): """Test getting order book snapshot from REST API.""" # Mock HTTP response mock_orderbook = { 'retCode': 0, 'result': { 'ts': 1609459200000, 'u': 12345, 'b': [['50000.00', '1.5']], 'a': [['50001.00', '1.2']] } } with patch('aiohttp.ClientSession.get') as mock_get: mock_response = AsyncMock() mock_response.status = 200 mock_response.json = AsyncMock(return_value=mock_orderbook) mock_get.return_value.__aenter__.return_value = mock_response orderbook = await connector.get_orderbook_snapshot('BTCUSDT') assert isinstance(orderbook, OrderBookSnapshot) assert orderbook.symbol == 'BTCUSDT' assert orderbook.exchange == 'bybit' assert len(orderbook.bids) == 1 assert len(orderbook.asks) == 1 def test_statistics(self, connector): """Test getting connector statistics.""" # Add some test data connector.subscribed_topics.add('orderbook.50.BTCUSDT') stats = connector.get_bybit_stats() assert stats['exchange'] == 'bybit' assert 'orderbook.50.BTCUSDT' in stats['subscribed_topics'] assert stats['use_testnet'] is True assert 'authenticated' in stats async def test_bybit_integration(): """Integration test for Bybit connector.""" connector = BybitConnector(use_testnet=True) try: # Test basic functionality assert connector.exchange_name == "bybit" # Test symbol normalization assert connector.normalize_symbol('BTCUSDT') == 'BTCUSDT' assert connector.normalize_symbol('btc-usdt') == 'BTCUSDT' # Test message type detection test_message = {'topic': 'orderbook.50.BTCUSDT', 'data': {}} assert connector._get_message_type(test_message) == 'orderbook' print("✓ Bybit connector integration test passed") return True except Exception as e: print(f"✗ Bybit connector integration test failed: {e}") return False if __name__ == "__main__": # Run integration test success = asyncio.run(test_bybit_integration()) if not success: exit(1)