Some checks failed
verify / verify (push) Has been cancelled
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
91 lines
3.1 KiB
Python
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()
|