Files
gogo2/COBY/tests/test_e2e_dashboard.py
Dobromir Popov 622d059aae 18: tests, fixes
2025-08-05 14:11:49 +03:00

485 lines
17 KiB
Python

"""
End-to-end tests for web dashboard functionality.
"""
import pytest
import asyncio
import json
from datetime import datetime, timezone
from unittest.mock import Mock, AsyncMock, patch
from typing import Dict, Any, List
import aiohttp
from aiohttp import web, WSMsgType
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
from ..api.rest_api import create_app
from ..api.websocket_server import WebSocketServer
from ..models.core import OrderBookSnapshot, TradeEvent, PriceLevel
from ..utils.logging import get_logger
logger = get_logger(__name__)
class TestDashboardAPI(AioHTTPTestCase):
"""Test dashboard REST API endpoints"""
async def get_application(self):
"""Create test application"""
return create_app()
@unittest_run_loop
async def test_health_endpoint(self):
"""Test health check endpoint"""
resp = await self.client.request("GET", "/health")
self.assertEqual(resp.status, 200)
data = await resp.json()
self.assertIn('status', data)
self.assertIn('timestamp', data)
self.assertEqual(data['status'], 'healthy')
@unittest_run_loop
async def test_metrics_endpoint(self):
"""Test metrics endpoint"""
resp = await self.client.request("GET", "/metrics")
self.assertEqual(resp.status, 200)
# Should return Prometheus format
text = await resp.text()
self.assertIn('# TYPE', text)
@unittest_run_loop
async def test_orderbook_endpoint(self):
"""Test order book data endpoint"""
# Mock data
with patch('COBY.caching.redis_manager.redis_manager') as mock_redis:
mock_redis.get.return_value = {
'symbol': 'BTCUSDT',
'exchange': 'binance',
'bids': [{'price': 50000.0, 'size': 1.0}],
'asks': [{'price': 50010.0, 'size': 1.0}]
}
resp = await self.client.request("GET", "/api/orderbook/BTCUSDT")
self.assertEqual(resp.status, 200)
data = await resp.json()
self.assertIn('symbol', data)
self.assertEqual(data['symbol'], 'BTCUSDT')
@unittest_run_loop
async def test_heatmap_endpoint(self):
"""Test heatmap data endpoint"""
with patch('COBY.caching.redis_manager.redis_manager') as mock_redis:
mock_redis.get.return_value = {
'symbol': 'BTCUSDT',
'bucket_size': 1.0,
'data': [
{'price': 50000.0, 'volume': 10.0, 'intensity': 0.8, 'side': 'bid'}
]
}
resp = await self.client.request("GET", "/api/heatmap/BTCUSDT")
self.assertEqual(resp.status, 200)
data = await resp.json()
self.assertIn('symbol', data)
self.assertIn('data', data)
@unittest_run_loop
async def test_exchanges_status_endpoint(self):
"""Test exchanges status endpoint"""
with patch('COBY.connectors.connection_manager.connection_manager') as mock_manager:
mock_manager.get_all_statuses.return_value = {
'binance': 'connected',
'coinbase': 'connected',
'kraken': 'disconnected'
}
resp = await self.client.request("GET", "/api/exchanges/status")
self.assertEqual(resp.status, 200)
data = await resp.json()
self.assertIn('binance', data)
self.assertIn('coinbase', data)
self.assertIn('kraken', data)
@unittest_run_loop
async def test_performance_metrics_endpoint(self):
"""Test performance metrics endpoint"""
with patch('COBY.monitoring.performance_monitor.get_performance_monitor') as mock_monitor:
mock_monitor.return_value.get_performance_dashboard_data.return_value = {
'timestamp': datetime.now(timezone.utc).isoformat(),
'system_metrics': {
'cpu_usage': 45.2,
'memory_usage': 67.8,
'active_connections': 150
},
'performance_summary': {
'throughput': 1250.5,
'error_rate': 0.1,
'avg_latency': 12.3
}
}
resp = await self.client.request("GET", "/api/performance")
self.assertEqual(resp.status, 200)
data = await resp.json()
self.assertIn('system_metrics', data)
self.assertIn('performance_summary', data)
@unittest_run_loop
async def test_static_files_served(self):
"""Test that static files are served correctly"""
# Test dashboard index
resp = await self.client.request("GET", "/")
self.assertEqual(resp.status, 200)
content_type = resp.headers.get('content-type', '')
self.assertIn('text/html', content_type)
@unittest_run_loop
async def test_cors_headers(self):
"""Test CORS headers are present"""
resp = await self.client.request("OPTIONS", "/api/health")
self.assertEqual(resp.status, 200)
# Check CORS headers
self.assertIn('Access-Control-Allow-Origin', resp.headers)
self.assertIn('Access-Control-Allow-Methods', resp.headers)
@unittest_run_loop
async def test_rate_limiting(self):
"""Test API rate limiting"""
# Make many requests quickly
responses = []
for i in range(150): # Exceed rate limit
resp = await self.client.request("GET", "/api/health")
responses.append(resp.status)
# Should have some rate limited responses
rate_limited = [status for status in responses if status == 429]
self.assertGreater(len(rate_limited), 0, "Rate limiting not working")
@unittest_run_loop
async def test_error_handling(self):
"""Test API error handling"""
# Test invalid symbol
resp = await self.client.request("GET", "/api/orderbook/INVALID")
self.assertEqual(resp.status, 404)
data = await resp.json()
self.assertIn('error', data)
@unittest_run_loop
async def test_api_documentation(self):
"""Test API documentation endpoints"""
# Test OpenAPI docs
resp = await self.client.request("GET", "/docs")
self.assertEqual(resp.status, 200)
# Test ReDoc
resp = await self.client.request("GET", "/redoc")
self.assertEqual(resp.status, 200)
class TestWebSocketFunctionality:
"""Test WebSocket functionality"""
@pytest.fixture
async def websocket_server(self):
"""Create WebSocket server for testing"""
server = WebSocketServer(host='localhost', port=8081)
await server.start()
yield server
await server.stop()
@pytest.mark.asyncio
async def test_websocket_connection(self, websocket_server):
"""Test WebSocket connection establishment"""
session = aiohttp.ClientSession()
try:
async with session.ws_connect('ws://localhost:8081/ws/dashboard') as ws:
# Connection should be established
self.assertEqual(ws.closed, False)
# Send ping
await ws.ping()
# Should receive pong
msg = await ws.receive()
self.assertEqual(msg.type, WSMsgType.PONG)
finally:
await session.close()
@pytest.mark.asyncio
async def test_websocket_data_streaming(self, websocket_server):
"""Test real-time data streaming via WebSocket"""
session = aiohttp.ClientSession()
try:
async with session.ws_connect('ws://localhost:8081/ws/dashboard') as ws:
# Subscribe to updates
subscribe_msg = {
'type': 'subscribe',
'channels': ['orderbook', 'trades', 'performance']
}
await ws.send_str(json.dumps(subscribe_msg))
# Should receive subscription confirmation
msg = await ws.receive()
self.assertEqual(msg.type, WSMsgType.TEXT)
data = json.loads(msg.data)
self.assertEqual(data.get('type'), 'subscription_confirmed')
finally:
await session.close()
@pytest.mark.asyncio
async def test_websocket_error_handling(self, websocket_server):
"""Test WebSocket error handling"""
session = aiohttp.ClientSession()
try:
async with session.ws_connect('ws://localhost:8081/ws/dashboard') as ws:
# Send invalid message
invalid_msg = {'invalid': 'message'}
await ws.send_str(json.dumps(invalid_msg))
# Should receive error response
msg = await ws.receive()
self.assertEqual(msg.type, WSMsgType.TEXT)
data = json.loads(msg.data)
self.assertEqual(data.get('type'), 'error')
finally:
await session.close()
@pytest.mark.asyncio
async def test_multiple_websocket_connections(self, websocket_server):
"""Test multiple concurrent WebSocket connections"""
session = aiohttp.ClientSession()
connections = []
try:
# Create multiple connections
for i in range(10):
ws = await session.ws_connect(f'ws://localhost:8081/ws/dashboard')
connections.append(ws)
# All connections should be active
for ws in connections:
self.assertEqual(ws.closed, False)
# Send message to all connections
test_msg = {'type': 'ping', 'id': 'test'}
for ws in connections:
await ws.send_str(json.dumps(test_msg))
# All should receive responses
for ws in connections:
msg = await ws.receive()
self.assertEqual(msg.type, WSMsgType.TEXT)
finally:
# Close all connections
for ws in connections:
if not ws.closed:
await ws.close()
await session.close()
class TestDashboardIntegration:
"""Test dashboard integration with backend services"""
@pytest.fixture
def mock_services(self):
"""Mock backend services"""
services = {
'redis': Mock(),
'timescale': Mock(),
'connectors': Mock(),
'aggregator': Mock(),
'monitor': Mock()
}
# Setup mock responses
services['redis'].get.return_value = {'test': 'data'}
services['timescale'].query.return_value = [{'result': 'data'}]
services['connectors'].get_status.return_value = 'connected'
services['aggregator'].get_heatmap.return_value = {'heatmap': 'data'}
services['monitor'].get_metrics.return_value = {'metrics': 'data'}
return services
@pytest.mark.asyncio
async def test_dashboard_data_flow(self, mock_services):
"""Test complete data flow from backend to dashboard"""
# Simulate data generation
orderbook = OrderBookSnapshot(
symbol="BTCUSDT",
exchange="binance",
timestamp=datetime.now(timezone.utc),
bids=[PriceLevel(price=50000.0, size=1.0)],
asks=[PriceLevel(price=50010.0, size=1.0)]
)
# Mock data processing pipeline
with patch.multiple(
'COBY.processing.data_processor',
DataProcessor=Mock()
):
# Process data
processor = Mock()
processor.normalize_orderbook.return_value = orderbook
# Aggregate data
aggregator = Mock()
aggregator.create_price_buckets.return_value = Mock()
aggregator.generate_heatmap.return_value = Mock()
# Cache data
cache = Mock()
cache.set.return_value = True
# Verify data flows through pipeline
processed = processor.normalize_orderbook({}, "binance")
buckets = aggregator.create_price_buckets(processed)
heatmap = aggregator.generate_heatmap(buckets)
cached = cache.set("test_key", heatmap)
assert processed is not None
assert buckets is not None
assert heatmap is not None
assert cached is True
@pytest.mark.asyncio
async def test_real_time_updates(self, mock_services):
"""Test real-time dashboard updates"""
# Mock WebSocket server
ws_server = Mock()
ws_server.broadcast = AsyncMock()
# Simulate real-time data updates
updates = [
{'type': 'orderbook', 'symbol': 'BTCUSDT', 'data': {}},
{'type': 'trade', 'symbol': 'BTCUSDT', 'data': {}},
{'type': 'performance', 'data': {}}
]
# Send updates
for update in updates:
await ws_server.broadcast(json.dumps(update))
# Verify broadcasts were sent
assert ws_server.broadcast.call_count == len(updates)
@pytest.mark.asyncio
async def test_dashboard_performance_under_load(self, mock_services):
"""Test dashboard performance under high update frequency"""
import time
# Mock high-frequency updates
update_count = 1000
start_time = time.time()
# Simulate processing many updates
for i in range(update_count):
# Mock data processing
mock_services['redis'].get(f"orderbook:BTCUSDT:binance:{i}")
mock_services['aggregator'].get_heatmap(f"BTCUSDT:{i}")
# Small delay to simulate processing
await asyncio.sleep(0.001)
end_time = time.time()
processing_time = end_time - start_time
updates_per_second = update_count / processing_time
# Should handle at least 500 updates per second
assert updates_per_second > 500, f"Dashboard too slow: {updates_per_second:.2f} updates/sec"
@pytest.mark.asyncio
async def test_dashboard_error_recovery(self, mock_services):
"""Test dashboard error recovery"""
# Simulate service failures
mock_services['redis'].get.side_effect = Exception("Redis connection failed")
mock_services['timescale'].query.side_effect = Exception("Database error")
# Dashboard should handle errors gracefully
try:
# Attempt operations that will fail
mock_services['redis'].get("test_key")
except Exception:
# Should recover and continue
pass
try:
mock_services['timescale'].query("SELECT * FROM test")
except Exception:
# Should recover and continue
pass
# Reset services to working state
mock_services['redis'].get.side_effect = None
mock_services['redis'].get.return_value = {'recovered': True}
# Should work again
result = mock_services['redis'].get("test_key")
assert result['recovered'] is True
class TestDashboardUI:
"""Test dashboard UI functionality (requires browser automation)"""
@pytest.mark.skipif(not pytest.config.getoption("--ui"),
reason="UI tests require --ui flag and browser setup")
def test_dashboard_loads(self):
"""Test that dashboard loads in browser"""
# This would require Selenium or similar
# Placeholder for UI tests
pass
@pytest.mark.skipif(not pytest.config.getoption("--ui"),
reason="UI tests require --ui flag and browser setup")
def test_real_time_chart_updates(self):
"""Test that charts update in real-time"""
# This would require browser automation
# Placeholder for UI tests
pass
@pytest.mark.skipif(not pytest.config.getoption("--ui"),
reason="UI tests require --ui flag and browser setup")
def test_responsive_design(self):
"""Test responsive design on different screen sizes"""
# This would require browser automation with different viewport sizes
# Placeholder for UI tests
pass
def pytest_configure(config):
"""Configure pytest with custom markers"""
config.addinivalue_line("markers", "e2e: mark test as end-to-end test")
config.addinivalue_line("markers", "ui: mark test as UI test")
def pytest_addoption(parser):
"""Add custom command line options"""
parser.addoption(
"--e2e",
action="store_true",
default=False,
help="run end-to-end tests"
)
parser.addoption(
"--ui",
action="store_true",
default=False,
help="run UI tests (requires browser setup)"
)