"""健康检查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