"""Tests for EPUB search health and readiness routes.""" from __future__ import annotations from typing import TYPE_CHECKING from fastapi.testclient import TestClient from sqlalchemy import create_engine from python.ebook_search.api.main import create_app from python.ebook_search.config import EbookSearchConfig, RerankConfig HEALTH_MODULE = "python.ebook_search.api.routes.health" if TYPE_CHECKING: from pytest_mock import MockerFixture def fake_get_postgres_engine(**_kwargs): """Return an in-memory engine for route tests.""" return create_engine("sqlite+pysqlite:///:memory:", future=True) def patch_app_runtime(mocker: MockerFixture): mocker.patch("python.ebook_search.api.main.get_postgres_engine", side_effect=fake_get_postgres_engine) mocker.patch("python.ebook_search.api.main.ensure_bm25_corpus", side_effect=lambda _session, _config: None) def patch_dependencies(mocker: MockerFixture, *, database=True, embedding=True, chat=True, bm25="ok"): mocker.patch(f"{HEALTH_MODULE}.check_database", side_effect=lambda _session: database) mocker.patch(f"{HEALTH_MODULE}.check_embedding_endpoint", side_effect=lambda _config: embedding) mocker.patch(f"{HEALTH_MODULE}.check_chat_endpoint", side_effect=lambda _config: chat) mocker.patch(f"{HEALTH_MODULE}.check_bm25_status", side_effect=lambda _config: bm25) def build_client(mocker: MockerFixture, config=None): resolved = config or EbookSearchConfig(rerank=RerankConfig(enabled=False)) mocker.patch("python.ebook_search.api.main.load_config", side_effect=lambda: resolved) patch_app_runtime(mocker) app = create_app() return TestClient(app) def test_health_returns_ok(mocker: MockerFixture) -> None: with build_client(mocker) as client: response = client.get("/health") assert response.status_code == 200 assert response.json() == {"status": "ok"} def test_ready_all_dependencies_ok(mocker: MockerFixture) -> None: patch_dependencies(mocker) with build_client(mocker) as client: response = client.get("/ready") assert response.status_code == 200 body = response.json() assert body["status"] == "ready" assert body["checks"] == {"database": "ok", "embedding": "ok", "chat": "ok", "bm25": "ok"} def test_ready_embedding_down_is_degraded(mocker: MockerFixture) -> None: patch_dependencies(mocker, embedding=False) with build_client(mocker) as client: response = client.get("/ready") assert response.status_code == 200 body = response.json() assert body["status"] == "degraded" assert body["checks"]["embedding"] == "fail" def test_ready_chat_down_is_degraded(mocker: MockerFixture) -> None: patch_dependencies(mocker, chat=False) with build_client(mocker) as client: response = client.get("/ready") assert response.status_code == 200 body = response.json() assert body["status"] == "degraded" assert body["checks"]["chat"] == "fail" def test_ready_chat_disabled_when_answers_off(mocker: MockerFixture) -> None: patch_dependencies(mocker) config = EbookSearchConfig(rerank=RerankConfig(enabled=False), answer_enabled=False) with build_client(mocker, config) as client: response = client.get("/ready") assert response.status_code == 200 body = response.json() assert body["status"] == "ready" assert body["checks"]["chat"] == "disabled" def test_ready_database_down_is_unavailable(mocker: MockerFixture) -> None: patch_dependencies(mocker, database=False) with build_client(mocker) as client: response = client.get("/ready") assert response.status_code == 503 body = response.json() assert body["status"] == "unavailable" assert body["checks"]["database"] == "fail" def test_ready_bm25_missing_is_degraded(mocker: MockerFixture) -> None: patch_dependencies(mocker, bm25="missing") with build_client(mocker) as client: response = client.get("/ready") assert response.status_code == 200 body = response.json() assert body["status"] == "degraded" assert body["checks"]["bm25"] == "missing"