From 713b34fdcd887d546ca6cb37387df761bf0dd16f Mon Sep 17 00:00:00 2001 From: charles Date: Wed, 17 Dec 2025 17:01:13 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA(backend)=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I add tests to test the app. --- .../document_rag_backends/find_rag_backend.py | 19 ++--- src/backend/chat/clients/pydantic_ai.py | 9 +- .../chat/tests/tools/test_web_search_brave.py | 3 - .../test_conversation_with_document_upload.py | 84 ++++++++++++++++--- src/backend/chat/views.py | 3 +- src/backend/utils/oicd.py | 12 ++- 6 files changed, 98 insertions(+), 32 deletions(-) diff --git a/src/backend/chat/agent_rag/document_rag_backends/find_rag_backend.py b/src/backend/chat/agent_rag/document_rag_backends/find_rag_backend.py index e028890..9dcc753 100644 --- a/src/backend/chat/agent_rag/document_rag_backends/find_rag_backend.py +++ b/src/backend/chat/agent_rag/document_rag_backends/find_rag_backend.py @@ -56,14 +56,14 @@ class FindRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-attri self.collection_id = self.collection_id or 1 return self.collection_id - #TODO + # TODO def delete_collection(self) -> None: """ Deletion not available """ - logger.warning(f"deletion of collections is not yet supported in FindRagBackend") + logger.warning("deletion of collections is not yet supported in FindRagBackend") - #TODO: factor with albert api + # TODO: factor with albert api def parse_pdf_document(self, name: str, content_type: str, content: BytesIO) -> str: """ Parse the PDF document content and return the text content. @@ -91,7 +91,7 @@ class FindRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-attri document_page["content"] for document_page in response.json().get("data", []) ) - #TODO: factor with albert api + # TODO: factor with albert api def parse_document(self, name: str, content_type: str, content: BytesIO): """ Parse the document and prepare it for the search operation. @@ -142,7 +142,7 @@ class FindRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-attri "updated_at": timezone.now().isoformat(), "tags": [f"collection-{self.collection_id}"], "size": len(content.encode("utf-8")), - "users": [], #TODO + "users": [], # TODO "groups": [], "reach": "public", "is_active": True, @@ -165,12 +165,11 @@ class FindRagBackend(BaseRagBackend): # pylint: disable=too-many-instance-attri RAGWebResults: The search results. """ logger.debug("search documents in Find with query '%s'", query) - #TODO: factor session auth in a decorator - session = kwargs.get("session") - refresh_access_token(session) - oidc_access_token = session.get('oidc_access_token') + # TODO: factor session auth in a decorator + session = refresh_access_token(kwargs.get("session")) + oidc_access_token = session.get("oidc_access_token") if not oidc_access_token: - raise AuthenticationError({'error': 'Not authenticated'}) + raise AuthenticationError({"error": "Not authenticated"}) collection_ids = self.get_all_collection_ids() response = requests.post( diff --git a/src/backend/chat/clients/pydantic_ai.py b/src/backend/chat/clients/pydantic_ai.py index 32c1484..1bf9634 100644 --- a/src/backend/chat/clients/pydantic_ai.py +++ b/src/backend/chat/clients/pydantic_ai.py @@ -106,7 +106,14 @@ def get_model_configuration(model_hrid: str): class AIAgentService: # pylint: disable=too-many-instance-attributes """Service class for AI-related operations (Pydantic-AI edition).""" - def __init__(self, conversation: models.ChatConversation, user, session=None, model_hrid=None, language=None): + def __init__( # noqa: PLR0913 # pylint: disable=too-many-arguments,too-many-positional-arguments + self, + conversation: models.ChatConversation, + user, + session=None, + model_hrid=None, + language=None, + ): """ Initialize the AI agent service. diff --git a/src/backend/chat/tests/tools/test_web_search_brave.py b/src/backend/chat/tests/tools/test_web_search_brave.py index dc45761..d5a8f29 100644 --- a/src/backend/chat/tests/tools/test_web_search_brave.py +++ b/src/backend/chat/tests/tools/test_web_search_brave.py @@ -38,9 +38,6 @@ def brave_settings(settings): settings.BRAVE_SEARCH_EXTRA_SNIPPETS = True settings.BRAVE_SUMMARIZATION_ENABLED = False settings.BRAVE_CACHE_TTL = 3600 - settings.RAG_DOCUMENT_SEARCH_BACKEND = ( - "chat.agent_rag.document_rag_backends.albert_rag_backend.AlbertRagBackend" - ) settings.BRAVE_RAG_WEB_SEARCH_CHUNK_NUMBER = 5 diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py index eea77fa..71c6b76 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation_with_document_upload.py @@ -7,6 +7,7 @@ import json import logging from io import BytesIO from unittest import mock +from unittest.mock import Mock from django.utils import formats, timezone @@ -41,28 +42,59 @@ from chat.tests.utils import replace_uuids_with_placeholder pytestmark = pytest.mark.django_db(transaction=True) -@pytest.fixture(autouse=True) -def ai_settings(settings): +@pytest.fixture( + autouse=True, + params=[ + "chat.agent_rag.document_rag_backends.find_rag_backend.FindRagBackend", + "chat.agent_rag.document_rag_backends.albert_rag_backend.AlbertRagBackend", + ], +) +def ai_settings(request, settings): """Fixture to set AI service URLs for testing.""" - settings.AI_BASE_URL = "https://www.external-ai-service.com/" - settings.AI_API_KEY = "test-api-key" - settings.AI_MODEL = "test-model" - settings.AI_AGENT_INSTRUCTIONS = "You are a helpful test assistant :)" - # Enable Albert API for document search - settings.RAG_DOCUMENT_SEARCH_BACKEND = ( - "chat.agent_rag.document_rag_backends.albert_rag_backend.AlbertRagBackend" - ) - settings.ALBERT_API_URL = "https://albert.api.etalab.gouv.fr" - settings.ALBERT_API_KEY = "albert-api-key" + # enable on rag document search tool + settings.RAG_DOCUMENT_SEARCH_BACKEND = request.param settings.RAG_WEB_SEARCH_PROMPT_UPDATE = ( "Based on the following document contents:\n\n{search_results}\n\n" "Please answer the user's question: {user_prompt}" ) + settings.AI_BASE_URL = "https://www.external-ai-service.com/" + settings.AI_API_KEY = "test-api-key" + settings.AI_MODEL = "test-model" + settings.AI_AGENT_INSTRUCTIONS = "You are a helpful test assistant :)" + + # Albert API settings + settings.ALBERT_API_URL = "https://albert.api.etalab.gouv.fr" + settings.ALBERT_API_KEY = "albert-api-key" + + # Find API settings + settings.FIND_API_URL = "https://find.api.example.com" + settings.FIND_API_KEY = "find-api-key" + return settings +@pytest.fixture(autouse=True) +def mock_process_request(): + """Mock process_request to bypass authentication in tests.""" + with mock.patch( + "lasuite.oidc_login.decorators.RefreshOIDCAccessToken.process_request" + ) as mocked_process_request: + mocked_process_request.return_value = None + yield mocked_process_request + + +@pytest.fixture(autouse=True) +def mock_refresh_access_token(): + """Mock refresh_access_token to bypass token refresh in tests.""" + with mock.patch("utils.oicd.refresh_access_token") as mocked_refresh_access_token: + mock_session = Mock(spec=httpx.Client) + mock_session.access_token = "mocked-access-token" + mocked_refresh_access_token.return_value = mock_session + yield mocked_refresh_access_token + + @pytest.fixture(name="sample_pdf_content") def fixture_sample_pdf_content(): """Create a dummy PDF content as BytesIO.""" @@ -134,6 +166,32 @@ def fixture_mock_albert_api(): ) +@pytest.fixture(name="mock_find_api") +def fixture_mock_find_api(): + """Fixture to mock the Find API endpoints.""" + # Mock document indexing (Find API) + responses.post( + "https://find.api.example.com/api/v1.0/documents/index/", + json={"id": "456", "status": "indexed"}, + status=status.HTTP_200_OK, + ) + + # Mock document search (Find API) + responses.post( + "https://find.api.example.com/api/v1.0/documents/search/", + json=[ + { + "_source": { + "title.fr": "sample.pdf", + "content.fr": "This is the content of the PDF.", + }, + "_score": 0.9, + } + ], + status=status.HTTP_200_OK, + ) + + @pytest.fixture(name="mock_summarization_agent") def fixture_mock_summarization_agent(): """Mock the SummarizationAgent to return a fixed summary.""" @@ -220,6 +278,7 @@ def test_post_conversation_with_document_upload( # pylint: disable=too-many-arguments,too-many-positional-arguments api_client, mock_albert_api, # pylint: disable=unused-argument + mock_find_api, # pylint: disable=unused-argument sample_pdf_content, today_promt_date, mock_ai_agent_service, @@ -549,6 +608,7 @@ def test_post_conversation_with_document_upload_feature_disabled( def test_post_conversation_with_document_upload_summarize( # pylint: disable=too-many-arguments,too-many-positional-arguments # noqa: PLR0913 api_client, mock_albert_api, # pylint: disable=unused-argument + mock_find_api, # pylint: disable=unused-argument sample_pdf_content, today_promt_date, mock_ai_agent_service, diff --git a/src/backend/chat/views.py b/src/backend/chat/views.py index 3a93735..5370de7 100644 --- a/src/backend/chat/views.py +++ b/src/backend/chat/views.py @@ -7,13 +7,12 @@ from uuid import uuid4 from django.conf import settings from django.core.files.storage import default_storage from django.http import Http404, StreamingHttpResponse +from django.utils.decorators import method_decorator import langfuse import magic import posthog -from django.utils.decorators import method_decorator from lasuite.malware_detection import malware_detection -from lasuite.oidc_login.backends import get_oidc_refresh_token from lasuite.oidc_login.decorators import refresh_oidc_access_token from rest_framework import decorators, filters, mixins, permissions, status, viewsets from rest_framework.exceptions import MethodNotAllowed, PermissionDenied, ValidationError diff --git a/src/backend/utils/oicd.py b/src/backend/utils/oicd.py index 1f602e6..05bd0dc 100644 --- a/src/backend/utils/oicd.py +++ b/src/backend/utils/oicd.py @@ -1,5 +1,8 @@ -import requests +"""Utility functions for OIDC token management.""" + from django.conf import settings + +import requests from lasuite.oidc_login.backends import get_oidc_refresh_token, store_tokens @@ -7,13 +10,13 @@ def refresh_access_token(session): """Refresh the OIDC access token using the refresh token.""" response = requests.post( settings.OIDC_OP_TOKEN_ENDPOINT, - data= { + data={ "grant_type": "refresh_token", "client_id": settings.OIDC_RP_CLIENT_ID, "client_secret": settings.OIDC_RP_CLIENT_SECRET, "refresh_token": get_oidc_refresh_token(session), }, - timeout=5 + timeout=5, ) response.raise_for_status() token_info = response.json() @@ -22,5 +25,6 @@ def refresh_access_token(session): session, access_token=token_info.get("access_token"), id_token=None, - refresh_token=token_info.get("refresh_token") + refresh_token=token_info.get("refresh_token"), ) + return session