Files
RAG/backend/tests/unit/test_health_api.py
YoVinchen cfd6d4cbad
Some checks failed
verify / verify (push) Has been cancelled
Expose a runnable backend without giving the ingress layer secrets
The backend can now be inspected through a loopback-only gateway while the database-aware API remains on the internal data network. A governed synthetic demo proves readiness, pgvector retrieval, reranking, and citation output through real HTTP without invoking cloud models.

Constraint: The previously exposed Bailian key is compromised and cannot be used for live validation

Constraint: The API must be locally reachable while retaining no internet egress

Rejected: Attach the API directly to the ingress network | a real socket test proved that configuration still had egress

Rejected: Publish a port from the internal-only network | Docker Desktop did not expose the host port

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Keep model and database credentials out of the gateway; do not relax the fixed demo identity/profile filters

Tested: make verify; 63 pytest tests; strict mypy; Ruff; Secret scan; Compose config; three backend image builds; API/DB/gateway healthy; migration exit 0; Swagger browser check; live/ready/meta/status/search HTTP; 20/20/20 index; API egress ENETUNREACH; empty gateway mounts and business environment

Not-tested: Live Bailian calls require a newly rotated key; full generated-answer flow and React UI are not implemented
2026-07-12 16:37:02 +08:00

91 lines
3.1 KiB
Python

from unittest.mock import MagicMock, patch
import httpx
import psycopg
import pytest
from sqlalchemy import URL
from app import main
class _SettingsStub:
def database_url(self) -> URL:
return URL.create(
"postgresql+psycopg",
username="app",
password="database-test-password",
host="db.internal",
port=5432,
database="rag",
)
@pytest.mark.asyncio
async def test_liveness_does_not_require_database_or_model_credentials() -> None:
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/health/live")
assert response.status_code == 200
assert response.json()["status"] == "ok"
@pytest.mark.asyncio
async def test_readiness_executes_select_one_and_returns_redacted_status() -> None:
connection = MagicMock()
connection.__enter__.return_value = connection
connection.execute.return_value.fetchone.return_value = (1,)
with (
patch.object(main, "get_settings", return_value=_SettingsStub()),
patch.object(psycopg, "connect", return_value=connection) as connect,
):
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/health/ready")
assert response.status_code == 200
assert response.json() == {"status": "ready", "checks": {"database": "ok"}}
connection.execute.assert_called_once_with("SELECT 1")
assert connect.call_args.kwargs == {"connect_timeout": 2, "autocommit": True}
assert "database-test-password" not in response.text
assert "db.internal" not in response.text
@pytest.mark.asyncio
async def test_readiness_returns_503_without_exposing_database_error() -> None:
private_error = psycopg.OperationalError(
"could not connect to db.internal with database-test-password"
)
with (
patch.object(main, "get_settings", return_value=_SettingsStub()),
patch.object(psycopg, "connect", side_effect=private_error),
):
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/health/ready")
assert response.status_code == 503
assert response.json() == {
"status": "not_ready",
"checks": {"database": "unavailable"},
}
assert "database-test-password" not in response.text
assert "db.internal" not in response.text
@pytest.mark.asyncio
async def test_meta_exposes_model_names_but_no_credentials() -> None:
transport = httpx.ASGITransport(app=main.app)
async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
response = await client.get("/api/v1/meta")
assert response.status_code == 200
payload = response.json()
assert payload["models"] == {
"embedding": "text-embedding-v4",
"rerank": "qwen3-rerank",
"generation": "deepseek-v4-flash",
}
assert "key" not in str(payload).lower()