- Authoritative candidate capture with server-issued artifacts and raw-byte
immutability hashing (source_response_hash server-owned)
- Closed-period lifecycle: request-hash bound approvals, persisted closure
immutability violations, byte-for-byte catalog stability on reclosure
- Verification runs: persisted VerificationRun model + FK migration,
publish gate (block_publish), scheduled observability runs (02:00 UTC)
- FR-013 baseline inheritance: prior_release_id migration, plan_inheritance/
execute_inheritance classification and re-extraction, API endpoints
- Visual executor bound to release-deployment environment; caller mismatch
rejected; visual SSIM/reconciliation modules
- Query execution decomposed: envelope/model/executor split, no direct SQL
- AgentRun approvals extracted to submodule; evidence adapter; _utils
- Dashboard testing service decomposed into 30+ modules (all <400 LOC)
- Five Feature-037 agent tools with permission guards (tools_037.py)
- API readiness endpoint; Alembic env/migrations; test fixture repos
- Specs 036/037 contracts, openapi.yaml, schema.json, tasks/traceability
updated; semantic index rebuilt with 0 parse warnings
- Fix ADR-0003 parser ambiguity: remove [DEF🆔ADR] prose example
- Add axiom-mcp-agent-feedback.md: agent findings for MCP rework plan
- Tests: 298 service + 1464 API + 45 agent passing; ruff clean
99 lines
3.4 KiB
Python
99 lines
3.4 KiB
Python
# #region Test.Api.Ready [C:2] [TYPE Module] [SEMANTICS test,readiness,api,healthcheck]
|
|
# @BRIEF Unit tests for the unauthenticated /api/ready readiness probe.
|
|
# @RELATION BINDS_TO -> [Api.Ready.ReadyRouter]
|
|
# @TEST_EDGE: db_success -> 200 + {"status": "ready"}
|
|
# @TEST_EDGE: db_failure_sqlalchemy -> 503 + {"status": "not_ready"}
|
|
# @TEST_EDGE: db_failure_generic -> 503 + {"status": "not_ready"}
|
|
|
|
import os
|
|
|
|
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
|
|
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
|
|
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")
|
|
|
|
from pathlib import Path
|
|
import pytest
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from sqlalchemy.exc import SQLAlchemyError
|
|
|
|
_src = str(Path(__file__).resolve().parent.parent.parent / "src")
|
|
if _src not in sys.path:
|
|
sys.path.insert(0, _src)
|
|
|
|
|
|
@pytest.fixture(name="client")
|
|
def fixture_client():
|
|
"""Build a TestClient with the ready router mounted (no auth dependencies)."""
|
|
from src.api.routes.ready import router
|
|
|
|
app = FastAPI()
|
|
app.include_router(router)
|
|
return TestClient(app)
|
|
|
|
|
|
class TestGetReady:
|
|
"""GET /api/ready"""
|
|
|
|
def test_ready_success(self, client):
|
|
"""Happy path: DB responds to SELECT 1 -> 200."""
|
|
mock_session = MagicMock()
|
|
mock_session.execute.return_value = True
|
|
|
|
with patch("src.api.routes.ready.SessionLocal", return_value=mock_session):
|
|
resp = client.get("/api/ready")
|
|
|
|
assert resp.status_code == 200
|
|
assert resp.json() == {"status": "ready"}
|
|
mock_session.execute.assert_called_once()
|
|
|
|
def test_ready_sqlalchemy_error(self, client):
|
|
"""SQLAlchemyError during execute -> 503 + not_ready."""
|
|
mock_session = MagicMock()
|
|
mock_session.execute.side_effect = SQLAlchemyError("connection refused")
|
|
|
|
with patch("src.api.routes.ready.SessionLocal", return_value=mock_session):
|
|
resp = client.get("/api/ready")
|
|
|
|
assert resp.status_code == 503
|
|
body = resp.json()
|
|
assert body["status"] == "not_ready"
|
|
assert body["detail"] == "Database unavailable"
|
|
|
|
def test_ready_generic_error(self, client):
|
|
"""Non-SQLAlchemy exception during execute -> 503 + not_ready."""
|
|
mock_session = MagicMock()
|
|
mock_session.execute.side_effect = RuntimeError("something exploded")
|
|
|
|
with patch("src.api.routes.ready.SessionLocal", return_value=mock_session):
|
|
resp = client.get("/api/ready")
|
|
|
|
assert resp.status_code == 503
|
|
body = resp.json()
|
|
assert body["status"] == "not_ready"
|
|
assert body["detail"] == "Backend not ready"
|
|
|
|
def test_ready_no_secrets_in_response(self, client):
|
|
"""Exception detail must not leak stack traces or connection strings."""
|
|
mock_session = MagicMock()
|
|
mock_session.execute.side_effect = SQLAlchemyError(
|
|
"FATAL: password authentication failed for user 'admin'"
|
|
)
|
|
|
|
with patch("src.api.routes.ready.SessionLocal", return_value=mock_session):
|
|
resp = client.get("/api/ready")
|
|
|
|
assert resp.status_code == 503
|
|
body = resp.json()
|
|
assert body["status"] == "not_ready"
|
|
detail_str = str(body.get("detail", ""))
|
|
assert "password" not in detail_str.lower()
|
|
assert "admin" not in detail_str
|
|
assert "FATAL" not in detail_str
|
|
|
|
|
|
# #endregion Test.Api.Ready
|