# tests/integration/test_endpoints.py """ Интеграционные тесты для новых эндпоинтов RAG-сервера. Проверяются все эндпоинты, добавленные для команд бота: - /rag/clear (личная, комнатная, глобальная) - /rag/list - /rag/delete_global - /rag/template/* (save, list, delete) - /rag/reset_history - /rag/room/* (add, list) - /rag/stats - /rag/generate_document Используется httpx.AsyncClient для отправки запросов к FastAPI приложению. Все зависимости (оркестратор, БД, Qdrant) замоканы, чтобы тесты были изолированы. """ import pytest import httpx from unittest.mock import AsyncMock, MagicMock, patch from fastapi import FastAPI from rag.rag_server import app from rag.config_models import AppConfig from rag.services.postgres_service import PostgresService from rag.services.kb_service import KBService @pytest.fixture def mock_orchestrator(): """Создаёт мок для RAGOrchestrator с необходимыми методами.""" with patch("rag.rag_server.get_orchestrator") as mock_get: orch = AsyncMock() # Методы для очистки orch.kb = AsyncMock() orch.kb.clear_global_kb = AsyncMock(return_value=None) orch.kb.clear_room_kb = AsyncMock(return_value=None) orch.kb.clear_user_kb = AsyncMock(return_value=None) orch.kb.get_sources_with_type = AsyncMock(return_value=[]) # Методы для шаблонов orch.db = AsyncMock() orch.db.save_template = AsyncMock(return_value=True) orch.db.list_templates = AsyncMock(return_value=["template1", "template2"]) orch.db.delete_template = AsyncMock(return_value=True) orch.db.add_room = AsyncMock(return_value=None) orch.db.get_rooms = AsyncMock(return_value=["room1@conference.domain", "room2@conference.domain"]) orch.db.clear_room_history = AsyncMock(return_value=None) orch.db.clear_user_history = AsyncMock(return_value=None) orch.db.get_template = AsyncMock(return_value={"file_path": "/tmp/template.docx", "file_hash": "abc123"}) # Метод для генерации документа orch.generate_document_from_template = AsyncMock(return_value="/tmp/generated.docx") # Метод для статистики orch.db.pool = AsyncMock() orch.db.pool.acquire = AsyncMock() conn = AsyncMock() conn.fetchval = AsyncMock(return_value=42) orch.db.pool.acquire.return_value.__aenter__.return_value = conn mock_get.return_value = orch yield orch @pytest.fixture def client(): """Создаёт асинхронный HTTP-клиент для тестового приложения.""" return httpx.AsyncClient(app=app, base_url="http://test") @pytest.mark.asyncio async def test_clear_personal(client, mock_orchestrator): """Тест очистки личной БЗ (room_jid=None, is_global=False).""" payload = { "user_jid": "user@domain", "room_jid": None, "is_global": False } headers = {"X-API-Key": "testkey"} response = await client.post("/rag/clear", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert data.get("status") == "ok" mock_orchestrator.kb.clear_user_kb.assert_called_once_with("user@domain") @pytest.mark.asyncio async def test_clear_room(client, mock_orchestrator): """Тест очистки комнатной БЗ (room_jid указан).""" payload = { "user_jid": "user@domain", "room_jid": "room@conference.domain", "is_global": False } headers = {"X-API-Key": "testkey"} response = await client.post("/rag/clear", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert data.get("status") == "ok" mock_orchestrator.kb.clear_room_kb.assert_called_once_with("room@conference.domain") @pytest.mark.asyncio async def test_clear_global(client, mock_orchestrator): """Тест очистки глобальной БЗ (is_global=True).""" payload = { "user_jid": "admin@domain", "room_jid": None, "is_global": True } headers = {"X-API-Key": "testkey"} response = await client.post("/rag/clear", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert data.get("status") == "ok" mock_orchestrator.kb.clear_global_kb.assert_called_once() @pytest.mark.asyncio async def test_list_documents(client, mock_orchestrator): """Тест получения списка документов.""" mock_orchestrator.kb.get_sources_with_type.return_value = [ {"source_name": "doc1.docx", "is_global": False}, {"source_name": "doc2.pdf", "is_global": True} ] payload = { "user_jid": "user@domain", "room_jid": None } headers = {"X-API-Key": "testkey"} response = await client.post("/rag/list", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert "documents" in data assert len(data["documents"]) == 2 assert data["documents"][0]["source_name"] == "doc1.docx" assert data["documents"][1]["source_name"] == "doc2.pdf" mock_orchestrator.kb.get_sources_with_type.assert_called_once_with("user@domain", None) @pytest.mark.asyncio async def test_delete_global(client, mock_orchestrator): """Тест удаления глобального документа.""" # Мокаем поиск doc_id mock_orchestrator.db.pool.acquire.return_value.__aenter__.return_value.fetch = AsyncMock(return_value=[ {"id": 123} ]) mock_orchestrator.qdrant = AsyncMock() mock_orchestrator.qdrant.delete_by_doc_id = MagicMock(return_value=None) mock_orchestrator.db.delete_documents_by_ids = AsyncMock(return_value=None) payload = {"source_name": "ГОСТ Р 12345"} headers = {"X-API-Key": "testkey"} response = await client.post("/rag/delete_global", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert data.get("status") == "ok" # Проверяем вызовы mock_orchestrator.qdrant.delete_by_doc_id.assert_called_once_with(123) mock_orchestrator.db.delete_documents_by_ids.assert_called_once_with([123]) @pytest.mark.asyncio async def test_template_save(client, mock_orchestrator): """Тест сохранения шаблона.""" payload = { "room_jid": "room@conference.domain", "name": "Шаблон договора", "file_path": "/tmp/template.docx", "file_hash": "abc123" } headers = {"X-API-Key": "testkey"} response = await client.post("/rag/template/save", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert data.get("status") == "ok" mock_orchestrator.db.save_template.assert_called_once_with( "room@conference.domain", "Шаблон договора", "/tmp/template.docx", "abc123" ) @pytest.mark.asyncio async def test_template_list(client, mock_orchestrator): """Тест получения списка шаблонов.""" payload = {"room_jid": "room@conference.domain"} headers = {"X-API-Key": "testkey"} response = await client.post("/rag/template/list", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert "templates" in data assert data["templates"] == ["template1", "template2"] mock_orchestrator.db.list_templates.assert_called_once_with("room@conference.domain") @pytest.mark.asyncio async def test_template_delete(client, mock_orchestrator): """Тест удаления шаблона.""" payload = { "room_jid": "room@conference.domain", "name": "Шаблон договора" } headers = {"X-API-Key": "testkey"} response = await client.post("/rag/template/delete", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert data.get("status") == "ok" mock_orchestrator.db.delete_template.assert_called_once_with("room@conference.domain", "Шаблон договора") @pytest.mark.asyncio async def test_reset_history_personal(client, mock_orchestrator): """Тест сброса личной истории.""" payload = { "user_jid": "user@domain", "room_jid": None } headers = {"X-API-Key": "testkey"} response = await client.post("/rag/reset_history", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert data.get("status") == "ok" mock_orchestrator.db.clear_user_history.assert_called_once_with("user@domain") @pytest.mark.asyncio async def test_reset_history_room(client, mock_orchestrator): """Тест сброса истории комнаты.""" payload = { "user_jid": "user@domain", "room_jid": "room@conference.domain" } headers = {"X-API-Key": "testkey"} response = await client.post("/rag/reset_history", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert data.get("status") == "ok" mock_orchestrator.db.clear_room_history.assert_called_once_with("room@conference.domain") @pytest.mark.asyncio async def test_add_room(client, mock_orchestrator): """Тест добавления комнаты.""" payload = {"room_jid": "newroom@conference.domain"} headers = {"X-API-Key": "testkey"} response = await client.post("/rag/room/add", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert data.get("status") == "ok" mock_orchestrator.db.add_room.assert_called_once_with("newroom@conference.domain") @pytest.mark.asyncio async def test_get_rooms(client, mock_orchestrator): """Тест получения списка комнат.""" headers = {"X-API-Key": "testkey"} response = await client.get("/rag/room/list", headers=headers) assert response.status_code == 200 data = response.json() assert "rooms" in data assert data["rooms"] == ["room1@conference.domain", "room2@conference.domain"] mock_orchestrator.db.get_rooms.assert_called_once() @pytest.mark.asyncio async def test_get_stats(client, mock_orchestrator): """Тест получения статистики.""" headers = {"X-API-Key": "testkey"} response = await client.post("/rag/stats", json={}, headers=headers) assert response.status_code == 200 data = response.json() assert "documents" in data assert data["documents"] == 42 # Также проверяем uptime (может быть 0 или другое) assert "uptime" in data @pytest.mark.asyncio async def test_generate_document(client, mock_orchestrator): """Тест генерации документа по шаблону.""" payload = { "template_name": "Шаблон договора", "user_jid": "user@domain", "room_jid": "room@conference.domain" } headers = {"X-API-Key": "testkey"} response = await client.post("/rag/generate_document", json=payload, headers=headers) assert response.status_code == 200 data = response.json() assert "document" in data assert data["document"] == "/tmp/generated.docx" mock_orchestrator.db.get_template.assert_called_once_with("room@conference.domain", "Шаблон договора") mock_orchestrator.generate_document_from_template.assert_called_once()