"""Integration tests for the auth flow (SPEC §4.3 steps 2-3, §12, §3). * No ``Authorization`` header => 401, sanitized body, X-Request-ID present. * Wrong/malformed Bearer => 401, identical sanitized body (no enumeration). * Valid key against a route that hits Ollama => 200 (or generic non-2xx that is NOT a leakage of upstream internals — body has only ``error.code``/``message``/``request_id``). Drives the real ``create_app()`` against testcontainer Postgres + Redis with the upstream Ollama overridden to the in-process mock. """ from __future__ import annotations import httpx import pytest from tests.integration.conftest import IntegrationApp, IntegrationKey pytestmark = pytest.mark.asyncio def _assert_sanitized_error(body: dict[str, object], request_id_header: str | None) -> None: """The error body must carry only safe fields, and echo the request id.""" assert "error" in body err = body["error"] assert isinstance(err, dict) # Exactly the three documented fields — no upstream detail, no stack trace. assert set(err.keys()) <= {"code", "message", "request_id"} assert isinstance(err["code"], str) and err["code"] assert isinstance(err["message"], str) and err["message"] # No mention of Ollama, Postgres, Redis, or Python tracebacks anywhere. blob = " ".join(str(v) for v in err.values()).lower() for needle in ("ollama", "postgres", "redis", "traceback", "asyncpg", "sqlalchemy"): assert needle not in blob, f"leaked internal token {needle!r}: {body}" # The request id from the response header round-trips into the body. if request_id_header is not None: assert err["request_id"] == request_id_header async def test_missing_bearer_returns_401( client: httpx.AsyncClient, integration_app: IntegrationApp ) -> None: resp = await client.post("/api/chat", json={"model": "llama3.1:8b", "messages": []}) assert resp.status_code == 401 request_id = resp.headers.get("X-Request-ID") assert request_id # always present (SPEC §6.5) _assert_sanitized_error(resp.json(), request_id) async def test_malformed_bearer_returns_401(client: httpx.AsyncClient) -> None: resp = await client.post( "/api/chat", headers={"Authorization": "NotBearer foo"}, json={"model": "llama3.1:8b", "messages": []}, ) assert resp.status_code == 401 _assert_sanitized_error(resp.json(), resp.headers.get("X-Request-ID")) async def test_wrong_key_returns_401(client: httpx.AsyncClient) -> None: # Well-formed prefix shape but no such key in the DB. resp = await client.post( "/api/chat", headers={"Authorization": "Bearer nz_doesnotexistxxxxxxxxxxxxxxxxxxxxxxxxxx"}, json={"model": "llama3.1:8b", "messages": []}, ) assert resp.status_code == 401 _assert_sanitized_error(resp.json(), resp.headers.get("X-Request-ID")) async def test_valid_key_authenticates( client: httpx.AsyncClient, api_key: IntegrationKey ) -> None: resp = await client.post( "/api/chat", headers={"Authorization": f"Bearer {api_key.full_key}"}, json={ "model": "llama3.1:8b", "messages": [{"role": "user", "content": "hi"}], "stream": False, }, ) # 200 means authenticated AND the model passed the allowlist AND the proxy # forwarded to mock Ollama. Any non-2xx must still be sanitized (no leakage). assert resp.status_code == 200, resp.text assert resp.headers.get("X-Request-ID") body = resp.json() # mock Ollama returns the chat response shape with usage on the final frame. assert body.get("model") == "llama3.1:8b" assert "message" in body async def test_unauthenticated_error_response_has_no_leakage( client: httpx.AsyncClient, ) -> None: # The body must never mention upstream details, even by accident. resp = await client.post("/api/generate", json={"model": "llama3.1:8b"}) assert resp.status_code == 401 _assert_sanitized_error(resp.json(), resp.headers.get("X-Request-ID")) async def test_healthz_does_not_require_auth(client: httpx.AsyncClient) -> None: resp = await client.get("/healthz") assert resp.status_code == 200 assert resp.json()["status"] == "ok"