File size: 5,961 Bytes
f813ba1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | """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
|