Files
fast-next-template/backend/tests/api/routes/test_health.py
Felipe Cardoso c589b565f0 Add pyproject.toml for consolidated project configuration and replace Black, isort, and Flake8 with Ruff
- Introduced `pyproject.toml` to centralize backend tool configurations (e.g., Ruff, mypy, coverage, pytest).
- Replaced Black, isort, and Flake8 with Ruff for linting, formatting, and import sorting.
- Updated `requirements.txt` to include Ruff and remove replaced tools.
- Added `Makefile` to streamline development workflows with commands for linting, formatting, type-checking, testing, and cleanup.
2025-11-10 11:55:15 +01:00

190 lines
6.9 KiB
Python
Executable File

# tests/api/routes/test_health.py
from datetime import datetime
from unittest.mock import patch
import pytest
from fastapi import status
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client():
"""Create a FastAPI test client for the main app with mocked database."""
# Mock check_database_health to avoid connecting to the actual database
with patch("app.main.check_database_health") as mock_health_check:
# By default, return True (healthy)
mock_health_check.return_value = True
yield TestClient(app)
class TestHealthEndpoint:
"""Tests for the /health endpoint"""
def test_health_check_healthy(self, client):
"""Test that health check returns healthy when database is accessible"""
response = client.get("/health")
assert response.status_code == status.HTTP_200_OK
data = response.json()
# Check required fields
assert "status" in data
assert data["status"] == "healthy"
assert "timestamp" in data
assert "version" in data
assert "environment" in data
assert "checks" in data
# Verify timestamp format (ISO 8601)
assert data["timestamp"].endswith("Z")
# Verify it's a valid datetime
datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00"))
# Check database health
assert "database" in data["checks"]
assert data["checks"]["database"]["status"] == "healthy"
assert "message" in data["checks"]["database"]
def test_health_check_response_structure(self, client):
"""Test that health check response has correct structure"""
response = client.get("/health")
data = response.json()
# Verify top-level structure
assert isinstance(data["status"], str)
assert isinstance(data["timestamp"], str)
assert isinstance(data["version"], str)
assert isinstance(data["environment"], str)
assert isinstance(data["checks"], dict)
# Verify database check structure
db_check = data["checks"]["database"]
assert isinstance(db_check["status"], str)
assert isinstance(db_check["message"], str)
def test_health_check_version_matches_settings(self, client):
"""Test that health check returns correct version from settings"""
from app.core.config import settings
response = client.get("/health")
data = response.json()
assert data["version"] == settings.VERSION
def test_health_check_environment_matches_settings(self, client):
"""Test that health check returns correct environment from settings"""
from app.core.config import settings
response = client.get("/health")
data = response.json()
assert data["environment"] == settings.ENVIRONMENT
def test_health_check_database_connection_failure(self):
"""Test that health check returns unhealthy when database is not accessible"""
# Mock check_database_health to return False (unhealthy)
with patch("app.main.check_database_health") as mock_health_check:
mock_health_check.return_value = False
test_client = TestClient(app)
response = test_client.get("/health")
assert response.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
data = response.json()
# Check overall status
assert data["status"] == "unhealthy"
# Check database status
assert "database" in data["checks"]
assert data["checks"]["database"]["status"] == "unhealthy"
assert "failed" in data["checks"]["database"]["message"].lower()
def test_health_check_timestamp_recent(self, client):
"""Test that health check timestamp is recent (within last minute)"""
before = datetime.utcnow()
response = client.get("/health")
after = datetime.utcnow()
data = response.json()
timestamp = datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00"))
# Timestamp should be between before and after
assert before <= timestamp.replace(tzinfo=None) <= after
def test_health_check_no_authentication_required(self, client):
"""Test that health check does not require authentication"""
# Make request without any authentication headers
response = client.get("/health")
# Should succeed without authentication
assert response.status_code in [
status.HTTP_200_OK,
status.HTTP_503_SERVICE_UNAVAILABLE,
]
def test_health_check_idempotent(self, client):
"""Test that multiple health checks return consistent results"""
response1 = client.get("/health")
response2 = client.get("/health")
# Both should have same status code (either both healthy or both unhealthy)
assert response1.status_code == response2.status_code
data1 = response1.json()
data2 = response2.json()
# Same overall health status
assert data1["status"] == data2["status"]
# Same version and environment
assert data1["version"] == data2["version"]
assert data1["environment"] == data2["environment"]
# Same database check status
assert (
data1["checks"]["database"]["status"]
== data2["checks"]["database"]["status"]
)
def test_health_check_content_type(self, client):
"""Test that health check returns JSON content type"""
response = client.get("/health")
assert "application/json" in response.headers["content-type"]
class TestHealthEndpointEdgeCases:
"""Edge case tests for the /health endpoint"""
def test_health_check_with_query_parameters(self, client):
"""Test that health check ignores query parameters"""
response = client.get("/health?foo=bar&baz=qux")
# Should still work with query params
assert response.status_code == status.HTTP_200_OK
def test_health_check_method_not_allowed(self, client):
"""Test that POST/PUT/DELETE are not allowed on health endpoint"""
# POST should not be allowed
response = client.post("/health")
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
# PUT should not be allowed
response = client.put("/health")
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
# DELETE should not be allowed
response = client.delete("/health")
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
def test_health_check_with_accept_header(self, client):
"""Test that health check works with different Accept headers"""
response = client.get("/health", headers={"Accept": "application/json"})
assert response.status_code == status.HTTP_200_OK
response = client.get("/health", headers={"Accept": "*/*"})
assert response.status_code == status.HTTP_200_OK