""" Unit tests for health check API endpoints Tests the /api/health and /api/health/db endpoints that are used by load balancers and monitoring systems to verify service availability. """ import pytest from httpx import AsyncClient, ASGITransport from unittest.mock import patch, AsyncMock import pendulum from app.main import app @pytest.fixture async def client(): """Async HTTP test client for API requests""" async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as ac: yield ac class TestBasicHealthEndpoint: """Tests for GET /api/health endpoint""" @pytest.mark.asyncio async def test_health_returns_200(self, client): """Test basic health endpoint returns 200 status""" response = await client.get("/api/health") assert response.status_code == 200 @pytest.mark.asyncio async def test_health_response_structure(self, client): """Test health response has all required fields""" response = await client.get("/api/health") data = response.json() # Verify all required fields present assert "status" in data assert "timestamp" in data assert "environment" in data assert "version" in data @pytest.mark.asyncio async def test_health_status_value(self, client): """Test health status is 'healthy'""" response = await client.get("/api/health") data = response.json() assert data["status"] == "healthy" @pytest.mark.asyncio async def test_health_timestamp_format(self, client): """Test timestamp is valid ISO8601 format""" response = await client.get("/api/health") data = response.json() # Should not raise exception when parsing timestamp = pendulum.parse(data["timestamp"]) assert timestamp is not None # Should be recent (within last minute) now = pendulum.now('UTC') age = now - timestamp assert age.total_seconds() < 60 # Less than 1 minute old @pytest.mark.asyncio async def test_health_environment_field(self, client): """Test environment field is populated""" response = await client.get("/api/health") data = response.json() assert "environment" in data assert isinstance(data["environment"], str) # Should be one of the valid environments assert data["environment"] in ["development", "staging", "production"] @pytest.mark.asyncio async def test_health_version_field(self, client): """Test version field is present and valid""" response = await client.get("/api/health") data = response.json() assert "version" in data assert data["version"] == "1.0.0" class TestDatabaseHealthEndpoint: """Tests for GET /api/health/db endpoint Note: Database error scenarios (connection failures, timeouts) are tested in integration tests where we can control the database state. Mocking SQLAlchemy's AsyncEngine is problematic due to read-only attributes. """ @pytest.mark.asyncio async def test_db_health_returns_200(self, client): """Test database health endpoint returns 200 status""" response = await client.get("/api/health/db") assert response.status_code == 200 @pytest.mark.asyncio async def test_db_health_response_structure(self, client): """Test database health response has all required fields""" response = await client.get("/api/health/db") data = response.json() assert "status" in data assert "database" in data assert "timestamp" in data # Note: status can be "healthy" or "unhealthy" depending on DB state @pytest.mark.asyncio async def test_db_health_timestamp_format(self, client): """Test DB health timestamp is valid ISO8601 format""" response = await client.get("/api/health/db") data = response.json() # Should not raise exception when parsing timestamp = pendulum.parse(data["timestamp"]) assert timestamp is not None # Should be recent (within last minute) now = pendulum.now('UTC') age = now - timestamp assert age.total_seconds() < 60 @pytest.mark.asyncio async def test_db_health_status_values(self, client): """Test database health status is either healthy or unhealthy""" response = await client.get("/api/health/db") data = response.json() # Status should be one of the expected values assert data["status"] in ["healthy", "unhealthy"] # Database field should be one of the expected values assert data["database"] in ["connected", "disconnected"] # If unhealthy, should have error field if data["status"] == "unhealthy": assert "error" in data class TestHealthEndpointIntegration: """Integration tests for health endpoints""" @pytest.mark.asyncio async def test_both_endpoints_accessible(self, client): """Test both health endpoints are accessible""" basic_response = await client.get("/api/health") db_response = await client.get("/api/health/db") assert basic_response.status_code == 200 assert db_response.status_code == 200 @pytest.mark.asyncio async def test_health_endpoint_performance(self, client): """Test health endpoint responds quickly""" import time start = time.time() response = await client.get("/api/health") duration = time.time() - start assert response.status_code == 200 # Should respond in less than 100ms assert duration < 0.1 @pytest.mark.asyncio async def test_db_health_endpoint_performance(self, client): """Test DB health endpoint responds reasonably quickly""" import time start = time.time() response = await client.get("/api/health/db") duration = time.time() - start assert response.status_code == 200 # Should respond in less than 1 second assert duration < 1.0 @pytest.mark.asyncio async def test_health_endpoints_consistency(self, client): """Test multiple calls return consistent data""" responses = [] for _ in range(3): response = await client.get("/api/health") responses.append(response.json()) # All should have same status and version for data in responses: assert data["status"] == "healthy" assert data["version"] == "1.0.0" assert data["environment"] == responses[0]["environment"]