feat: add health endpoints, static serving, and base template (Step 7.2)
- Add /healthz endpoint with DB writable check - Add /metrics endpoint with Prometheus-compatible format (enabled by default) - Configure static file serving at /static/v1/... with immutable cache headers - Create base page template with MonsterUI slate theme - Create industrial farm aesthetic bottom navigation with custom SVG icons - Add StaticCacheMiddleware for adding cache-control headers Changes: - src/animaltrack/web/routes/health.py: Health and metrics endpoints - src/animaltrack/web/templates/: Base template, nav, and icons - src/animaltrack/web/app.py: Integrate theme, routes, static serving - src/animaltrack/config.py: metrics_enabled defaults to True 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
129
tests/test_web_health.py
Normal file
129
tests/test_web_health.py
Normal file
@@ -0,0 +1,129 @@
|
||||
# ABOUTME: Tests for health and metrics endpoints.
|
||||
# ABOUTME: Covers /healthz DB check and /metrics Prometheus format.
|
||||
|
||||
import os
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def make_test_settings(
|
||||
csrf_secret: str = "test-secret",
|
||||
trusted_proxy_ips: str = "127.0.0.1",
|
||||
metrics_enabled: bool = True,
|
||||
):
|
||||
"""Create Settings for testing by setting env vars temporarily."""
|
||||
from animaltrack.config import Settings
|
||||
|
||||
old_env = os.environ.copy()
|
||||
try:
|
||||
os.environ["CSRF_SECRET"] = csrf_secret
|
||||
os.environ["TRUSTED_PROXY_IPS"] = trusted_proxy_ips
|
||||
os.environ["METRICS_ENABLED"] = str(metrics_enabled).lower()
|
||||
return Settings()
|
||||
finally:
|
||||
os.environ.clear()
|
||||
os.environ.update(old_env)
|
||||
|
||||
|
||||
class TestHealthzEndpoint:
|
||||
"""Tests for /healthz endpoint."""
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, seeded_db):
|
||||
"""Create a test client for the app."""
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from animaltrack.web.app import create_app
|
||||
|
||||
settings = make_test_settings(trusted_proxy_ips="testclient")
|
||||
app, rt = create_app(settings=settings, db=seeded_db)
|
||||
return TestClient(app, raise_server_exceptions=False)
|
||||
|
||||
def test_healthz_returns_200_when_db_healthy(self, client):
|
||||
"""GET /healthz returns 200 OK when database is healthy."""
|
||||
resp = client.get("/healthz")
|
||||
assert resp.status_code == 200
|
||||
assert resp.text == "OK"
|
||||
|
||||
def test_healthz_returns_503_when_db_fails(self, seeded_db):
|
||||
"""GET /healthz returns 503 when database query fails."""
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from animaltrack.web.app import create_app
|
||||
|
||||
# Create a mock db that raises on execute
|
||||
mock_db = MagicMock()
|
||||
mock_db.execute.side_effect = Exception("Database connection lost")
|
||||
|
||||
settings = make_test_settings(trusted_proxy_ips="testclient")
|
||||
app, rt = create_app(settings=settings, db=mock_db)
|
||||
client = TestClient(app, raise_server_exceptions=False)
|
||||
|
||||
resp = client.get("/healthz")
|
||||
assert resp.status_code == 503
|
||||
assert "Database error" in resp.text
|
||||
|
||||
|
||||
class TestMetricsEndpoint:
|
||||
"""Tests for /metrics Prometheus endpoint."""
|
||||
|
||||
@pytest.fixture
|
||||
def client_metrics_enabled(self, seeded_db):
|
||||
"""Create a test client with metrics enabled."""
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from animaltrack.web.app import create_app
|
||||
|
||||
settings = make_test_settings(
|
||||
trusted_proxy_ips="testclient",
|
||||
metrics_enabled=True,
|
||||
)
|
||||
app, rt = create_app(settings=settings, db=seeded_db)
|
||||
return TestClient(app, raise_server_exceptions=False)
|
||||
|
||||
@pytest.fixture
|
||||
def client_metrics_disabled(self, seeded_db):
|
||||
"""Create a test client with metrics disabled."""
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from animaltrack.web.app import create_app
|
||||
|
||||
settings = make_test_settings(
|
||||
trusted_proxy_ips="testclient",
|
||||
metrics_enabled=False,
|
||||
)
|
||||
app, rt = create_app(settings=settings, db=seeded_db)
|
||||
return TestClient(app, raise_server_exceptions=False)
|
||||
|
||||
def test_metrics_returns_200_when_enabled(self, client_metrics_enabled):
|
||||
"""GET /metrics returns 200 when metrics_enabled=True."""
|
||||
resp = client_metrics_enabled.get("/metrics")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_metrics_returns_404_when_disabled(self, client_metrics_disabled):
|
||||
"""GET /metrics returns 404 when metrics_enabled=False."""
|
||||
resp = client_metrics_disabled.get("/metrics")
|
||||
assert resp.status_code == 404
|
||||
|
||||
def test_metrics_returns_prometheus_format(self, client_metrics_enabled):
|
||||
"""GET /metrics returns valid Prometheus text format."""
|
||||
resp = client_metrics_enabled.get("/metrics")
|
||||
|
||||
# Check Content-Type
|
||||
content_type = resp.headers.get("content-type", "")
|
||||
assert "text/plain" in content_type
|
||||
|
||||
# Check body contains Prometheus format elements
|
||||
body = resp.text
|
||||
assert "# HELP" in body
|
||||
assert "# TYPE" in body
|
||||
assert "animaltrack_up" in body
|
||||
|
||||
def test_metrics_includes_db_health_metric(self, client_metrics_enabled):
|
||||
"""GET /metrics includes database health gauge."""
|
||||
resp = client_metrics_enabled.get("/metrics")
|
||||
body = resp.text
|
||||
|
||||
assert "animaltrack_db_healthy" in body
|
||||
assert "# TYPE animaltrack_db_healthy gauge" in body
|
||||
Reference in New Issue
Block a user