Newer
Older
cortex-hub / ai-hub / tests / api / test_routes.py
# 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.api.dependencies 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"]