205 lines
6.7 KiB
Python
205 lines
6.7 KiB
Python
"""健康检查API测试"""
|
||
import pytest
|
||
import pytest_asyncio
|
||
from httpx import AsyncClient, ASGITransport
|
||
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession, create_async_engine
|
||
from sqlalchemy.pool import StaticPool
|
||
|
||
from app.database import Base
|
||
from app.main import app
|
||
from app.api.deps import get_db
|
||
|
||
|
||
# ==================== Fixtures ====================
|
||
|
||
@pytest_asyncio.fixture
|
||
async def async_engine():
|
||
"""创建测试用SQLite异步引擎"""
|
||
engine = create_async_engine(
|
||
"sqlite+aiosqlite:///:memory:",
|
||
connect_args={"check_same_thread": False},
|
||
poolclass=StaticPool,
|
||
)
|
||
async with engine.begin() as conn:
|
||
await conn.run_sync(Base.metadata.create_all)
|
||
yield engine
|
||
await engine.dispose()
|
||
|
||
|
||
@pytest_asyncio.fixture
|
||
async def async_session(async_engine):
|
||
"""创建测试用异步数据库会话"""
|
||
async_session_maker = async_sessionmaker(
|
||
async_engine,
|
||
class_=AsyncSession,
|
||
expire_on_commit=False,
|
||
autoflush=False,
|
||
autocommit=False,
|
||
)
|
||
async with async_session_maker() as session:
|
||
yield session
|
||
|
||
|
||
@pytest_asyncio.fixture
|
||
async def async_client(async_session):
|
||
"""创建异步HTTP客户端用于API测试"""
|
||
|
||
async def override_get_db():
|
||
yield async_session
|
||
|
||
app.dependency_overrides[get_db] = override_get_db
|
||
|
||
transport = ASGITransport(app=app)
|
||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||
yield client
|
||
|
||
app.dependency_overrides.clear()
|
||
|
||
|
||
# ==================== 测试类 ====================
|
||
|
||
class TestHealthAPI:
|
||
"""健康检查API测试"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_basic_health(self):
|
||
"""测试基本健康检查"""
|
||
transport = ASGITransport(app=app)
|
||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||
response = await client.get("/health")
|
||
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["status"] == "healthy"
|
||
assert "timestamp" in data
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_liveness(self):
|
||
"""测试存活探针"""
|
||
transport = ASGITransport(app=app)
|
||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||
response = await client.get("/health/liveness")
|
||
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
assert data["status"] == "alive"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ready_endpoint(self):
|
||
"""测试就绪端点"""
|
||
transport = ASGITransport(app=app)
|
||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||
response = await client.get("/ready")
|
||
|
||
# 返回200或503取决于依赖服务状态
|
||
assert response.status_code in [200, 503]
|
||
data = response.json()
|
||
assert "status" in data
|
||
assert "checks" in data
|
||
assert "database" in data["checks"]
|
||
assert "redis" in data["checks"]
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_metrics(self):
|
||
"""测试Prometheus指标端点"""
|
||
transport = ASGITransport(app=app)
|
||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||
response = await client.get("/metrics")
|
||
|
||
assert response.status_code == 200
|
||
# Prometheus指标返回文本格式
|
||
assert "text/plain" in response.headers["content-type"] or \
|
||
"text/plain" in str(response.headers.get("content-type", ""))
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_detailed_health(self, async_client):
|
||
"""测试详细健康检查"""
|
||
response = await async_client.get("/health/detailed")
|
||
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
# 检查返回结构
|
||
assert "checks" in data
|
||
assert "app" in data
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_readiness_probe(self, async_client):
|
||
"""测试就绪探针"""
|
||
response = await async_client.get("/health/readiness")
|
||
|
||
# 健康状态应该是200,unhealthy是503
|
||
assert response.status_code in [200, 503]
|
||
data = response.json()
|
||
assert "status" in data
|
||
assert "checks" in data
|
||
|
||
|
||
class TestHealthEndpointsStructure:
|
||
"""健康检查端点结构测试"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_health_endpoint_returns_json(self):
|
||
"""测试健康端点返回JSON格式"""
|
||
transport = ASGITransport(app=app)
|
||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||
response = await client.get("/health")
|
||
|
||
assert response.status_code == 200
|
||
assert "application/json" in response.headers.get("content-type", "")
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_detailed_health_has_required_fields(self, async_client):
|
||
"""测试详细健康检查包含必需字段"""
|
||
response = await async_client.get("/health/detailed")
|
||
|
||
assert response.status_code == 200
|
||
data = response.json()
|
||
|
||
# 检查app信息
|
||
if "app" in data:
|
||
app_info = data["app"]
|
||
assert isinstance(app_info, dict)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_ready_checks_database_and_redis(self, async_client):
|
||
"""测试就绪检查包含数据库和Redis检查"""
|
||
response = await async_client.get("/ready")
|
||
|
||
data = response.json()
|
||
checks = data.get("checks", {})
|
||
|
||
assert "database" in checks
|
||
assert "redis" in checks
|
||
|
||
|
||
class TestHealthCheckIndependence:
|
||
"""健康检查独立性测试"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_health_no_auth_required(self):
|
||
"""测试健康检查不需要认证"""
|
||
transport = ASGITransport(app=app)
|
||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||
# 不带token访问
|
||
response = await client.get("/health")
|
||
|
||
assert response.status_code == 200
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_liveness_no_auth_required(self):
|
||
"""测试存活探针不需要认证"""
|
||
transport = ASGITransport(app=app)
|
||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||
response = await client.get("/health/liveness")
|
||
|
||
assert response.status_code == 200
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_metrics_no_auth_required(self):
|
||
"""测试指标端点不需要认证"""
|
||
transport = ASGITransport(app=app)
|
||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||
response = await client.get("/metrics")
|
||
|
||
assert response.status_code == 200
|