# tests/api/test_routes.py import pytest from unittest.mock import MagicMock, AsyncMock from fastapi import FastAPI from fastapi.testclient import TestClient from sqlalchemy.orm import Session # Import the dependencies and router factory from app.core.services import RAGService from app.db.session import get_db from app.api.routes import create_api_router @pytest.fixture def client(): """ Pytest fixture to create a TestClient with a fully mocked environment. This fixture creates a new, isolated FastAPI app for each test, ensuring that mocks for the RAGService and database are always used. """ # 1. Create a fresh FastAPI app for this test run to prevent state leakage. test_app = FastAPI() # 2. Mock the RAGService and the database session. mock_rag_service = MagicMock(spec=RAGService) mock_db_session = MagicMock(spec=Session) def override_get_db(): """Dependency override for the database session.""" yield mock_db_session # 3. Create the API router using the MOCKED service instance. api_router = create_api_router(rag_service=mock_rag_service) # 4. Apply the dependency override and the router to the isolated test app. test_app.dependency_overrides[get_db] = override_get_db test_app.include_router(api_router) # 5. Yield the client and the mock service for use in the tests. yield TestClient(test_app), mock_rag_service # --- Test Cases --- def test_read_root(client): """ Tests the root endpoint to ensure the API is running. """ test_client, _ = client response = test_client.get("/") assert response.status_code == 200 assert response.json() == {"status": "AI Model Hub is running!"} def test_chat_handler_success(client): """ Tests a successful chat request. """ test_client, mock_rag_service = client # Arrange: Configure the mock service to return a successful async response. mock_rag_service.chat_with_rag = AsyncMock(return_value="This is a mocked RAG response.") # Act response = test_client.post("/chat", json={"prompt": "Hello there!", "model": "gemini"}) # Assert assert response.status_code == 200 assert response.json() == { "answer": "This is a mocked RAG response.", "model_used": "gemini" } # Verify the mock was called correctly. mock_rag_service.chat_with_rag.assert_called_once() def test_chat_handler_validation_error(client): """ Tests the chat endpoint with invalid data (an empty prompt). """ test_client, _ = client response = test_client.post("/chat", json={"prompt": "", "model": "deepseek"}) assert response.status_code == 422 # FastAPI's validation error code def test_chat_handler_internal_error(client): """ Tests the chat endpoint when the RAG service raises an unexpected exception. """ test_client, mock_rag_service = client # Arrange: Configure the mock to raise an exception. error_message = "LLM provider is down" mock_rag_service.chat_with_rag.side_effect = Exception(error_message) # Act response = test_client.post("/chat", json={"prompt": "A valid question", "model": "deepseek"}) # Assert assert response.status_code == 500 assert f"An unexpected error occurred with the deepseek API: {error_message}" in response.json()["detail"] def test_add_document_success(client): """ Tests successfully adding a document. """ test_client, mock_rag_service = client # Arrange: Configure the mock to return a specific document ID. mock_rag_service.add_document.return_value = 123 doc_payload = { "title": "Test Document", "text": "This is the content of the document.", "source_url": "http://example.com", "author": "Tester", "user_id": "default_user" } # Act response = test_client.post("/document", json=doc_payload) # Assert assert response.status_code == 200 assert response.json() == {"message": "Document 'Test Document' added successfully with ID 123"} # Verify the mock was called with the correct data. mock_rag_service.add_document.assert_called_once_with( db=mock_rag_service.add_document.call_args.kwargs['db'], doc_data=doc_payload ) def test_add_document_error(client): """ Tests the document creation endpoint when the service raises an exception. """ test_client, mock_rag_service = client # Arrange: Configure the mock to raise an exception. error_message = "Database connection failed" mock_rag_service.add_document.side_effect = Exception(error_message) # FIX: This payload must be valid to pass Pydantic validation and reach the # part of the code that handles the exception we are testing for. doc_payload = { "title": "Error Doc", "text": "Some text", "source_url": "http://example.com/error", "author": "Error Author", "user_id": "error_user" } # Act response = test_client.post("/document", json=doc_payload) # Assert assert response.status_code == 500 assert error_message in response.json()["detail"]