WebhockTransfer/tests/test_health.py
auto-bot 6f4793a330 feat: add comprehensive health checks and Prometheus metrics
- Add app/health.py with HealthChecker and MetricsCollector classes
- Implement composite /health endpoint checking DB and HTTP client
- Add /metrics endpoint with Prometheus exposition format
- Add webhook request metrics (counters, histograms, gauges)
- Create app/http_client.py for shared AsyncClient management
- Update app/main.py lifespan to init/close HTTP client
- Add comprehensive tests in tests/test_health.py

This enables proper observability with health checks and metrics
for monitoring system status and webhook processing performance.
2025-12-24 11:10:54 +08:00

181 lines
7.1 KiB
Python

import pytest
from unittest.mock import patch, AsyncMock, Mock
from app.health import HealthChecker, MetricsCollector
class TestHealthChecker:
@pytest.mark.asyncio
async def test_database_check_success(self):
"""Test successful database health check."""
checker = HealthChecker()
# Mock successful database operation (SessionLocal returns sync session)
with patch('app.health.SessionLocal') as mock_session:
mock_db = Mock()
mock_session.return_value = mock_db
result = await checker.check_database()
assert result["status"] == "ok"
assert result["cached"] is False
assert "last_check" in result
mock_db.execute.assert_called_with("SELECT 1")
mock_db.close.assert_called_once()
@pytest.mark.asyncio
async def test_database_check_failure(self):
"""Test failed database health check."""
checker = HealthChecker()
with patch('app.health.SessionLocal') as mock_session:
mock_db = Mock()
mock_session.return_value = mock_db
mock_db.execute.side_effect = Exception("Connection failed")
result = await checker.check_database()
assert result["status"] == "error"
assert "Connection failed" in result["error"]
assert result["cached"] is False
@pytest.mark.asyncio
async def test_http_client_check_success(self):
"""Test successful HTTP client health check."""
checker = HealthChecker()
with patch('app.health.get_http_client') as mock_get_client:
mock_client = AsyncMock()
mock_get_client.return_value = mock_client
result = await checker.check_http_client()
assert result["status"] == "ok"
assert result["cached"] is False
@pytest.mark.asyncio
async def test_http_client_check_with_fallback(self):
"""Test HTTP client check when primary client is None (fallback to httpx)."""
checker = HealthChecker()
with patch('app.health.get_http_client') as mock_get_client, \
patch('httpx.AsyncClient') as mock_httpx_client:
mock_get_client.return_value = None
mock_client = AsyncMock()
mock_httpx_client.return_value.__aenter__ = AsyncMock(return_value=mock_client)
mock_httpx_client.return_value.__aexit__ = AsyncMock(return_value=None)
result = await checker.check_http_client()
assert result["status"] == "ok"
assert result["cached"] is False
@pytest.mark.asyncio
async def test_overall_health_check_success(self):
"""Test comprehensive health check when all components are healthy."""
checker = HealthChecker()
with patch.object(checker, 'check_database', new_callable=AsyncMock) as mock_db_check, \
patch.object(checker, 'check_http_client', new_callable=AsyncMock) as mock_http_check:
mock_db_check.return_value = {"status": "ok"}
mock_http_check.return_value = {"status": "ok"}
result = await checker.check_overall()
assert result["status"] == "ok"
assert "timestamp" in result
assert "checks" in result
assert result["checks"]["database"]["status"] == "ok"
assert result["checks"]["http_client"]["status"] == "ok"
assert result["version"] == "2.2.0"
assert result["service"] == "webhook-relay"
@pytest.mark.asyncio
async def test_overall_health_check_partial_failure(self):
"""Test comprehensive health check when some components fail."""
checker = HealthChecker()
with patch.object(checker, 'check_database', new_callable=AsyncMock) as mock_db_check, \
patch.object(checker, 'check_http_client', new_callable=AsyncMock) as mock_http_check:
mock_db_check.return_value = {"status": "ok"}
mock_http_check.return_value = {"status": "error", "error": "Connection timeout"}
result = await checker.check_overall()
assert result["status"] == "error"
assert result["checks"]["database"]["status"] == "ok"
assert result["checks"]["http_client"]["status"] == "error"
class TestMetricsCollector:
def test_increment_counter(self):
"""Test counter metric increment."""
collector = MetricsCollector()
collector.increment_counter("test_counter")
collector.increment_counter("test_counter", 2.0)
prometheus_output = collector.get_prometheus_format()
assert "test_counter 3.0" in prometheus_output
assert "# TYPE test_counter counter" in prometheus_output
def test_increment_counter_with_labels(self):
"""Test counter metric with labels."""
collector = MetricsCollector()
collector.increment_counter("requests_total", labels={"method": "POST", "status": "200"})
prometheus_output = collector.get_prometheus_format()
assert 'requests_total{method=POST,status=200} 1.0' in prometheus_output
def test_set_gauge(self):
"""Test gauge metric setting."""
collector = MetricsCollector()
collector.set_gauge("memory_usage", 85.5, labels={"unit": "percent"})
prometheus_output = collector.get_prometheus_format()
assert 'memory_usage{unit=percent} 85.5' in prometheus_output
assert "# TYPE memory_usage gauge" in prometheus_output
def test_observe_histogram(self):
"""Test histogram metric observation."""
collector = MetricsCollector()
collector.observe_histogram("response_time", 0.125, labels={"endpoint": "/health"})
prometheus_output = collector.get_prometheus_format()
assert 'response_time{endpoint=/health} 0.125' in prometheus_output
assert "# TYPE response_time histogram" in prometheus_output
def test_uptime_metric(self):
"""Test that uptime metric is automatically included."""
collector = MetricsCollector()
prometheus_output = collector.get_prometheus_format()
assert "# TYPE uptime_seconds gauge" in prometheus_output
assert "uptime_seconds " in prometheus_output
def test_prometheus_format_structure(self):
"""Test overall Prometheus format structure."""
collector = MetricsCollector()
collector.increment_counter("webhook_requests_total", labels={"status": "success"})
collector.observe_histogram("webhook_processing_duration_seconds", 0.5)
output = collector.get_prometheus_format()
# Should contain HELP comments for known metrics
assert "# HELP webhook_requests_total Total number of webhook requests processed" in output
assert "# HELP webhook_processing_duration_seconds Time spent processing webhooks" in output
# Should have proper TYPE declarations
assert "# TYPE webhook_requests_total counter" in output
assert "# TYPE webhook_processing_duration_seconds histogram" in output
# Should have metric values
assert 'webhook_requests_total{status=success} 1.0' in output
assert 'webhook_processing_duration_seconds 0.5' in output