🐛(find) allow the chat completion view w/o access token

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.
This commit is contained in:
Quentin BEY
2026-02-02 22:13:14 +01:00
parent 88bdcc2e60
commit 8c62f5d933
5 changed files with 174 additions and 18 deletions
+38
View File
@@ -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)
@@ -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
@@ -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":"<mocked_uuid>"}\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
+14 -1
View File
@@ -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,
+18
View File
@@ -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: