import pytest
from fastapi.testclient import TestClient
from httpx import Response # Используем Response из httpx, так как TestClient его возвращает
from unittest.mock import patch, MagicMock
# Важно: Импортируем 'app' из модуля, где он создан
from backend.app import app
# Фикстура для создания тестового клиента
@pytest.fixture(scope="module")
def client() -> TestClient:
return TestClient(app)
# Фикстура для мокирования сессии requests
@pytest.fixture
def mock_search_session():
with patch("backend.app.requests.Session") as mock_session_cls:
mock_session_instance = MagicMock()
# Настраиваем мок для POST запроса на /search
mock_response_search = MagicMock(spec=Response)
mock_response_search.status_code = 200
mock_response_search.json.return_value = {
"hits": [
{"id": "test.txt", "_formatted": {"id": "test.txt", "content": "Это тест"}},
{"id": "another.pdf", "_formatted": {"id": "another.pdf", "content": "Еще один тестовый файл"}}
],
"query": "тест",
"processingTimeMs": 10,
"limit": 20,
"offset": 0,
"estimatedTotalHits": 2
}
# Настраиваем мок для GET запроса на /health
mock_response_health = MagicMock(spec=Response)
mock_response_health.status_code = 200
mock_response_health.json.return_value = {"status": "available"}
# Используем side_effect для разных ответов на разные URL
def side_effect(*args, **kwargs):
url = args[0] # Первый аргумент - URL
if 'search' in url:
if 'json' in kwargs and kwargs['json'].get('q') == "ошибка": # Имитируем ошибку Meili
mock_err_resp = MagicMock(spec=Response)
mock_err_resp.status_code = 500
mock_err_resp.raise_for_status.side_effect = requests.exceptions.HTTPError("Meili Error")
return mock_err_resp
return mock_response_search
elif 'health' in url:
return mock_response_health
else: # Поведение по умолчанию
default_resp = MagicMock(spec=Response)
default_resp.status_code = 404
return default_resp
# Назначаем side_effect для разных методов
mock_session_instance.post.side_effect = side_effect
mock_session_instance.get.side_effect = side_effect
mock_session_cls.return_value = mock_session_instance
yield mock_session_instance
def test_search_success(client: TestClient, mock_search_session: MagicMock):
"""Тестирует успешный поиск."""
response = client.get("/search?q=тест")
assert response.status_code == 200
data = response.json()
assert "results" in data
assert len(data["results"]) == 2
assert data["results"][0]["id"] == "test.txt"
assert "тест" in data["results"][0]["content"]
# Проверяем, что был вызван POST к MeiliSearch (т.к. app.py использует POST)
mock_search_session.post.assert_called_once()
def test_search_empty_query(client: TestClient):
"""Тестирует поиск с пустым запросом (FastAPI вернет 422)."""
response = client.get("/search?q=")
assert response.status_code == 422 # Ошибка валидации FastAPI
def test_search_meili_error(client: TestClient, mock_search_session: MagicMock):
"""Тестирует обработку ошибки от Meilisearch."""
response = client.get("/search?q=ошибка") # Используем запрос, который вызовет ошибку в моке
assert response.status_code == 503 # Service Unavailable
assert response.json()["detail"] == "Сервис поиска временно недоступен"
def test_get_file_not_found(client: TestClient):
"""Тестирует запрос несуществующего файла."""
# Мы не мокируем os.path.exists, поэтому он вернет False
response = client.get("/files/nonexistent.txt")
assert response.status_code == 404
assert response.json()["detail"] == "Файл не найден"
def test_get_file_invalid_name(client: TestClient):
"""Тестирует запрос файла с некорректным именем."""
response = client.get("/files/../secret.txt")
assert response.status_code == 400 # Bad Request (изменен в app.py)
# Пример теста для /health (уже частично покрыт в mock_search_session)
def test_health_check(client: TestClient, mock_search_session: MagicMock):
"""Тестирует эндпоинт /health."""
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert data["meilisearch_status"] == "доступен"
mock_search_session.get.assert_called_once_with(f"{app.SEARCH_ENGINE_URL}/health")
# TODO: Добавить тест для успешного получения файла (требует мокирования os.path и создания временного файла)