""" Smoke tests for the FastAPI layer. These deliberately do NOT require a live Postgres/pgvector or Ollama instance - they assert that every endpoint is reachable and degrades gracefully (returns a sensible JSON error/empty-result rather than a 500/crash) when those dependencies are unavailable, which is the situation on a fresh checkout before `.env` + the database are set up. For tests that exercise real retrieval/generation against a live database and Ollama, see `tests/test_rag_integration.py` (skipped automatically unless `RUN_INTEGRATION_TESTS=1` is set - see that file). """ from __future__ import annotations # Environment setup (settings that must exist before app.main is imported, # including the auth secrets) lives in tests/conftest.py, which pytest loads # first. The `client`, `admin_headers` and `user_headers` fixtures come from # there too. def test_root(client) -> None: resp = client.get("/") assert resp.status_code == 200 if "text/html" in resp.headers.get("content-type", ""): assert " None: resp = client.get("/api/health") assert resp.status_code == 200 body = resp.json() assert body["status"] in {"ok", "degraded"} assert isinstance(body["database"], bool) assert isinstance(body["ollama"], bool) def test_brands_returns_empty_list_without_database(client) -> None: resp = client.get("/api/brands") assert resp.status_code == 200 assert resp.json() == {"brands": []} def test_brand_products_returns_empty_without_database(client) -> None: resp = client.get("/api/brands/Parle/products") assert resp.status_code == 200 body = resp.json() assert body["brand"] == "Parle" assert body["products"] == [] assert body["total"] == 0 def test_product_detail_404_when_missing(client) -> None: resp = client.get("/api/brands/Parle/products/does-not-exist") assert resp.status_code == 404 # These now sign in first. The guard runs before body validation, so an # unauthenticated call gets 401 and never reaches the schema check these # assertions are actually about. tests/test_auth.py covers the guards # themselves. def test_chat_validates_empty_query(client, user_headers) -> None: resp = client.post("/api/chat", json={"query": ""}, headers=user_headers) assert resp.status_code == 422 # min_length=1 violated def test_chat_rejects_too_many_top_k(client, user_headers) -> None: resp = client.post("/api/chat", json={"query": "snacks", "top_k": 999}, headers=user_headers) assert resp.status_code == 422 # le=15 violated def test_catalog_generate_returns_job_id(client, admin_headers) -> None: resp = client.post( "/api/catalog/generate", json={"brand": "TestBrand", "max_products": 1}, headers=admin_headers, ) assert resp.status_code == 202 body = resp.json() assert body["brand"] == "TestBrand" assert body["status"] in {"pending", "running", "done", "failed"} def test_openapi_schema_lists_all_routers(client) -> None: resp = client.get("/openapi.json") assert resp.status_code == 200 paths = resp.json()["paths"] for expected in ("/api/health", "/api/brands", "/api/search", "/api/suggest", "/api/chat", "/api/catalog/generate"): assert expected in paths, f"missing route: {expected}" def test_suggest_works_without_a_database(client) -> None: """Autocomplete must degrade, not 500, when the catalog is unreachable. This suite runs with no reachable database, so this pins that the search box keeps suggesting from the static brand table instead of erroring - a failing suggest call must never block someone from typing and searching. """ resp = client.get("/api/suggest", params={"q": "cavin"}) assert resp.status_code == 200 labels = [s["label"] for s in resp.json()["suggestions"]] assert "Cavinkare" in labels def test_suggest_requires_a_query(client) -> None: assert client.get("/api/suggest").status_code == 422