308 lines
11 KiB
Python
308 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Integration tests for performance monitoring and admin APIs."""
|
|
|
|
import json
|
|
import os
|
|
import pytest
|
|
import tempfile
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from mes_dashboard.app import create_app
|
|
import mes_dashboard.core.database as db
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create application for testing."""
|
|
db._ENGINE = None
|
|
app = create_app('testing')
|
|
app.config['TESTING'] = True
|
|
app.config['WTF_CSRF_ENABLED'] = False
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
"""Create test client."""
|
|
return app.test_client()
|
|
|
|
|
|
@pytest.fixture
|
|
def admin_client(app, client):
|
|
"""Create authenticated admin client."""
|
|
# Set admin session - the permissions module checks for 'admin' key in session
|
|
with client.session_transaction() as sess:
|
|
sess['admin'] = {'username': 'admin', 'role': 'admin'}
|
|
yield client
|
|
|
|
|
|
class TestAPIResponseFormat:
|
|
"""Test standardized API response format."""
|
|
|
|
def test_success_response_format(self, admin_client):
|
|
"""Success responses have correct format."""
|
|
response = admin_client.get('/admin/api/system-status')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data["success"] is True
|
|
assert "data" in data
|
|
|
|
def test_unauthenticated_redirect(self, client):
|
|
"""Unauthenticated requests redirect to login."""
|
|
response = client.get('/admin/performance')
|
|
|
|
# Should redirect to login page
|
|
assert response.status_code == 302
|
|
|
|
|
|
class TestHealthEndpoints:
|
|
"""Test health check endpoints."""
|
|
|
|
def test_health_basic_endpoint(self, client):
|
|
"""Basic health endpoint returns status."""
|
|
response = client.get('/health')
|
|
|
|
assert response.status_code in (200, 503)
|
|
data = json.loads(response.data)
|
|
assert "status" in data
|
|
assert data["status"] in {"healthy", "degraded", "unhealthy"}
|
|
# Database status is under 'services' key
|
|
assert "services" in data
|
|
assert "database" in data["services"]
|
|
|
|
def test_health_deep_requires_auth(self, client):
|
|
"""Deep health endpoint requires authentication."""
|
|
response = client.get('/health/deep')
|
|
# Redirects to login for unauthenticated requests
|
|
assert response.status_code == 302
|
|
|
|
def test_health_deep_returns_metrics(self, admin_client):
|
|
"""Deep health endpoint returns detailed metrics."""
|
|
response = admin_client.get('/health/deep')
|
|
|
|
if response.status_code == 200:
|
|
data = json.loads(response.data)
|
|
assert "status" in data
|
|
|
|
|
|
class TestSystemStatusAPI:
|
|
"""Test system status API endpoint."""
|
|
|
|
def test_system_status_returns_all_components(self, admin_client):
|
|
"""System status includes all component statuses."""
|
|
response = admin_client.get('/admin/api/system-status')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data["success"] is True
|
|
assert "database" in data["data"]
|
|
assert "redis" in data["data"]
|
|
assert "circuit_breaker" in data["data"]
|
|
assert "runtime_resilience" in data["data"]
|
|
assert "thresholds" in data["data"]["runtime_resilience"]
|
|
assert "restart_churn" in data["data"]["runtime_resilience"]
|
|
assert "recovery_recommendation" in data["data"]["runtime_resilience"]
|
|
assert "worker_pid" in data["data"]
|
|
|
|
|
|
class TestMetricsAPI:
|
|
"""Test metrics API endpoint."""
|
|
|
|
def test_metrics_returns_percentiles(self, admin_client):
|
|
"""Metrics API returns percentile data."""
|
|
response = admin_client.get('/admin/api/metrics')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data["success"] is True
|
|
assert "p50_ms" in data["data"]
|
|
assert "p95_ms" in data["data"]
|
|
assert "p99_ms" in data["data"]
|
|
assert "count" in data["data"]
|
|
assert "slow_count" in data["data"]
|
|
assert "slow_rate" in data["data"]
|
|
|
|
def test_metrics_includes_latencies(self, admin_client):
|
|
"""Metrics API includes latency distribution."""
|
|
response = admin_client.get('/admin/api/metrics')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert "latencies" in data["data"]
|
|
assert isinstance(data["data"]["latencies"], list)
|
|
|
|
|
|
class TestLogsAPI:
|
|
"""Test logs API endpoint."""
|
|
|
|
def test_logs_api_returns_logs(self, admin_client):
|
|
"""Logs API returns log entries."""
|
|
response = admin_client.get('/admin/api/logs')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data["success"] is True
|
|
assert "logs" in data["data"]
|
|
assert "enabled" in data["data"]
|
|
|
|
def test_logs_api_filter_by_level(self, admin_client):
|
|
"""Logs API filters by level."""
|
|
response = admin_client.get('/admin/api/logs?level=ERROR')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data["success"] is True
|
|
|
|
def test_logs_api_filter_by_search(self, admin_client):
|
|
"""Logs API filters by search term."""
|
|
response = admin_client.get('/admin/api/logs?q=database')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data["success"] is True
|
|
|
|
def test_logs_api_pagination(self, admin_client):
|
|
"""Logs API supports pagination with limit and offset."""
|
|
# Test with limit=10
|
|
response = admin_client.get('/admin/api/logs?limit=10&offset=0')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data["success"] is True
|
|
assert "total" in data["data"]
|
|
assert "logs" in data["data"]
|
|
assert len(data["data"]["logs"]) <= 10
|
|
|
|
def test_logs_api_pagination_offset(self, admin_client):
|
|
"""Logs API offset skips entries correctly."""
|
|
# Get first page
|
|
response1 = admin_client.get('/admin/api/logs?limit=5&offset=0')
|
|
data1 = json.loads(response1.data)
|
|
|
|
# Get second page
|
|
response2 = admin_client.get('/admin/api/logs?limit=5&offset=5')
|
|
data2 = json.loads(response2.data)
|
|
|
|
# Total should be the same
|
|
assert data1["data"]["total"] == data2["data"]["total"]
|
|
|
|
# If there are enough logs, pages should be different
|
|
if data1["data"]["total"] > 5:
|
|
logs1_ids = [log.get("id") for log in data1["data"]["logs"]]
|
|
logs2_ids = [log.get("id") for log in data2["data"]["logs"]]
|
|
# No overlap between pages
|
|
assert not set(logs1_ids) & set(logs2_ids)
|
|
|
|
|
|
class TestLogsCleanupAPI:
|
|
"""Test log cleanup API endpoint."""
|
|
|
|
def test_logs_cleanup_requires_auth(self, client):
|
|
"""Log cleanup requires admin authentication."""
|
|
response = client.post('/admin/api/logs/cleanup')
|
|
# Should redirect to login page
|
|
assert response.status_code == 302
|
|
|
|
def test_logs_cleanup_success(self, admin_client):
|
|
"""Log cleanup returns success with stats."""
|
|
response = admin_client.post('/admin/api/logs/cleanup')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data["success"] is True
|
|
assert "deleted" in data["data"]
|
|
assert "before" in data["data"]
|
|
assert "after" in data["data"]
|
|
assert "count" in data["data"]["before"]
|
|
assert "size_bytes" in data["data"]["before"]
|
|
|
|
|
|
class TestWorkerControlAPI:
|
|
"""Test worker control API endpoints."""
|
|
|
|
def test_worker_status_returns_info(self, admin_client):
|
|
"""Worker status API returns worker information."""
|
|
response = admin_client.get('/admin/api/worker/status')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data["success"] is True
|
|
assert "worker_pid" in data["data"]
|
|
assert "cooldown" in data["data"]
|
|
assert "resilience" in data["data"]
|
|
assert "restart_history" in data["data"]
|
|
assert "restart_churn" in data["data"]["resilience"]
|
|
assert "last_restart" in data["data"]
|
|
|
|
def test_worker_restart_requires_auth(self, client):
|
|
"""Worker restart requires admin authentication."""
|
|
response = client.post('/admin/api/worker/restart')
|
|
# Should redirect to login page for unauthenticated requests
|
|
assert response.status_code == 302
|
|
|
|
def test_worker_restart_writes_flag(self, admin_client):
|
|
"""Worker restart creates flag file."""
|
|
# Use a temp file for the flag
|
|
fd, temp_flag = tempfile.mkstemp()
|
|
os.close(fd)
|
|
os.unlink(temp_flag) # Remove so we can test creation
|
|
|
|
with patch('mes_dashboard.routes.admin_routes.RESTART_FLAG_PATH', temp_flag):
|
|
with patch('mes_dashboard.routes.admin_routes._check_restart_cooldown', return_value=(False, 0)):
|
|
response = admin_client.post('/admin/api/worker/restart')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert data["success"] is True
|
|
|
|
# Cleanup
|
|
try:
|
|
os.unlink(temp_flag)
|
|
except OSError:
|
|
pass
|
|
|
|
def test_worker_restart_cooldown(self, admin_client):
|
|
"""Worker restart respects cooldown."""
|
|
with patch('mes_dashboard.routes.admin_routes._check_restart_cooldown', return_value=(True, 45)):
|
|
response = admin_client.post('/admin/api/worker/restart')
|
|
|
|
assert response.status_code == 429
|
|
data = json.loads(response.data)
|
|
assert data["success"] is False
|
|
assert "cooldown" in data["error"]["message"].lower()
|
|
|
|
|
|
class TestCircuitBreakerIntegration:
|
|
"""Test circuit breaker integration with database layer."""
|
|
|
|
def test_circuit_breaker_status_in_system_status(self, admin_client):
|
|
"""Circuit breaker status is included in system status."""
|
|
response = admin_client.get('/admin/api/system-status')
|
|
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
cb_status = data["data"]["circuit_breaker"]
|
|
assert "state" in cb_status
|
|
assert "enabled" in cb_status
|
|
|
|
|
|
class TestPerformancePage:
|
|
"""Test performance monitoring page."""
|
|
|
|
def test_performance_page_requires_auth(self, client):
|
|
"""Performance page requires admin authentication."""
|
|
response = client.get('/admin/performance')
|
|
# Should redirect to login
|
|
assert response.status_code == 302
|
|
|
|
def test_performance_page_loads(self, admin_client):
|
|
"""Performance page loads for admin users."""
|
|
response = admin_client.get('/admin/performance')
|
|
|
|
# Should be 200 for authenticated admin
|
|
assert response.status_code == 200
|
|
# Check for performance-related content
|
|
data_str = response.data.decode('utf-8', errors='ignore').lower()
|
|
assert 'performance' in data_str or '效能' in data_str
|