diff --git a/tests/integration/test_endpoints.py b/tests/integration/test_endpoints.py index fbf9d4c..ed593b4 100644 --- a/tests/integration/test_endpoints.py +++ b/tests/integration/test_endpoints.py @@ -1,21 +1,293 @@ +# 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, patch +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_endpoint(): - async with httpx.AsyncClient() as client: - with patch("rag.rag_server.get_orchestrator") as mock_orch: - mock_orch.return_value.clear_kb = AsyncMock(return_value={"status": "ok"}) - response = await client.post( - "http://localhost:8080/rag/clear", - json={"user_jid": "test@domain", "room_jid": None, "is_global": False}, - headers={"X-API-Key": "testkey"} - ) - assert response.status_code == 200 - data = response.json() - assert data.get("status") == "ok" \ No newline at end of file +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() \ No newline at end of file