From 8c62f5d9332c17722ab6e50d3cea67d2b064cb05 Mon Sep 17 00:00:00 2001 From: Quentin BEY Date: Mon, 2 Feb 2026 22:13:14 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(find)=20allow=20the=20chat=20compl?= =?UTF-8?q?etion=20view=20w/o=20access=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Find requires access and refresh tokens, but the current beahavior using Albert API does not. We need to be able to still use the completion endpoint without any stored token. --- src/backend/chat/tests/conftest.py | 38 +++++++ src/backend/chat/tests/views/chat/conftest.py | 17 --- .../chat/conversations/test_conversation.py | 104 ++++++++++++++++++ src/backend/chat/views.py | 15 ++- src/backend/conversations/settings.py | 18 +++ 5 files changed, 174 insertions(+), 18 deletions(-) delete mode 100644 src/backend/chat/tests/views/chat/conftest.py diff --git a/src/backend/chat/tests/conftest.py b/src/backend/chat/tests/conftest.py index 93107f0..0d2bbc0 100644 --- a/src/backend/chat/tests/conftest.py +++ b/src/backend/chat/tests/conftest.py @@ -2,12 +2,18 @@ import logging from contextlib import ExitStack, contextmanager +from importlib import reload from unittest.mock import patch +from django.urls import clear_url_caches, set_urlconf from django.utils import formats, timezone import pytest +import core.urls + +import chat.views +import conversations.urls from chat.agents.summarize import SummarizationAgent from chat.clients.pydantic_ai import AIAgentService @@ -90,3 +96,35 @@ PIXEL_PNG = ( b"\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\xf8\xff\xff?\x00\x05\xfe\x02\xfe" b"\xa7V\xbd\xfa\x00\x00\x00\x00IEND\xaeB`\x82" ) + + +@pytest.fixture(name="oidc_refresh_token_enabled") +def fixture_oidc_refresh_token_enabled(settings): + """ + Fixture to enable OIDC refresh token storage during the test. + + This is not a nice fixture, as it forces reloading views and URL configurations. + Maybe there is a better way to do this, because even the conditional_refresh_oidc_token + function is not nice, but I don't want it to be lazy either. + """ + __initial_value = bool(settings.OIDC_STORE_REFRESH_TOKEN) + + settings.OIDC_STORE_REFRESH_TOKEN = True + + # force view reload + reload(chat.views) + reload(core.urls) + reload(conversations.urls) + clear_url_caches() + set_urlconf(None) + + yield settings + + settings.OIDC_STORE_REFRESH_TOKEN = __initial_value + + # force view reload + reload(chat.views) + reload(core.urls) + reload(conversations.urls) + clear_url_caches() + set_urlconf(None) diff --git a/src/backend/chat/tests/views/chat/conftest.py b/src/backend/chat/tests/views/chat/conftest.py deleted file mode 100644 index d4be3b6..0000000 --- a/src/backend/chat/tests/views/chat/conftest.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Common test fixtures for chat views tests.""" - -from unittest import mock - -import pytest - - -@pytest.fixture(autouse=True) -def mock_process_request(): - """ - Mock process_request to bypass OIDC 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 diff --git a/src/backend/chat/tests/views/chat/conversations/test_conversation.py b/src/backend/chat/tests/views/chat/conversations/test_conversation.py index 0c779c5..27378d3 100644 --- a/src/backend/chat/tests/views/chat/conversations/test_conversation.py +++ b/src/backend/chat/tests/views/chat/conversations/test_conversation.py @@ -3,6 +3,7 @@ import json import logging +import time from unittest.mock import ANY, patch from django.utils import timezone @@ -1612,3 +1613,106 @@ async def test_post_conversation_async_triggers_keepalive( "run_id": _run_id, }, ] + + +def test_post_conversation_oidc_refresh_enabled_unrefreshed( # pylint: disable=unused-argument + api_client, oidc_refresh_token_enabled +): + """Test posting messages to a conversation without fresh access token should be forbidden.""" + chat_conversation = ChatConversationFactory(owner__language="en-us") + + url = f"/api/v1.0/chats/{chat_conversation.pk}/conversation/?protocol=data" + data = { + "messages": [ + { + "id": "yuPoOuBkKA4FnKvk", + "role": "user", + "parts": [{"text": "Hello", "type": "text"}], + "content": "Hello", + "createdAt": "2025-07-03T15:22:17.105Z", + } + ] + } + api_client.force_login( + chat_conversation.owner, backend="core.authentication.backends.OIDCAuthenticationBackend" + ) + + response = api_client.post(url, data, format="json") + + assert response.status_code == status.HTTP_401_UNAUTHORIZED + + +@freeze_time("2025-07-25T10:36:35.297675Z") +@respx.mock +def test_post_conversation_oidc_refresh_enabled( # pylint: disable=unused-argument + api_client, mock_openai_stream, oidc_refresh_token_enabled +): + """Test posting messages to a conversation using the 'data' protocol.""" + chat_conversation = ChatConversationFactory(owner__language="en-us") + + url = f"/api/v1.0/chats/{chat_conversation.pk}/conversation/?protocol=data" + data = { + "messages": [ + { + "id": "yuPoOuBkKA4FnKvk", + "role": "user", + "parts": [{"text": "Hello", "type": "text"}], + "content": "Hello", + "createdAt": "2025-07-03T15:22:17.105Z", + } + ] + } + api_client.force_login( + chat_conversation.owner, backend="core.authentication.backends.OIDCAuthenticationBackend" + ) + session = api_client.session + + session["oidc_id_token_expiration"] = time.time() + 3600 # valid for 1 hour + session["oidc_token_expiration"] = session["oidc_id_token_expiration"] # ... + session.save() + + response = api_client.post(url, data, format="json") + + assert response.status_code == status.HTTP_200_OK + assert response.get("Content-Type") == "text/event-stream" + assert response.get("x-vercel-ai-data-stream") == "v1" + assert response.streaming + + # Wait for the streaming content to be fully received + response_content = b"".join(response.streaming_content).decode("utf-8") + + # Replace UUIDs with placeholders for assertion + response_content = replace_uuids_with_placeholder(response_content) + + assert response_content == ( + '0:"Hello"\n' + '0:" there"\n' + 'f:{"messageId":""}\n' + 'd:{"finishReason":"stop","usage":{"promptTokens":0,"completionTokens":0}}\n' + ) + + assert mock_openai_stream.called + + # ensure instructions are merged as a system prompt + last_request_payload = json.loads(respx.calls.last.request.content) + assert last_request_payload["messages"][0] == { + "content": ( + "You are a helpful test assistant :)\n\nToday is Friday 25/07/2025.\n\n" + "Answer in english." + ), + "role": "system", + } + + chat_conversation.refresh_from_db() + assert chat_conversation.ui_messages == [ + { + "content": "Hello", + "createdAt": "2025-07-03T15:22:17.105Z", + "id": "yuPoOuBkKA4FnKvk", + "parts": [{"text": "Hello", "type": "text"}], + "role": "user", + } + ] + + assert len(chat_conversation.messages) == 2 + assert len(chat_conversation.pydantic_messages) == 2 diff --git a/src/backend/chat/views.py b/src/backend/chat/views.py index e2ec47d..c9f63c1 100644 --- a/src/backend/chat/views.py +++ b/src/backend/chat/views.py @@ -37,6 +37,19 @@ from chat.serializers import ChatConversationRequestSerializer logger = logging.getLogger(__name__) +def conditional_refresh_oidc_token(func): + """ + Conditionally apply refresh_oidc_access_token decorator. + + The decorator is only applied if OIDC_STORE_REFRESH_TOKEN is True, meaning + we can actually refresh something. Broader settings checks are done in settings.py. + """ + if settings.OIDC_STORE_REFRESH_TOKEN: + return method_decorator(refresh_oidc_access_token)(func) + + return func + + class ChatConversationFilter(filters.BaseFilterBackend): """Filter conversation.""" @@ -128,7 +141,7 @@ class ChatViewSet( # pylint: disable=too-many-ancestors, abstract-method self.permission_classes = [] return super().get_permissions() - @method_decorator(refresh_oidc_access_token) + @conditional_refresh_oidc_token @decorators.action( methods=["post"], detail=True, diff --git a/src/backend/conversations/settings.py b/src/backend/conversations/settings.py index c416f59..5eb49b7 100755 --- a/src/backend/conversations/settings.py +++ b/src/backend/conversations/settings.py @@ -1096,6 +1096,24 @@ USER QUESTION: "Please set FILE_BACKEND_URL to a valid URL for backend temporary file access." ) + # Find configuration + if ( + cls.RAG_DOCUMENT_SEARCH_BACKEND + == "chat.agent_rag.document_rag_backends.find_rag_backend.FindRagBackend" + and not all( + ( + cls.FIND_API_KEY, + cls.FIND_API_URL, + cls.OIDC_STORE_ACCESS_TOKEN, + cls.OIDC_STORE_REFRESH_TOKEN, + ) + ) + ): + raise ValueError( + f"{cls.RAG_DOCUMENT_SEARCH_BACKEND} requires FIND_API_KEY, FIND_API_URL, " + "OIDC_STORE_ACCESS_TOKEN and OIDC_STORE_REFRESH_TOKEN to be set." + ) + # Langfuse initialization if cls.LANGFUSE_ENABLED: if not cls.LANGFUSE_MEDIA_UPLOAD_ENABLED: