Files
xiaoxia-saas/tests/unit/test_health_routes.py
T
xiaoxia 4a6d84921d
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 53s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m24s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m42s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 3m16s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 3m19s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 3m29s
AI Code Review / AI Code Review (pull_request) Successful in 2m32s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 4m27s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 5m56s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m45s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m32s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 7m12s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m43s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m24s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 8m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m21s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m27s
CI/CD Pipeline / Integration Tests (push) Successful in 3m10s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m32s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m36s
CI/CD Pipeline / Unit Tests (push) Successful in 14m5s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m32s
CI/CD Pipeline / CI Gate (pull_request) Successful in 8s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 593h19m49s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 593h19m50s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 593h19m51s
CI/CD Pipeline / Production Browser E2E (push) Failing after 593h20m8s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 593h19m51s
CI/CD Pipeline / Deploy Production (push) Failing after 593h20m9s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 593h19m52s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 593h20m9s
CI/CD Pipeline / Build Production API Image (push) Failing after 593h20m10s
CI/CD Pipeline / CI Gate (push) Failing after 593h20m10s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 593h29m6s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 593h29m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 593h29m12s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 593h33m27s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 593h29m14s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 593h31m49s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 593h33m44s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 593h33m48s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 593h31m51s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 593h33m52s
CI/CD Pipeline / PR Build Web Image (push) Failing after 593h33m46s
CI/CD Pipeline / Frontend Lint (push) Failing after 593h34m11s
CI/CD Pipeline / PR Build API Image (push) Failing after 593h33m46s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 593h35m11s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 593h53m36s
CI/CD Pipeline / Canary Release to Production (push) Failing after 593h53m55s
CI/CD Pipeline / Build Production Web Image (push) Failing after 593h53m56s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 594h2m55s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 594h7m38s
fix(cover): add plan.config.cover_candidates fallback in cover generation endpoint (#1458)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-08-22 21:51:01 +08:00

173 lines
6.6 KiB
Python

"""Unit tests for apps/api/app/api/routes/health.py
覆盖 _check_database() 和 _check_migrations() 中 psycopg3 连接逻辑。
"""
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
def _make_cursor(fetchone_result=None):
"""Create a mock cursor with context manager support."""
cur = MagicMock()
cur.__enter__ = MagicMock(return_value=cur)
cur.__exit__ = MagicMock(return_value=False)
if fetchone_result is not None:
cur.fetchone.return_value = fetchone_result
return cur
def _make_conn(cursor_result=None):
conn = MagicMock()
conn.cursor.return_value = cursor_result or _make_cursor()
return conn
@pytest.mark.asyncio
class TestCheckDatabase:
async def test_check_database_success(self):
mock_cur = _make_cursor(fetchone_result=(1,))
mock_conn = _make_conn(mock_cur)
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = False
mock_settings.DATABASE_URL = "postgresql+psycopg://test:test@localhost/test"
from apps.api.app.api.routes import health
with patch.object(health, "psycopg") as mock_psycopg, patch.object(health, "settings", mock_settings):
mock_psycopg.connect.return_value = mock_conn
result = await health._check_database()
assert result["status"] == "healthy"
assert result["type"] == "postgresql"
assert result["message"] == "Database connection successful"
mock_psycopg.connect.assert_called_once_with(
"postgresql+psycopg://test:test@localhost/test", connect_timeout=3
)
mock_cur.execute.assert_called_once_with("SELECT 1")
mock_conn.close.assert_called_once()
async def test_check_database_connection_failure(self):
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = False
mock_settings.DATABASE_URL = "postgresql+psycopg://test:test@localhost/test"
from apps.api.app.api.routes import health
with patch.object(health, "psycopg") as mock_psycopg, patch.object(health, "settings", mock_settings):
mock_psycopg.connect.side_effect = Exception("connection refused")
result = await health._check_database()
assert result["status"] == "unhealthy"
assert result["type"] == "postgresql"
assert "connection refused" in result["message"]
async def test_check_database_in_memory(self):
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = True
from apps.api.app.api.routes import health
with patch.object(health, "settings", mock_settings):
result = await health._check_database()
assert result["status"] == "healthy"
assert result["type"] == "in_memory"
@pytest.mark.asyncio
class TestCheckMigrations:
async def test_check_migrations_success(self):
mock_cur = _make_cursor(fetchone_result=(5,))
mock_conn = _make_conn(mock_cur)
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = False
mock_settings.DATABASE_URL = "postgresql+psycopg://test:test@localhost/test"
from apps.api.app.api.routes import health
with patch.object(health, "psycopg") as mock_psycopg, patch.object(health, "settings", mock_settings):
mock_psycopg.connect.return_value = mock_conn
result = await health._check_migrations()
assert result["status"] == "healthy"
assert result["message"] == "Database migrations applied"
mock_psycopg.connect.assert_called_once_with(
"postgresql+psycopg://test:test@localhost/test", connect_timeout=3
)
mock_conn.close.assert_called_once()
async def test_check_migrations_missing_tables(self):
mock_cur = _make_cursor(fetchone_result=(2,))
mock_conn = _make_conn(mock_cur)
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = False
mock_settings.DATABASE_URL = "postgresql+psycopg://test:test@localhost/test"
from apps.api.app.api.routes import health
with patch.object(health, "psycopg") as mock_psycopg, patch.object(health, "settings", mock_settings):
mock_psycopg.connect.return_value = mock_conn
result = await health._check_migrations()
assert result["status"] == "unhealthy"
assert "Missing tables" in result["message"]
assert "2/5" in result["message"]
async def test_check_migrations_connection_failure(self):
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = False
mock_settings.DATABASE_URL = "postgresql+psycopg://test:test@localhost/test"
from apps.api.app.api.routes import health
with patch.object(health, "psycopg") as mock_psycopg, patch.object(health, "settings", mock_settings):
mock_psycopg.connect.side_effect = Exception("connection refused")
result = await health._check_migrations()
assert result["status"] == "unhealthy"
assert "Migration check failed" in result["message"]
async def test_check_migrations_in_memory(self):
mock_settings = MagicMock()
mock_settings.USE_IN_MEMORY_DB = True
from apps.api.app.api.routes import health
with patch.object(health, "settings", mock_settings):
result = await health._check_migrations()
assert result["status"] == "healthy"
assert "no migrations needed" in result["message"]
@pytest.mark.asyncio
class TestStartupCheck:
async def test_startup_all_healthy(self):
from apps.api.app.api.routes import health
with patch.object(health, "_check_migrations", new_callable=AsyncMock) as mock_mig, patch.object(health, "_check_database", new_callable=AsyncMock) as mock_db:
mock_db.return_value = {"status": "healthy"}
mock_mig.return_value = {"status": "healthy"}
result = await health.startup_check()
assert result["status"] == "started"
async def test_startup_db_unhealthy(self):
import json
from apps.api.app.api.routes import health
with patch.object(health, "_check_migrations", new_callable=AsyncMock) as mock_mig, patch.object(health, "_check_database", new_callable=AsyncMock) as mock_db:
mock_db.return_value = {"status": "unhealthy", "message": "fail"}
mock_mig.return_value = {"status": "healthy"}
result = await health.startup_check()
assert result.status_code == 503
body = json.loads(result.body)
assert body["status"] == "starting"