"""Integration tests for shared-secret auth between the Next proxy and the backend. When settings.proxy_shared_secret is set, every endpoint except shallow /health requires the X-Proxy-Secret header. When unset, auth is disabled (local dev). """ from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient from app.rag.retrieval import Chunk from tests.conftest import FakeEmbedder, FakeLLMProvider, _fake_settings, _reset_limiter TEST_SECRET = "test-secret" def _make_chunk() -> Chunk: return Chunk( id="chunk-1", content="Rules content.", section="Rules", parent_section=None, source_type="rulebook", similarity=0.9, ) def _auth_client(proxy_shared_secret: str | None): from app.api.v1.query import get_db_pool, get_embedder, get_llm_provider from app.main import app _reset_limiter() app.dependency_overrides[get_embedder] = lambda: FakeEmbedder() app.dependency_overrides[get_db_pool] = lambda: MagicMock() app.dependency_overrides[get_llm_provider] = lambda: FakeLLMProvider() settings = _fake_settings() settings.proxy_shared_secret = proxy_shared_secret with ( patch("app.main.init_pool", return_value=MagicMock()), patch("app.main.close_pool"), patch("app.main.Embedder.load", return_value=FakeEmbedder()), patch("app.main.genai.Client", return_value=MagicMock()), patch("app.main.get_settings", return_value=settings), ): with TestClient(app) as c: yield c app.dependency_overrides.clear() @pytest.fixture def secured_client(): yield from _auth_client(TEST_SECRET) @pytest.fixture def open_client(): yield from _auth_client(None) def _post_query(client: TestClient, headers: dict | None = None): with ( patch("app.rag.pipeline.hybrid_search", return_value=[_make_chunk()]), patch("app.rag.pipeline.get_cached", return_value=None), patch("app.rag.pipeline.set_cached"), ): return client.post( "/api/v1/query", json={"question": "Can a unit attack twice?"}, headers=headers or {}, ) def test_query_without_secret_returns_401(secured_client: TestClient): resp = _post_query(secured_client) assert resp.status_code == 401 def test_query_with_wrong_secret_returns_401(secured_client: TestClient): resp = _post_query(secured_client, headers={"X-Proxy-Secret": "wrong"}) assert resp.status_code == 401 def test_query_with_valid_secret_succeeds(secured_client: TestClient): resp = _post_query(secured_client, headers={"X-Proxy-Secret": TEST_SECRET}) assert resp.status_code == 200 def test_401_detail_is_generic(secured_client: TestClient): """The 401 must not reveal that a proxy secret mechanism exists.""" resp = _post_query(secured_client) assert resp.json() == {"detail": "Unauthorized"} def test_shallow_health_is_exempt(secured_client: TestClient): resp = secured_client.get("/health") assert resp.status_code == 200 def test_deep_health_requires_secret(secured_client: TestClient): resp = secured_client.get("/health/deep") assert resp.status_code == 401 def test_deep_health_with_secret_succeeds(secured_client: TestClient): resp = secured_client.get("/health/deep", headers={"X-Proxy-Secret": TEST_SECRET}) assert resp.status_code == 200 def test_auth_disabled_when_secret_unset(open_client: TestClient): """Local dev mode: no secret configured means no auth enforcement.""" resp = _post_query(open_client) assert resp.status_code == 200 assert open_client.get("/health/deep").status_code == 200