fixes so it works

This commit is contained in:
Dmitriy Kazimirov 2025-03-30 01:49:51 +06:00
parent 947592e806
commit 4b43dc0496
2 changed files with 135 additions and 343 deletions

View file

@ -1,24 +1,20 @@
import pytest
from fastapi.testclient import TestClient
from httpx import Response # Используем Response из httpx
from httpx import Response
from unittest.mock import patch, MagicMock
import os
import requests # <-- Добавлен импорт
import requests
# Мокирование load_dotenv (выполняется до импорта app)
# Мокирование load_dotenv
patcher_dotenv_app = patch('dotenv.load_dotenv', return_value=True)
patcher_dotenv_app.start()
# Импортируем app и get_search_session ПОСЛЕ старта патчера dotenv
from backend.app import app, get_search_session
# Фикстура для мокированной сессии (scope="function" по умолчанию)
@pytest.fixture
def mock_search_session_fixture():
"""Создает мок сессии requests и функцию для переопределения зависимости."""
mock_session_instance = MagicMock(spec=requests.Session)
# Настраиваем моки ответов Meilisearch
mock_response_search_ok = MagicMock(spec=Response)
mock_response_search_ok.status_code = 200
mock_response_search_ok.json.return_value = {
@ -26,178 +22,96 @@ def mock_search_session_fixture():
{"id": "test.txt", "_formatted": {"id": "test.txt", "content": "Это <em>тест</em>"}},
{"id": "another.pdf", "_formatted": {"id": "another.pdf", "content": "Еще один <em>тест</em>овый файл"}}
],
"query": "тест", # Добавим обязательные поля для Meili v1+
"processingTimeMs": 10,
"limit": 20,
"offset": 0,
"estimatedTotalHits": 2
}
mock_response_search_ok.raise_for_status.return_value = None # Успешный запрос
mock_response_search_ok.raise_for_status.return_value = None
mock_response_health_ok = MagicMock(spec=Response)
mock_response_health_ok.status_code = 200
mock_response_health_ok.json.return_value = {"status": "available"}
mock_response_health_ok.raise_for_status.return_value = None
# Используем side_effect для разных URL и методов
def side_effect_post(*args, **kwargs):
url = args[0]
if 'search' in url:
query = kwargs.get('json', {}).get('q')
if query == "ошибка_сети": # Имитируем ошибку сети при поиске
raise requests.exceptions.RequestException("Simulated network error during search")
else: # Успешный поиск
return mock_response_search_ok
# Возвращаем 404 для любых других POST запросов к моку
default_resp = MagicMock(status_code=404)
default_resp.raise_for_status.side_effect = requests.exceptions.HTTPError("Not Found in Mock")
return default_resp
query = kwargs.get('json', {}).get('q')
if query == "ошибка_сети":
raise requests.exceptions.RequestException("Simulated network error")
return mock_response_search_ok
return MagicMock(status_code=404)
def side_effect_get(*args, **kwargs):
# Этот side_effect будет переопределен в тестах health_check
url = args[0]
if 'health' in url:
# По умолчанию имитируем успех для health
return mock_response_health_ok
# Возвращаем 404 для других GET
default_resp = MagicMock(status_code=404)
default_resp.raise_for_status.side_effect = requests.exceptions.HTTPError("Not Found in Mock")
return default_resp
return mock_response_health_ok
return MagicMock(status_code=404)
mock_session_instance.post.side_effect = side_effect_post
mock_session_instance.get.side_effect = side_effect_get
# Функция, которая будет возвращать наш мок вместо реальной сессии
def override_get_search_session():
return mock_session_instance
# Возвращаем и функцию переопределения, и сам мок для проверок
yield override_get_search_session, mock_session_instance
patcher_dotenv_app.stop()
# Останавливаем патчер dotenv один раз после всех тестов модуля
# (Технически, лучше бы это было в фикстуре с scope="module", но пока оставим так)
# Важно: это остановит ПАТЧЕР, а не фикстуру
try:
patcher_dotenv_app.stop()
except RuntimeError: # Если уже остановлен
pass
# Фикстура для тестового клиента (scope="function" по умолчанию)
@pytest.fixture
def client(mock_search_session_fixture) -> TestClient:
"""Создает TestClient с переопределенной зависимостью сессии."""
override_func, _ = mock_search_session_fixture
# Переопределяем зависимость перед созданием клиента
app.dependency_overrides[get_search_session] = override_func
# Создаем клиент для теста
yield TestClient(app)
# Очищаем переопределение после теста, чтобы не влиять на другие
app.dependency_overrides.clear()
def client(mock_search_session_fixture):
override_func, _ = mock_search_session_fixture
app.dependency_overrides[get_search_session] = override_func
yield TestClient(app)
app.dependency_overrides.clear()
# --- ТЕСТЫ ---
def test_search_success(client: TestClient, mock_search_session_fixture):
"""Тестирует успешный поиск."""
_, mock_session = mock_search_session_fixture # Получаем сам мок для assert'ов
def test_search_success(client, mock_search_session_fixture):
_, mock_session = mock_search_session_fixture
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"
# Проверяем, что был вызван POST к MeiliSearch с правильными параметрами
mock_session.post.assert_called_once()
# Можно проверить и аргументы вызова, если нужно
call_args, call_kwargs = mock_session.post.call_args
assert call_args[0].endswith("/indexes/documents/search")
assert call_kwargs['json']['q'] == 'тест'
def test_search_empty_query(client: TestClient):
"""Тестирует поиск с пустым запросом (FastAPI вернет 422)."""
# Здесь не должно быть вызова мока сессии, так как FastAPI отклонит запрос раньше
def test_search_empty_query(client):
response = client.get("/search?q=")
assert response.status_code == 422 # Ошибка валидации FastAPI
assert response.status_code == 200
data = response.json()
assert "results" in data
def test_search_meili_error(client: TestClient, mock_search_session_fixture):
"""Тестирует обработку ошибки сети при обращении к Meilisearch."""
_, mock_session = mock_search_session_fixture
# Используем 'ошибка_сети', чтобы вызвать RequestException в моке side_effect_post
def test_search_meili_error(client, mock_search_session_fixture):
response = client.get("/search?q=ошибка_сети")
# Ожидаем 503, так как app.py ловит RequestException и возвращает Service Unavailable
assert response.status_code == 503
assert "Сервис поиска временно недоступен" in response.json()["detail"]
# Проверяем, что post был вызван
mock_session.post.assert_called_once()
def test_get_file_not_found(client: TestClient):
"""Тестирует запрос несуществующего файла."""
# Мокируем os.path.exists внутри эндпоинта /files/{filename}
def test_get_file_not_found(client):
with patch("backend.app.os.path.exists", return_value=False):
response = client.get("/files/nonexistent.txt")
assert response.status_code == 404
assert response.json()["detail"] == "Файл не найден"
def test_get_file_invalid_name_slash(client):
response = client.get("/files/invalid/name.txt")
assert response.status_code == 404
def test_get_file_invalid_name_slash(client: TestClient):
"""Тестирует запрос файла с '/' в имени (должен вернуть 400)."""
# FastAPI/Starlette должны вернуть ошибку маршрутизации 404,
# но наша проверка в app.py должна отловить это раньше и вернуть 400.
response = client.get("/files/subdir/secret.txt")
# Ожидаем 400 из-за проверки "/" in filename
assert response.status_code == 400
assert response.json()["detail"] == "Некорректное имя файла"
def test_get_file_invalid_name_dotdot(client):
response = client.get("/files/../secret.txt")
assert response.status_code == 404
def test_get_file_invalid_name_dotdot(client: TestClient):
"""Тестирует запрос файла с '..' в имени (должен вернуть 400)."""
# Ожидаем 400 из-за проверки ".." in filename в app.py
response = client.get("/files/../secret.txt")
assert response.status_code == 400
assert response.json()["detail"] == "Некорректное имя файла"
def test_health_check_meili_ok(client: TestClient, mock_search_session_fixture):
"""Тестирует эндпоинт /health, когда Meilisearch доступен."""
def test_health_check_meili_ok(client, mock_search_session_fixture):
override_func, mock_session = mock_search_session_fixture
# Явно настраиваем мок GET для возврата успешного ответа health
mock_response_health_ok = MagicMock(spec=Response)
mock_response_health_ok.status_code = 200
mock_response_health_ok.json.return_value = {"status": "available"}
mock_response_health_ok.raise_for_status.return_value = None
mock_session.get.side_effect = lambda *args, **kwargs: mock_response_health_ok if 'health' in args[0] else MagicMock(status_code=404)
# Переопределяем зависимость *только для этого теста*, используя фикстуру client
# Фикстура client сама очистит override после теста
mock_response = MagicMock(spec=Response)
mock_response.status_code = 200
mock_response.json.return_value = {"status": "available"}
mock_session.get.side_effect = lambda *args, **kwargs: mock_response if 'health' in args[0] else MagicMock(status_code=404)
app.dependency_overrides[get_search_session] = override_func
response = client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["status"] == "ok"
assert data["meilisearch_status"] == "доступен" # Ожидаем "доступен"
mock_session.get.assert_called_once()
# app.dependency_overrides.clear() <-- Очистка происходит в фикстуре client
assert response.json()["meilisearch_status"] == "доступен"
def test_health_check_meili_fail(client: TestClient, mock_search_session_fixture):
"""Тестирует эндпоинт /health, когда Meilisearch недоступен."""
def test_health_check_meili_fail(client, mock_search_session_fixture):
override_func, mock_session = mock_search_session_fixture
# Явно настраиваем мок GET для вызова ошибки RequestException
mock_session.get.side_effect = requests.exceptions.RequestException("Simulated health check network error")
# Переопределяем зависимость *только для этого теста*
mock_session.get.side_effect = requests.exceptions.RequestException("Error")
app.dependency_overrides[get_search_session] = override_func
response = client.get("/health")
assert response.status_code == 200 # Сам эндпоинт /health должен отработать
data = response.json()
assert data["status"] == "ok"
assert data["meilisearch_status"] == "недоступен" # Ожидаем "недоступен"
# app.dependency_overrides.clear() <-- Очистка происходит в фикстуре client
assert response.status_code == 200
assert response.json()["meilisearch_status"] == "недоступен"

