From 49ef7997a2cb194f815cb79c3457f841515a4ae0 Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Mon, 29 Dec 2025 14:11:59 -0600 Subject: [PATCH] Add tests for health endpoint --- .vscode/settings.json | 7 +++++ .vscode/tasks.json | 2 +- app/config.py | 7 +++-- app/middleware.py | 4 +-- app/resources/health.py | 8 +++++- pyproject.toml | 5 +++- tests/test_health.py | 61 +++++++++++++++++++++++++++++++++++++++++ uv.lock | 4 +++ 8 files changed, 90 insertions(+), 8 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 tests/test_health.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a0f6732..4f5211d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,7 +6,7 @@ { "label": "Run Mypy", "type": "shell", - "command": "uv run mypy .", + "command": "uv run mypy . --no-incremental", "problemMatcher": [ "$python" ] diff --git a/app/config.py b/app/config.py index c370680..d6843c7 100644 --- a/app/config.py +++ b/app/config.py @@ -25,9 +25,10 @@ class Settings(BaseSettings): dd_env: str = Field(default="development", alias="DD_ENV") dd_version: str = Field(default="1.0.0", alias="DD_VERSION") - class Config: - env_file = ".env" - case_sensitive = False + model_config = { + "env_file": ".env", + "case_sensitive": False + } settings = Settings() # type:ignore[call-arg] diff --git a/app/middleware.py b/app/middleware.py index 0ab17e2..e208e9d 100644 --- a/app/middleware.py +++ b/app/middleware.py @@ -1,4 +1,4 @@ -from typing import Callable +from typing import Callable, Any from fastapi import Request, Response from slowapi import Limiter, _rate_limit_exceeded_handler @@ -15,7 +15,7 @@ logger = get_logger(__name__) limiter = Limiter(key_func=get_remote_address) -async def logging_middleware(request: Request, call_next: Callable) -> Response: +async def logging_middleware(request: Request, call_next: Callable[[Request], Any]) -> Any: """Log all requests and responses.""" logger.debug( "Request received", diff --git a/app/resources/health.py b/app/resources/health.py index 1fdf6c8..a37c442 100644 --- a/app/resources/health.py +++ b/app/resources/health.py @@ -2,8 +2,10 @@ from fastapi import APIRouter from app.database import DatabaseService +from app.logging import get_logger router = APIRouter() +logger = get_logger(__name__) @router.get("/health-check") @@ -11,7 +13,11 @@ async def health_check() -> dict[str, str]: """Comprehensive health check endpoint.""" # Check database connectivity - db_healthy = DatabaseService.health_check() + try: + db_healthy = DatabaseService.health_check() + except Exception as e: + logger.error("Database health check failed", exc_info=e) + db_healthy = False status = "healthy" if db_healthy else "unhealthy" diff --git a/pyproject.toml b/pyproject.toml index 330638d..587bddd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ strict = true warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true +exclude = ["tests"] [tool.pylint.messages_control] max-line-length = 88 @@ -48,15 +49,17 @@ multi_line_output = 3 line_length = 88 [tool.pytest.ini_options] -asyncio_mode = "auto" +pythonpath = ["."] testpaths = ["tests"] [dependency-groups] dev = [ "autoflake>=2.3.1", "autopep8>=2.3.2", + "httpx>=0.28.1", "isort>=7.0.0", "mypy>=1.19.1", "pylint-pydantic>=0.4.1", "pylint[pydantic]>=4.0.4", + "pytest[coverage]>=9.0.2", ] diff --git a/tests/test_health.py b/tests/test_health.py new file mode 100644 index 0000000..fbe7172 --- /dev/null +++ b/tests/test_health.py @@ -0,0 +1,61 @@ +"""Tests for health check endpoints.""" +import pytest +from fastapi.testclient import TestClient +from unittest.mock import patch + +from main import create_app + + +class TestHealthCheck: + """Test cases for health check endpoint.""" + + @pytest.fixture + def client(self) -> TestClient: + """Create test client.""" + app = create_app() + return TestClient(app) + + @patch('app.database.DatabaseService.health_check') + def test_health_check_database_connected(self, mock_db_health, client) -> None: + """Test health check when database is connected.""" + # Mock database as healthy + mock_db_health.return_value = True + + response = client.get("/health-check") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + assert data["service"] == "loapi" + assert data["database"] == "connected" + mock_db_health.assert_called_once() + + @patch('app.database.DatabaseService.health_check') + def test_health_check_database_disconnected(self, mock_db_health, client): + """Test health check when database is disconnected.""" + # Mock database as unhealthy + mock_db_health.return_value = False + + response = client.get("/health-check") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "unhealthy" + assert data["service"] == "loapi" + assert data["database"] == "disconnected" + mock_db_health.assert_called_once() + + @patch('app.database.DatabaseService.health_check') + def test_health_check_database_exception(self, mock_db_health, client): + """Test health check when database check raises exception.""" + # Mock database health check to raise exception + mock_db_health.side_effect = Exception("Database connection failed") + + response = client.get("/health-check") + + assert response.status_code == 200 + data = response.json() + assert data["status"] == "unhealthy" + assert data["service"] == "loapi" + assert data["database"] == "disconnected" + mock_db_health.assert_called_once() diff --git a/uv.lock b/uv.lock index 2b33986..52f8ede 100644 --- a/uv.lock +++ b/uv.lock @@ -715,10 +715,12 @@ dev = [ dev = [ { name = "autoflake" }, { name = "autopep8" }, + { name = "httpx" }, { name = "isort" }, { name = "mypy" }, { name = "pylint" }, { name = "pylint-pydantic" }, + { name = "pytest" }, ] [package.metadata] @@ -750,10 +752,12 @@ provides-extras = ["dev"] dev = [ { name = "autoflake", specifier = ">=2.3.1" }, { name = "autopep8", specifier = ">=2.3.2" }, + { name = "httpx", specifier = ">=0.28.1" }, { name = "isort", specifier = ">=7.0.0" }, { name = "mypy", specifier = ">=1.19.1" }, { name = "pylint", extras = ["pydantic"], specifier = ">=4.0.4" }, { name = "pylint-pydantic", specifier = ">=0.4.1" }, + { name = "pytest", extras = ["coverage"], specifier = ">=9.0.2" }, ] [[package]]