485 lines
17 KiB
Python
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)"
|
|
) |