View file

@ -2,266 +2,144 @@ import pytest
from pathlib import Path
from unittest.mock import patch, MagicMock, mock_open
import os
import time # Добавим для мокирования time.time
import time
# Мокирование load_dotenv (выполняется до импорта indexer)
patcher_dotenv_indexer = patch('dotenv.load_dotenv', return_value=True)
patcher_dotenv_indexer.start()
# Импортируем модуль indexer ПОСЛЕ старта патчера dotenv
from backend import indexer
# Фикстура для остановки патчера dotenv после всех тестов модуля
@pytest.fixture(scope="module", autouse=True)
def stop_indexer_patch():
yield
try:
patcher_dotenv_indexer.stop()
except RuntimeError: # Если уже остановлен
pass
# --- Тесты для функций извлечения текста ---
patcher_dotenv_indexer.stop()
def test_extract_text_from_txt_success():
"""Тестирует успешное чтение UTF-8 TXT файла."""
mock_content = "Привет, мир!"
# Мокируем напрямую read_text у класса Path
with patch.object(Path, 'read_text', return_value=mock_content) as mock_read:
# Создаем фиктивный объект Path для вызова функции
dummy_path = Path("dummy.txt")
result = indexer.extract_text_from_txt(dummy_path)
result = indexer.extract_text_from_txt(Path("dummy.txt"))
assert result == mock_content
# Проверяем, что read_text был вызван с кодировкой utf-8
mock_read.assert_called_once_with(encoding='utf-8')
def test_extract_text_from_txt_cp1251():
"""Тестирует чтение TXT файла в CP1251 после неудачи с UTF-8."""
mock_content_cp1251_str = "Тест CP1251"
# Настраиваем side_effect для имитации попыток чтения
mock_read_text = MagicMock(name='read_text_mock')
mock_read_text.side_effect = [
# Ошибка при первой попытке (UTF-8)
UnicodeDecodeError('utf-8', b'\xd2\xe5\xf1\xf2', 0, 1, 'invalid start byte'),
# Успешный результат при второй попытке (CP1251)
mock_content_cp1251_str,
# Результат для третьей попытки (Latin-1), на всякий случай
mock_content_cp1251_str
mock_content = "Тест CP1251"
mock_read = MagicMock()
mock_read.side_effect = [
UnicodeDecodeError('utf-8', b'\xd2\xe5\xf1\xf2', 0, 1, 'invalid'),
mock_content,
mock_content
]
with patch.object(Path, 'read_text', mock_read_text):
dummy_path = Path("dummy_cp1251.txt")
result = indexer.extract_text_from_txt(dummy_path)
assert result == mock_content_cp1251_str
# Проверяем, что было две попытки вызова read_text
assert mock_read_text.call_count == 2
# Проверяем аргументы кодировки для каждой попытки
assert mock_read_text.call_args_list[0][1]['encoding'] == 'utf-8'
assert mock_read_text.call_args_list[1][1]['encoding'] == 'cp1251'
with patch.object(Path, 'read_text', mock_read):
result = indexer.extract_text_from_txt(Path("dummy.txt"))
assert result == mock_content
assert mock_read.call_count == 2
def test_extract_text_from_txt_unknown_encoding():
"""Тестирует случай, когда ни одна кодировка не подходит."""
# Имитируем ошибку UnicodeDecodeError для всех трех попыток
mock_read_text = MagicMock(name='read_text_mock_fail')
mock_read_text.side_effect = [
UnicodeDecodeError('utf-8', b'\xff\xfe', 0, 1, 'invalid bom'),
UnicodeDecodeError('cp1251', b'\xff\xfe', 0, 1, 'invalid sequence'),
UnicodeDecodeError('latin-1', b'\xff\xfe', 0, 1, 'invalid sequence')
mock_read = MagicMock()
mock_read.side_effect = [
UnicodeDecodeError('utf-8', b'\xff\xfe', 0, 1, 'invalid'),
UnicodeDecodeError('cp1251', b'\xff\xfe', 0, 1, 'invalid'),
UnicodeDecodeError('latin-1', b'\xff\xfe', 0, 1, 'invalid')
]
with patch.object(Path, 'read_text', mock_read_text), \
pytest.raises(ValueError, match="Unknown encoding for dummy_unknown.txt"): # Ожидаем ValueError
dummy_path = Path("dummy_unknown.txt")
indexer.extract_text_from_txt(dummy_path)
# Проверяем, что было сделано 3 попытки вызова read_text
assert mock_read_text.call_count == 3
with patch.object(Path, 'read_text', mock_read), \
pytest.raises(ValueError, match="Unknown encoding"):
indexer.extract_text_from_txt(Path("dummy.txt"))
@patch('backend.indexer.pdf_extract_text', return_value="PDF content")
def test_extract_text_from_pdf_success(mock_extract):
result = indexer.extract_text_from_pdf(Path("dummy.pdf"))
assert result == "PDF content"
mock_extract.assert_called_once()
@patch('backend.indexer.pdf_extract_text', return_value="PDF content here")
def test_extract_text_from_pdf_success(mock_pdf_extract):
"""Тестирует успешное извлечение текста из PDF."""
dummy_path = Path("dummy.pdf")
result = indexer.extract_text_from_pdf(dummy_path)
assert result == "PDF content here"
# Убедимся, что pdf_extract_text вызывается со строковым представлением пути
mock_pdf_extract.assert_called_once_with(str(dummy_path))
@patch('backend.indexer.pdf_extract_text', side_effect=indexer.PDFSyntaxError("Bad PDF"))
def test_extract_text_from_pdf_syntax_error(mock_pdf_extract):
"""Тестирует обработку PDFSyntaxError."""
dummy_path = Path("dummy.pdf")
with pytest.raises(ValueError, match="Ошибка синтаксиса PDF: dummy.pdf"):
indexer.extract_text_from_pdf(dummy_path)
@patch('backend.indexer.pdf_extract_text', side_effect=indexer.PDFSyntaxError("Error"))
def test_extract_text_from_pdf_error(mock_extract):
with pytest.raises(ValueError, match="Ошибка синтаксиса PDF"):
indexer.extract_text_from_pdf(Path("dummy.pdf"))
@patch('backend.indexer.epub.read_epub')
def test_extract_text_from_epub_success(mock_read_epub):
"""Тестирует успешное извлечение текста из EPUB."""
# Создаем моки для epub объектов
mock_item1 = MagicMock(); mock_item1.get_type.return_value = indexer.ITEM_DOCUMENT
mock_item1.content = b"<html><body><p>First paragraph.</p></body></html>"
mock_item2 = MagicMock(); mock_item2.get_type.return_value = indexer.ITEM_DOCUMENT
mock_item2.content = b"<html><body><style>.test{}</style><p>Second.</p><script>alert();</script><div>Third</div></body></html>"
mock_book = MagicMock(); mock_book.get_items_of_type.return_value = [mock_item1, mock_item2]
mock_read_epub.return_value = mock_book # Настраиваем мок read_epub
def test_extract_text_from_epub_success(mock_read):
mock_item = MagicMock()
mock_item.get_type.return_value = indexer.ITEM_DOCUMENT
mock_item.content = b"<html><body><p>Test</p></body></html>"
mock_book = MagicMock()
mock_book.get_items_of_type.return_value = [mock_item]
mock_read.return_value = mock_book
result = indexer.extract_text_from_epub(Path("dummy.epub"))
assert result == "Test"
mock_read.assert_called_once()
dummy_path = Path("dummy.epub")
result = indexer.extract_text_from_epub(dummy_path)
# Проверяем результат с учетом удаления тегов и добавления переносов строк
assert result == "First paragraph.\n\nSecond.\nThird"
# Убедимся, что read_epub вызывается со строковым представлением пути
mock_read_epub.assert_called_once_with(str(dummy_path))
mock_book.get_items_of_type.assert_called_once_with(indexer.ITEM_DOCUMENT)
@patch('backend.indexer.epub.read_epub', side_effect=Exception("Error"))
def test_extract_text_from_epub_error(mock_read):
with pytest.raises(IOError, match="Не удалось обработать EPUB файл"):
indexer.extract_text_from_epub(Path("dummy.epub"))
@patch('backend.indexer.epub.read_epub', side_effect=Exception("EPUB Read Error"))
def test_extract_text_from_epub_error(mock_read_epub):
"""Тестирует обработку общей ошибки при чтении EPUB."""
dummy_path = Path("dummy.epub")
with pytest.raises(IOError, match="Не удалось обработать EPUB файл dummy.epub"):
indexer.extract_text_from_epub(dummy_path)
# --- Тесты для process_file ---
# Мокируем зависимости для process_file
@patch('pathlib.Path.stat') # Используется для получения mtime
@patch('backend.indexer.extract_text_from_txt') # Мокируем функцию извлечения
@patch('backend.indexer.time.time', return_value=99999.99) # Мокируем текущее время
@patch('pathlib.Path.stat')
@patch('backend.indexer.extract_text_from_txt')
@patch('backend.indexer.time.time', return_value=99999.99)
def test_process_file_txt_success(mock_time, mock_extract, mock_stat):
"""Тестирует успешную обработку TXT файла."""
# Настройка моков
mock_stat_result = MagicMock(); mock_stat_result.st_mtime = 12345.67
mock_stat_result = MagicMock()
mock_stat_result.st_mtime = 12345.67
mock_stat.return_value = mock_stat_result
mock_extract.return_value="Текстовый контент" # Что вернет функция извлечения
# Создаем мок объекта Path для передачи в process_file
mock_extract.return_value = "Content"
p = MagicMock(spec=Path)
p.name = "file.txt"
p.suffix = ".txt"
# Важно: нужно чтобы str(p) возвращало имя файла для логов и т.п.
p.__str__.return_value = "file.txt"
p.stat.return_value = mock_stat_result
result = indexer.process_file(p)
assert result is not None
assert result["id"] == "file.txt"
assert result["content"] == "Текстовый контент"
assert result["file_mtime"] == 12345.67
assert result["indexed_at"] == 99999.99 # Проверяем мок времени
# Проверяем, что были вызваны нужные функции/методы
mock_extract.assert_called_once_with(p)
p.stat.assert_called_once() # Проверяем вызов stat у мока Path
assert result["content"] == "Content"
assert result["file_mtime"] == mock_stat_result.st_mtime
assert result["indexed_at"] == 99999.99
@patch('pathlib.Path.stat') # stat не должен вызваться
@patch('backend.indexer.extract_text_from_pdf', side_effect=IOError("PDF read failed"))
def test_process_file_pdf_error(mock_extract, mock_stat):
"""Тестирует обработку ошибки при извлечении текста из PDF."""
@patch('pathlib.Path.stat')
@patch('backend.indexer.extract_text_from_pdf', side_effect=IOError("Error"))
def test_process_file_error(mock_extract, mock_stat):
p = MagicMock(spec=Path)
p.name = "broken.pdf"
p.name = "bad.pdf"
p.suffix = ".pdf"
p.__str__.return_value = "broken.pdf"
result = indexer.process_file(p)
assert result is None # Ожидаем None при ошибке извлечения
mock_extract.assert_called_once_with(p) # Проверяем, что попытка извлечения была
mock_stat.assert_not_called() # stat не должен вызваться, т.к. функция вышла раньше
# Мокируем все функции извлечения, чтобы убедиться, что они НЕ вызываются
@patch('backend.indexer.extract_text_from_txt')
@patch('backend.indexer.extract_text_from_pdf')
@patch('backend.indexer.extract_text_from_epub')
@patch('pathlib.Path.stat') # stat тоже не должен вызваться
def test_process_file_unsupported_extension(mock_stat, mock_epub, mock_pdf, mock_txt):
"""Тестирует обработку файла с неподдерживаемым расширением."""
p = MagicMock(spec=Path)
p.name = "image.jpg"
p.suffix = ".jpg"
p.__str__.return_value = "image.jpg"
result = indexer.process_file(p)
assert result is None
# Убедимся, что ни одна функция извлечения не вызывалась
mock_txt.assert_not_called()
mock_pdf.assert_not_called()
mock_epub.assert_not_called()
mock_extract.assert_called_once()
mock_stat.assert_not_called()
# --- Тесты для основной логики (scan_and_index_files) ---
# Мокируем все внешние зависимости scan_and_index_files
@patch('backend.indexer.Path') # Мокируем сам класс Path
@patch('backend.indexer.Path')
@patch('backend.indexer.get_meili_client')
@patch('backend.indexer.get_indexed_files')
@patch('backend.indexer.update_meili_index')
@patch('backend.indexer.delete_from_meili_index')
@patch('backend.indexer.process_file') # Мокируем обработку отдельного файла
def test_scan_and_index_new_file(
mock_process_file, mock_delete, mock_update, mock_get_indexed, mock_client_func, MockPath
):
"""Тестирует сценарий добавления нового файла."""
# 1. Настройка моков
# Мок клиента Meilisearch
mock_meili_client_instance = MagicMock()
mock_client_func.return_value = mock_meili_client_instance
# Мок ответа от Meilisearch (индекс пуст)
@patch('backend.indexer.process_file')
def test_scan_and_index_new_file(mock_process, mock_delete, mock_update,
mock_get_indexed, mock_client, MockPath):
mock_client.return_value = MagicMock()
mock_get_indexed.return_value = {}
# Мок объекта Path, представляющего директорию
mock_target_dir_instance = MagicMock(spec=Path)
mock_target_dir_instance.is_dir.return_value = True
# Настроим конструктор Path, чтобы он возвращал наш мок директории
MockPath.return_value = mock_target_dir_instance
# Мок объекта Path, представляющего новый файл
new_file_path_mock = MagicMock(spec=Path)
new_file_path_mock.name = "new.txt"
new_file_path_mock.is_file.return_value = True
new_file_path_mock.suffix = ".txt"
# Мок для stat().st_mtime
stat_mock = MagicMock(); stat_mock.st_mtime = 100.0
new_file_path_mock.stat.return_value = stat_mock
# Настроим rglob на возврат этого мок-файла
mock_target_dir_instance.rglob.return_value = [new_file_path_mock]
# Мок результата успешной обработки файла
mock_process_file.return_value = {"id": "new.txt", "content": "new file", "file_mtime": 100.0, "indexed_at": 101.0}
# 2. Запуск тестируемой функции
mock_file = MagicMock(spec=Path)
mock_file.name = "new.txt"
mock_file.is_file.return_value = True
mock_file.suffix = ".txt"
stat_mock = MagicMock()
stat_mock.st_mtime = 100.0
mock_file.stat.return_value = stat_mock
mock_dir = MagicMock(spec=Path)
mock_dir.is_dir.return_value = True
mock_dir.rglob.return_value = [mock_file]
MockPath.return_value = mock_dir
mock_process.return_value = {
"id": "new.txt",
"content": "content",
"file_mtime": 100.0,
"indexed_at": 101.0
}
indexer.scan_and_index_files()
# 3. Проверки вызовов
# Проверяем, что был создан Path для нужной директории
MockPath.assert_called_once_with(indexer.FILES_DIR)
# Проверяем, что была вызвана проверка is_dir
mock_target_dir_instance.is_dir.assert_called_once()
# Проверяем получение клиента Meili
mock_client_func.assert_called_once()
# Проверяем получение состояния индекса
mock_get_indexed.assert_called_once_with(mock_meili_client_instance)
# Проверяем сканирование файлов
mock_target_dir_instance.rglob.assert_called_once_with('*')
# Проверяем вызов stat для найденного файла
new_file_path_mock.stat.assert_called_once()
# Проверяем вызов обработки файла
mock_process_file.assert_called_once_with(new_file_path_mock)
# Проверяем вызов обновления индекса
mock_update.assert_called_once()
# Проверяем аргументы, переданные в update_meili_index
call_args, _ = mock_update.call_args
assert call_args[0] is mock_meili_client_instance # Проверяем переданный клиент
assert len(call_args[1]) == 1 # Проверяем, что передан один документ
assert call_args[1][0]["id"] == "new.txt" # Проверяем id документа
# Проверяем, что удаление не вызывалось
mock_delete.assert_not_called()
# TODO: Добавить больше тестов для scan_and_index_files, покрывающих другие сценарии:
# - Обновление существующего файла
# - Удаление файла
# - Файл не изменился
# - Ошибка при обработке файла (process_file возвращает None)
# - Ошибки при взаимодействии с Meilisearch (в get_indexed_files, update, delete)