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