"""End-to-end tests for pricing data flow.""" import pytest import asyncio from unittest.mock import Mock, patch, AsyncMock from decimal import Decimal from datetime import datetime from src.data.pricing_service import get_pricing_service, PricingService from src.data.providers.base_provider import BasePricingProvider class MockProvider(BasePricingProvider): """Mock provider for E2E testing.""" def __init__(self, name: str = "MockProvider"): super().__init__() self._name = name self._ticker_data = { 'symbol': 'BTC/USD', 'bid': Decimal('50000'), 'ask': Decimal('50001'), 'last': Decimal('50000.5'), 'high': Decimal('51000'), 'low': Decimal('49000'), 'volume': Decimal('1000000'), 'timestamp': int(datetime.now().timestamp() * 1000), } self._ohlcv_data = [ [int(datetime.now().timestamp() * 1000), 50000, 51000, 49000, 50000, 1000], ] self._callbacks = [] @property def name(self) -> str: return self._name @property def supports_websocket(self) -> bool: return False def connect(self) -> bool: self._connected = True return True def disconnect(self): self._connected = False self._callbacks.clear() def get_ticker(self, symbol: str): return self._ticker_data.copy() def get_ohlcv(self, symbol: str, timeframe: str = '1h', since=None, limit: int = 100): return self._ohlcv_data.copy() def subscribe_ticker(self, symbol: str, callback) -> bool: if symbol not in self._subscribers: self._subscribers[symbol] = [] self._subscribers[symbol].append(callback) self._callbacks.append((symbol, callback)) # Simulate price update import threading def send_update(): import time time.sleep(0.1) if callback and symbol in self._subscribers: callback(self._ticker_data.copy()) thread = threading.Thread(target=send_update, daemon=True) thread.start() return True @pytest.mark.e2e class TestPricingDataE2E: """End-to-end tests for pricing data system.""" @pytest.fixture(autouse=True) def reset_service(self): """Reset pricing service between tests.""" import src.data.pricing_service src.data.pricing_service._pricing_service = None yield src.data.pricing_service._pricing_service = None @patch('src.data.pricing_service.get_config') def test_pricing_service_initialization(self, mock_get_config): """Test pricing service initializes correctly.""" mock_config = Mock() mock_config.get = Mock(side_effect=lambda key, default=None: { "data_providers.primary": [ {"name": "mock", "enabled": True, "priority": 1} ], "data_providers.fallback": {"enabled": True, "api_key": ""}, "data_providers.caching.ticker_ttl": 2, "data_providers.caching.ohlcv_ttl": 60, "data_providers.caching.max_cache_size": 1000, }.get(key, default)) mock_get_config.return_value = mock_config # Patch provider initialization to use mock with patch('src.data.pricing_service.CCXTProvider') as mock_ccxt: mock_provider = MockProvider("CCXT-Mock") mock_ccxt.return_value = mock_provider service = get_pricing_service() assert service is not None assert service.cache is not None assert service.health_monitor is not None def test_get_ticker_with_failover(self): """Test getting ticker with provider failover.""" # Create service with mock providers service = PricingService() # Create two providers - one will fail, one will succeed provider1 = MockProvider("Provider1") provider1.get_ticker = Mock(side_effect=Exception("Provider1 failed")) provider1.connect = Mock(return_value=True) provider2 = MockProvider("Provider2") provider2.connect = Mock(return_value=True) service._providers = {"Provider1": provider1, "Provider2": provider2} service._provider_priority = ["Provider1", "Provider2"] service._active_provider = "Provider1" # Get ticker - should failover to Provider2 ticker = service.get_ticker("BTC/USD", use_cache=False) assert ticker is not None assert ticker['symbol'] == 'BTC/USD' assert provider1.get_ticker.called assert provider2.get_ticker.called def test_caching_works(self): """Test that caching works correctly.""" service = PricingService() provider = MockProvider() provider.connect() service._providers["MockProvider"] = provider service._active_provider = "MockProvider" # First call - should hit provider ticker1 = service.get_ticker("BTC/USD", use_cache=True) # Modify provider response provider._ticker_data['last'] = Decimal('60000') # Second call - should get cached value ticker2 = service.get_ticker("BTC/USD", use_cache=True) # Should be same as first call (cached) assert ticker1['last'] == ticker2['last'] def test_subscription_and_updates(self): """Test subscribing to price updates.""" service = PricingService() provider = MockProvider() provider.connect() service._providers["MockProvider"] = provider service._active_provider = "MockProvider" received_updates = [] def callback(data): received_updates.append(data) # Subscribe success = service.subscribe_ticker("BTC/USD", callback) assert success is True # Wait for update import time time.sleep(0.2) # Should have received at least one update assert len(received_updates) > 0 assert received_updates[0]['symbol'] == 'BTC/USD' def test_health_monitoring(self): """Test health monitoring tracks provider status.""" service = PricingService() provider = MockProvider("TestProvider") provider.connect() service._providers["TestProvider"] = provider service._active_provider = "TestProvider" # Record some operations service.health_monitor.record_success("TestProvider", 0.1) service.health_monitor.record_success("TestProvider", 0.2) service.health_monitor.record_failure("TestProvider") # Check health health = service.get_provider_health("TestProvider") assert health is not None assert health['success_count'] == 2 assert health['failure_count'] == 1 assert health['avg_response_time'] > 0 def test_provider_priority_selection(self): """Test that providers are selected by priority.""" service = PricingService() provider1 = MockProvider("Provider1") provider1.connect() provider2 = MockProvider("Provider2") provider2.connect() service._providers = {"Provider1": provider1, "Provider2": provider2} service._provider_priority = ["Provider1", "Provider2"] # Select active provider active = service._select_active_provider() assert active == "Provider1" # Should select first in priority def test_cache_stats(self): """Test cache statistics are tracked.""" service = PricingService() # Perform some cache operations service.cache.set("key1", "value1") service.cache.get("key1") # Hit service.cache.get("missing") # Miss stats = service.get_cache_stats() assert stats['hits'] >= 1 assert stats['misses'] >= 1 assert stats['size'] >= 1 def test_get_ohlcv_flow(self): """Test complete OHLCV data flow.""" service = PricingService() provider = MockProvider() provider.connect() service._providers["MockProvider"] = provider service._active_provider = "MockProvider" # Get OHLCV data ohlcv = service.get_ohlcv("BTC/USD", "1h", limit=10, use_cache=False) assert len(ohlcv) > 0 assert len(ohlcv[0]) == 6 # timestamp, open, high, low, close, volume assert ohlcv[0][0] > 0 # Valid timestamp def test_unsubscribe_ticker(self): """Test unsubscribing from ticker updates.""" service = PricingService() provider = MockProvider() provider.connect() service._providers["MockProvider"] = provider service._active_provider = "MockProvider" callback = Mock() # Subscribe service.subscribe_ticker("BTC/USD", callback) assert "ticker:BTC/USD" in service._subscriptions # Unsubscribe service.unsubscribe_ticker("BTC/USD", callback) assert "ticker:BTC/USD" not in service._subscriptions def test_multiple_symbol_subscriptions(self): """Test subscribing to multiple symbols.""" service = PricingService() provider = MockProvider() provider.connect() service._providers["MockProvider"] = provider service._active_provider = "MockProvider" callback1 = Mock() callback2 = Mock() # Subscribe to multiple symbols service.subscribe_ticker("BTC/USD", callback1) service.subscribe_ticker("ETH/USD", callback2) assert "ticker:BTC/USD" in service._subscriptions assert "ticker:ETH/USD" in service._subscriptions assert len(service._subscriptions) == 2 @pytest.mark.e2e @pytest.mark.asyncio class TestPricingDataWebSocketE2E: """E2E tests for WebSocket pricing updates.""" async def test_websocket_price_broadcast(self): """Test WebSocket broadcasts price updates.""" # This test would require a running WebSocket server # For now, we test the integration point from backend.api.websocket import ConnectionManager manager = ConnectionManager() # Mock pricing service with patch('backend.api.websocket.get_pricing_service') as mock_get_service: mock_service = Mock() mock_service.subscribe_ticker = Mock(return_value=True) mock_get_service.return_value = mock_service # Subscribe to symbol manager.subscribe_to_symbol("BTC/USD") assert "BTC/USD" in manager.subscribed_symbols assert mock_service.subscribe_ticker.called async def test_websocket_subscription_flow(self): """Test WebSocket subscription and unsubscription.""" from backend.api.websocket import ConnectionManager manager = ConnectionManager() with patch('backend.api.websocket.get_pricing_service') as mock_get_service: mock_service = Mock() mock_service.subscribe_ticker = Mock(return_value=True) mock_service.unsubscribe_ticker = Mock() mock_get_service.return_value = mock_service # Subscribe manager.subscribe_to_symbol("BTC/USD") assert "BTC/USD" in manager.subscribed_symbols # Unsubscribe manager.unsubscribe_from_symbol("BTC/USD") assert "BTC/USD" not in manager.subscribed_symbols assert mock_service.unsubscribe_ticker.called