Files
gogo2/COBY/tests/test_coinbase_connector.py
Dobromir Popov 4170553cf3 Binance (completed previously)
 Coinbase Pro (completed in this task)
 Kraken (completed in this task)
2025-08-04 23:21:21 +03:00

364 lines
13 KiB
Python

"""
Unit tests for Coinbase exchange connector.
"""
import asyncio
import pytest
from unittest.mock import Mock, AsyncMock, patch
from datetime import datetime, timezone
from ..connectors.coinbase_connector import CoinbaseConnector
from ..models.core import OrderBookSnapshot, TradeEvent, PriceLevel
class TestCoinbaseConnector:
"""Test suite for Coinbase connector."""
@pytest.fixture
def connector(self):
"""Create connector instance for testing."""
return CoinbaseConnector(use_sandbox=True)
def test_initialization(self, connector):
"""Test connector initializes correctly."""
assert connector.exchange_name == "coinbase"
assert connector.use_sandbox is True
assert connector.SANDBOX_URL in connector.websocket_url
assert 'l2update' in connector.message_handlers
assert 'match' in connector.message_handlers
def test_symbol_normalization(self, connector):
"""Test symbol normalization to Coinbase format."""
# Test standard conversions
assert connector.normalize_symbol('BTCUSDT') == 'BTC-USD'
assert connector.normalize_symbol('ETHUSDT') == 'ETH-USD'
assert connector.normalize_symbol('ADAUSDT') == 'ADA-USD'
# Test generic conversion
assert connector.normalize_symbol('LINKUSDT') == 'LINK-USD'
# Test already correct format
assert connector.normalize_symbol('BTC-USD') == 'BTC-USD'
def test_symbol_denormalization(self, connector):
"""Test converting Coinbase format back to standard."""
assert connector._denormalize_symbol('BTC-USD') == 'BTCUSDT'
assert connector._denormalize_symbol('ETH-USD') == 'ETHUSDT'
assert connector._denormalize_symbol('ADA-USD') == 'ADAUSDT'
# Test other quote currencies
assert connector._denormalize_symbol('BTC-EUR') == 'BTCEUR'
def test_message_type_detection(self, connector):
"""Test message type detection."""
# Test l2update message
l2_message = {'type': 'l2update', 'product_id': 'BTC-USD'}
assert connector._get_message_type(l2_message) == 'l2update'
# Test match message
match_message = {'type': 'match', 'product_id': 'BTC-USD'}
assert connector._get_message_type(match_message) == 'match'
# Test error message
error_message = {'type': 'error', 'message': 'Invalid signature'}
assert connector._get_message_type(error_message) == 'error'
# Test unknown message
unknown_message = {'data': 'something'}
assert connector._get_message_type(unknown_message) == 'unknown'
@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 'level2' in connector.subscribed_channels
assert 'BTC-USD' in connector.product_ids
# Verify correct message was sent
connector._send_message.assert_called()
call_args = connector._send_message.call_args[0][0]
assert call_args['type'] == 'subscribe'
assert 'BTC-USD' in call_args['product_ids']
assert 'level2' in call_args['channels']
# Test trade subscription
await connector.subscribe_trades('ETHUSDT')
assert 'ETHUSDT' in connector.subscriptions
assert 'trades' in connector.subscriptions['ETHUSDT']
assert 'matches' in connector.subscribed_channels
assert 'ETH-USD' in connector.product_ids
# 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 Coinbase
mock_data = {
'sequence': 12345,
'bids': [
['50000.00', '1.5', 1],
['49999.00', '2.0', 2]
],
'asks': [
['50001.00', '1.2', 1],
['50002.00', '0.8', 1]
]
}
# Parse the data
orderbook = connector._parse_orderbook_snapshot(mock_data, 'BTCUSDT')
# Verify parsing
assert isinstance(orderbook, OrderBookSnapshot)
assert orderbook.symbol == 'BTCUSDT'
assert orderbook.exchange == 'coinbase'
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
assert orderbook.bids[1].price == 49999.00
assert orderbook.bids[1].size == 2.0
# Verify asks
assert len(orderbook.asks) == 2
assert orderbook.asks[0].price == 50001.00
assert orderbook.asks[0].size == 1.2
assert orderbook.asks[1].price == 50002.00
assert orderbook.asks[1].size == 0.8
@pytest.mark.asyncio
async def test_orderbook_update_handling(self, connector):
"""Test handling order book l2update 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 l2update message
update_message = {
'type': 'l2update',
'product_id': 'BTC-USD',
'time': '2023-01-01T12:00:00.000000Z',
'changes': [
['buy', '50000.00', '1.5'],
['sell', '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 == 'coinbase'
# Verify bids and asks
assert len(received_data.bids) == 1
assert received_data.bids[0].price == 50000.00
assert received_data.bids[0].size == 1.5
assert len(received_data.asks) == 1
assert received_data.asks[0].price == 50001.00
assert received_data.asks[0].size == 1.2
@pytest.mark.asyncio
async def test_trade_handling(self, connector):
"""Test handling trade (match) 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 match message
trade_message = {
'type': 'match',
'product_id': 'BTC-USD',
'time': '2023-01-01T12:00:00.000000Z',
'price': '50000.50',
'size': '0.1',
'side': 'buy',
'trade_id': 12345
}
# Handle the message
await connector._handle_trade_update(trade_message)
# Verify callback was called
assert callback_called
assert isinstance(received_data, TradeEvent)
assert received_data.symbol == 'BTCUSDT'
assert received_data.exchange == 'coinbase'
assert received_data.price == 50000.50
assert received_data.size == 0.1
assert received_data.side == 'buy'
assert received_data.trade_id == '12345'
@pytest.mark.asyncio
async def test_error_handling(self, connector):
"""Test error message handling."""
# Test error message
error_message = {
'type': 'error',
'message': 'Invalid signature',
'reason': 'Authentication failed'
}
# Should not raise exception
await connector._handle_error_message(error_message)
@pytest.mark.asyncio
async def test_get_symbols(self, connector):
"""Test getting available symbols."""
# Mock HTTP response
mock_products = [
{
'id': 'BTC-USD',
'status': 'online',
'trading_disabled': False
},
{
'id': 'ETH-USD',
'status': 'online',
'trading_disabled': False
},
{
'id': 'DISABLED-USD',
'status': 'offline',
'trading_disabled': True
}
]
with patch('aiohttp.ClientSession.get') as mock_get:
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value=mock_products)
mock_get.return_value.__aenter__.return_value = mock_response
symbols = await connector.get_symbols()
# Should only return online, enabled 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 = {
'sequence': 12345,
'bids': [['50000.00', '1.5']],
'asks': [['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 == 'coinbase'
assert len(orderbook.bids) == 1
assert len(orderbook.asks) == 1
def test_authentication_headers(self, connector):
"""Test authentication header generation."""
# Set up credentials
connector.api_key = 'test_key'
connector.api_secret = 'dGVzdF9zZWNyZXQ=' # base64 encoded 'test_secret'
connector.passphrase = 'test_passphrase'
# Test message
test_message = {'type': 'subscribe', 'channels': ['level2']}
# Generate headers
headers = connector._get_auth_headers(test_message)
# Verify headers are present
assert 'CB-ACCESS-KEY' in headers
assert 'CB-ACCESS-SIGN' in headers
assert 'CB-ACCESS-TIMESTAMP' in headers
assert 'CB-ACCESS-PASSPHRASE' in headers
assert headers['CB-ACCESS-KEY'] == 'test_key'
assert headers['CB-ACCESS-PASSPHRASE'] == 'test_passphrase'
def test_statistics(self, connector):
"""Test getting connector statistics."""
# Add some test data
connector.subscribed_channels.add('level2')
connector.product_ids.add('BTC-USD')
stats = connector.get_coinbase_stats()
assert stats['exchange'] == 'coinbase'
assert 'level2' in stats['subscribed_channels']
assert 'BTC-USD' in stats['product_ids']
assert stats['use_sandbox'] is True
assert 'authenticated' in stats
async def test_coinbase_integration():
"""Integration test for Coinbase connector."""
connector = CoinbaseConnector(use_sandbox=True)
try:
# Test basic functionality
assert connector.exchange_name == "coinbase"
# Test symbol normalization
assert connector.normalize_symbol('BTCUSDT') == 'BTC-USD'
assert connector._denormalize_symbol('BTC-USD') == 'BTCUSDT'
# Test message type detection
test_message = {'type': 'l2update', 'product_id': 'BTC-USD'}
assert connector._get_message_type(test_message) == 'l2update'
print("✓ Coinbase connector integration test passed")
return True
except Exception as e:
print(f"✗ Coinbase connector integration test failed: {e}")
return False
if __name__ == "__main__":
# Run integration test
success = asyncio.run(test_coinbase_integration())
if not success:
exit(1)