Skip to content

Commit d5d21fd

Browse files
cheddarfoxclaude
andcommitted
test(health): add readiness probe and schema integration tests [REN-83]
Add tests/integration/test_health.py with 10 integration tests: - Liveness probes: schema validation for /health and /api/v1/health - Version endpoint: schema validation for /version - Readiness probe: basic 200 check, database/redis key presence, degraded state without Redis, ready state with mocked Redis - Content-Type validation for JSON responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 09cc6ae commit d5d21fd

1 file changed

Lines changed: 168 additions & 0 deletions

File tree

‎tests/integration/test_health.py‎

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# Copyright 2025 ByBren, LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Integration tests for health check endpoints.
16+
17+
Covers:
18+
- Liveness probe schema validation (/health, /api/v1/health)
19+
- Version endpoint schema validation (/version)
20+
- Readiness probe (/api/v1/health/ready):
21+
- Database connectivity check
22+
- Redis connectivity check
23+
- Degraded state when Redis is unavailable
24+
- Ready state when all services are available (mocked Redis)
25+
"""
26+
27+
from __future__ import annotations
28+
29+
from typing import TYPE_CHECKING
30+
from unittest.mock import AsyncMock, patch
31+
32+
import pytest
33+
34+
if TYPE_CHECKING:
35+
from httpx import AsyncClient
36+
37+
38+
# ═══════════════════════════════════════════════════════════════════════════
39+
# Liveness probes — schema validation
40+
# ═══════════════════════════════════════════════════════════════════════════
41+
class TestLivenessProbes:
42+
"""Validate liveness probe response schemas."""
43+
44+
@pytest.mark.integration
45+
async def test_root_health_schema(self, client: AsyncClient) -> None:
46+
"""GET /health response contains all expected keys."""
47+
response = await client.get("/health")
48+
assert response.status_code == 200
49+
data = response.json()
50+
assert "status" in data
51+
assert "version" in data
52+
assert data["status"] == "healthy"
53+
assert data["version"] == "0.1.0"
54+
55+
@pytest.mark.integration
56+
async def test_api_v1_health_includes_environment(self, client: AsyncClient) -> None:
57+
"""GET /api/v1/health returns environment field."""
58+
response = await client.get("/api/v1/health")
59+
assert response.status_code == 200
60+
data = response.json()
61+
assert "environment" in data
62+
assert "status" in data
63+
assert "version" in data
64+
assert data["status"] == "healthy"
65+
assert data["version"] == "0.1.0"
66+
67+
@pytest.mark.integration
68+
async def test_health_response_is_json(self, client: AsyncClient) -> None:
69+
"""GET /health returns application/json content type."""
70+
response = await client.get("/health")
71+
assert response.status_code == 200
72+
content_type = response.headers.get("content-type", "")
73+
assert "application/json" in content_type
74+
75+
76+
# ═══════════════════════════════════════════════════════════════════════════
77+
# Version endpoint — schema validation
78+
# ═══════════════════════════════════════════════════════════════════════════
79+
class TestVersionEndpoint:
80+
"""Validate version endpoint response schema."""
81+
82+
@pytest.mark.integration
83+
async def test_version_response_schema(self, client: AsyncClient) -> None:
84+
"""GET /version has name, version, and environment keys."""
85+
response = await client.get("/version")
86+
assert response.status_code == 200
87+
data = response.json()
88+
assert "name" in data
89+
assert "version" in data
90+
assert "environment" in data
91+
assert data["name"] == "rendertrust"
92+
assert data["version"] == "0.1.0"
93+
94+
95+
# ═══════════════════════════════════════════════════════════════════════════
96+
# Readiness probe — /api/v1/health/ready
97+
# ═══════════════════════════════════════════════════════════════════════════
98+
class TestReadinessProbe:
99+
"""Validate the readiness probe endpoint behaviour."""
100+
101+
@pytest.mark.integration
102+
async def test_readiness_returns_200(self, client: AsyncClient) -> None:
103+
"""GET /api/v1/health/ready returns 200 regardless of service state."""
104+
response = await client.get("/api/v1/health/ready")
105+
assert response.status_code == 200
106+
107+
@pytest.mark.integration
108+
async def test_readiness_checks_database(self, client: AsyncClient) -> None:
109+
"""Readiness response includes a database check."""
110+
response = await client.get("/api/v1/health/ready")
111+
assert response.status_code == 200
112+
data = response.json()
113+
assert "checks" in data
114+
assert "database" in data["checks"]
115+
116+
@pytest.mark.integration
117+
async def test_readiness_checks_redis(self, client: AsyncClient) -> None:
118+
"""Readiness response includes a Redis check."""
119+
response = await client.get("/api/v1/health/ready")
120+
assert response.status_code == 200
121+
data = response.json()
122+
assert "checks" in data
123+
assert "redis" in data["checks"]
124+
125+
@pytest.mark.integration
126+
async def test_readiness_status_is_ready_or_degraded(self, client: AsyncClient) -> None:
127+
"""Status field is one of the expected values."""
128+
response = await client.get("/api/v1/health/ready")
129+
assert response.status_code == 200
130+
data = response.json()
131+
assert data["status"] in {"ready", "degraded"}
132+
133+
@pytest.mark.integration
134+
async def test_readiness_degraded_when_redis_unavailable(self, client: AsyncClient) -> None:
135+
"""Without a running Redis instance, readiness reports degraded.
136+
137+
The test database (SQLite in-memory) responds to ``SELECT 1``,
138+
so the database check passes. Redis is not running in the test
139+
environment, so the endpoint should report degraded status with
140+
``redis = "unavailable"``.
141+
"""
142+
response = await client.get("/api/v1/health/ready")
143+
assert response.status_code == 200
144+
data = response.json()
145+
assert data["status"] == "degraded"
146+
assert data["checks"]["database"] == "connected"
147+
assert data["checks"]["redis"] == "unavailable"
148+
149+
@pytest.mark.integration
150+
async def test_readiness_ready_with_all_services(self, client: AsyncClient) -> None:
151+
"""With all services up, readiness should report ready.
152+
153+
Mocks ``redis.asyncio.from_url`` inside the health module so that
154+
the Redis ping succeeds, while the real database check still runs
155+
against the test SQLite engine.
156+
"""
157+
mock_redis = AsyncMock()
158+
mock_redis.ping.return_value = True
159+
mock_redis.aclose.return_value = None
160+
161+
with patch("core.api.v1.health.aioredis.from_url", return_value=mock_redis):
162+
response = await client.get("/api/v1/health/ready")
163+
164+
assert response.status_code == 200
165+
data = response.json()
166+
assert data["status"] == "ready"
167+
assert data["checks"]["database"] == "connected"
168+
assert data["checks"]["redis"] == "connected"

0 commit comments

Comments
 (0)