"""Tests for FastAPI API endpoints using TestClient with mocked services.""" from __future__ import annotations from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi.testclient import TestClient @pytest.fixture def client() -> TestClient: """Create a FastAPI test client with mocked startup.""" with patch("app.main.get_vector_store") as mock_vs: mock_store = MagicMock() mock_store.ensure_collection = AsyncMock() mock_store.check_connection.return_value = True mock_vs.return_value = mock_store from app.main import app return TestClient(app) def test_health_endpoint(client: TestClient) -> None: """Test the health check endpoint returns proper status.""" with patch("app.main.get_vector_store") as mock_vs, \ patch("app.main.get_llm_client") as mock_llm: mock_store = MagicMock() mock_store.check_connection.return_value = True mock_vs.return_value = mock_store mock_client = MagicMock() mock_client.check_connection.return_value = True mock_llm.return_value = mock_client response = client.get("/api/v1/health") assert response.status_code == 200 data = response.json() assert "status" in data assert "qdrant_connected" in data assert "groq_connected" in data assert "timestamp" in data def test_root_endpoint(client: TestClient) -> None: """Test the root endpoint returns API info.""" response = client.get("/") assert response.status_code == 200 data = response.json() assert data["name"] == "Multi-Document RAG System" assert data["version"] == "1.0.0" @patch("app.api.routes.ingest.run_ingestion_pipeline") def test_ingest_pdf_endpoint(mock_pipeline: MagicMock, client: TestClient) -> None: """Test the ingest endpoint with a mock PDF file.""" from app.models.schemas import IngestResult mock_pipeline.return_value = IngestResult( doc_id="test-doc-id", filename="test.pdf", chunks_created=10, metadata={"doc_type": "research_paper", "source": "test"}, status="success", ) import io file_content = b"%PDF-1.4 fake pdf content" response = client.post( "/api/v1/ingest", files={"file": ("test.pdf", io.BytesIO(file_content), "application/pdf")}, data={"source": "test"}, ) assert response.status_code == 200 data = response.json() assert data["doc_id"] == "test-doc-id" assert data["chunks_created"] == 10 assert data["status"] == "success" def test_ingest_rejects_unsupported_format(client: TestClient) -> None: """Test that ingest rejects unsupported file formats.""" import io response = client.post( "/api/v1/ingest", files={"file": ("test.xlsx", io.BytesIO(b"data"), "application/xlsx")}, data={"source": "test"}, ) assert response.status_code == 400 @patch("app.api.routes.query.run_rag_chain") def test_query_endpoint(mock_chain: MagicMock, client: TestClient) -> None: """Test the query endpoint with a mock RAG chain response.""" from app.models.schemas import QueryResponse, SourceChunk mock_chain.return_value = QueryResponse( answer="Machine learning is a subset of AI.", sources=[SourceChunk( doc_id="doc-1", filename="paper.pdf", text="ML text", chunk_index=0, score=0.9, )], model_used="llama-3.3-70b-versatile", latency_ms=500, relevance_score=0.85, ) response = client.post( "/api/v1/query", json={"question": "What is ML?", "mode": "standard", "top_k": 5}, ) assert response.status_code == 200 data = response.json() assert "answer" in data assert "sources" in data assert data["model_used"] == "llama-3.3-70b-versatile" @patch("app.api.routes.query.run_cross_doc_chain") def test_compare_endpoint(mock_chain: MagicMock, client: TestClient) -> None: """Test the compare endpoint with a mock cross-doc chain response.""" from app.models.schemas import CompareResponse mock_chain.return_value = CompareResponse( comparison="Paper A uses CNN, Paper B uses RNN.", agreements=["Both classify images"], contradictions=["Different architectures"], model_used="deepseek-r1-distill-llama-70b", latency_ms=1200, ) response = client.post( "/api/v1/query/compare", json={ "question": "Compare approaches", "doc_ids": ["doc-1", "doc-2"], "aspect": "methodology", }, ) assert response.status_code == 200 data = response.json() assert "comparison" in data assert "agreements" in data assert "contradictions" in data def test_compare_rejects_single_doc(client: TestClient) -> None: """Test that compare rejects requests with less than 2 doc_ids.""" response = client.post( "/api/v1/query/compare", json={"question": "Compare", "doc_ids": ["doc-1"], "aspect": "general"}, ) assert response.status_code == 422 # Pydantic validation @patch("app.api.routes.evaluate.run_ragas_evaluation") def test_evaluate_endpoint(mock_eval: MagicMock, client: TestClient) -> None: """Test the evaluate endpoint with a mock RAGAS result.""" from app.models.schemas import RAGASMetrics mock_eval.return_value = RAGASMetrics( faithfulness=0.85, answer_relevancy=0.90, context_precision=0.80, context_recall=0.75, answer_correctness=0.82, eval_id="eval-test123", timestamp="2024-01-01T00:00:00Z", question_count=5, ) response = client.post( "/api/v1/evaluate", json={"sample_size": 3}, ) assert response.status_code == 200 data = response.json() assert data["status"] == "completed" assert data["metrics"]["faithfulness"] == 0.85