From c0ecf53fbf3366c19773b88d41f8c5763a2e101e Mon Sep 17 00:00:00 2001 From: charles Date: Fri, 20 Mar 2026 11:00:44 +0100 Subject: [PATCH] =?UTF-8?q?fixup!=20=F0=9F=9A=A8(backend)=20fix=20search?= =?UTF-8?q?=20without=20refresh=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: charles --- CHANGELOG.md | 1 + src/backend/core/api/utils.py | 2 +- src/backend/core/api/viewsets.py | 12 +-- src/backend/core/tests/conftest.py | 4 +- .../documents/test_api_documents_search.py | 1 - .../test_api_documents_search_feature_flag.py | 1 - .../test_api_utils_refresh_access_token.py | 82 +++++++++++++++++++ 7 files changed, 88 insertions(+), 15 deletions(-) create mode 100644 src/backend/core/tests/test_api_utils_refresh_access_token.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a68854e9..20ec2c594 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to - ♿️(frontend) fix waffle aria-label spacing for new-window links #2030 - 🐛(backend) stop using add_sibling method to create sandbox document #2084 - 🐛(backend) duplicate a document as last-sibling #2084 +- 🐛(backend) fix search without refresh token #2090 ### Removed diff --git a/src/backend/core/api/utils.py b/src/backend/core/api/utils.py index bd185da40..8afdf57cd 100644 --- a/src/backend/core/api/utils.py +++ b/src/backend/core/api/utils.py @@ -198,7 +198,7 @@ def refresh_access_token(session): "client_secret": settings.OIDC_RP_CLIENT_SECRET, "refresh_token": refresh_token, }, - timeout=5, + timeout=settings.OIDC_TIMEOUT, ) response.raise_for_status() token_info = response.json() diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 87e08b070..f138f5743 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -1429,15 +1429,6 @@ class DocumentViewSet( if search_type == SearchType.TITLE: return self._title_search(request, params.validated_data, *args, **kwargs) - try: - request.session = refresh_access_token(request.session) - except AuthenticationFailed: - logging.warning( - "User unauthenticated or error while refreshing token, " - "falling back to title search." - ) - return self._title_search(request, params.validated_data, *args, **kwargs) - indexer = get_document_indexer() if indexer is None: # fallback on title search if the indexer is not configured @@ -1447,7 +1438,7 @@ class DocumentViewSet( return self._search_with_indexer( indexer, request, params=params, search_type=search_type ) - except requests.exceptions.RequestException as e: + except (requests.exceptions.RequestException, AuthenticationFailed) as e: logger.error( "Error while searching documents with indexer \n%s \nfall back on title search", e, @@ -1473,6 +1464,7 @@ class DocumentViewSet( """ Returns a list of documents matching the query (q) according to the configured indexer. """ + request.session = refresh_access_token(request.session) queryset = models.Document.objects.all() results = indexer.search( diff --git a/src/backend/core/tests/conftest.py b/src/backend/core/tests/conftest.py index 5782a752f..0f9e6568d 100644 --- a/src/backend/core/tests/conftest.py +++ b/src/backend/core/tests/conftest.py @@ -147,8 +147,8 @@ def user_token(): return build_authorization_bearer("some_token") -@pytest.fixture(name="oidc_settings") -def fixture_oidc_settings(settings): +@pytest.fixture +def oidc_settings(settings): """Fixture to configure OIDC settings for the tests.""" settings.OIDC_OP_TOKEN_ENDPOINT = "https://auth.example.com/token" settings.OIDC_OP_AUTHORIZATION_ENDPOINT = "https://auth.example.com/authorize" diff --git a/src/backend/core/tests/documents/test_api_documents_search.py b/src/backend/core/tests/documents/test_api_documents_search.py index c60640378..2997311fd 100644 --- a/src/backend/core/tests/documents/test_api_documents_search.py +++ b/src/backend/core/tests/documents/test_api_documents_search.py @@ -2,7 +2,6 @@ Tests for Documents API endpoint in impress's core app: search """ -import re from unittest import mock from unittest.mock import patch diff --git a/src/backend/core/tests/documents/test_api_documents_search_feature_flag.py b/src/backend/core/tests/documents/test_api_documents_search_feature_flag.py index 6fc8c2dbe..505e370bf 100644 --- a/src/backend/core/tests/documents/test_api_documents_search_feature_flag.py +++ b/src/backend/core/tests/documents/test_api_documents_search_feature_flag.py @@ -4,7 +4,6 @@ Tests for Find search feature flags from unittest import mock -from django.contrib.sessions.backends.cache import SessionStore from django.http import HttpResponse import pytest diff --git a/src/backend/core/tests/test_api_utils_refresh_access_token.py b/src/backend/core/tests/test_api_utils_refresh_access_token.py new file mode 100644 index 000000000..61e7ac795 --- /dev/null +++ b/src/backend/core/tests/test_api_utils_refresh_access_token.py @@ -0,0 +1,82 @@ +"""Unit tests for the refresh_access_token utility function.""" + +import pytest +import responses +from cryptography.fernet import Fernet +from lasuite.oidc_login.backends import get_oidc_refresh_token, store_tokens +from requests import HTTPError +from rest_framework.exceptions import AuthenticationFailed + +from core.api.utils import refresh_access_token + +pytestmark = pytest.mark.django_db + + +@pytest.fixture(name="mock_oidc_settings") +def mock_oidc_settings_fixture(settings): + """Fixture to mock OIDC settings.""" + settings.OIDC_OP_TOKEN_ENDPOINT = "https://example.com/token" + settings.OIDC_RP_CLIENT_ID = "test-client-id" + settings.OIDC_RP_CLIENT_SECRET = "test-client-secret" + settings.OIDC_STORE_REFRESH_TOKEN = True + settings.OIDC_STORE_REFRESH_TOKEN_KEY = Fernet.generate_key() + yield settings + + +@responses.activate +def test_refresh_access_token_success(mock_oidc_settings): # pylint: disable=unused-argument + """Test successful token refresh.""" + session = {} + store_tokens( + session, + access_token="old-access-token", + id_token=None, + refresh_token="valid-refresh-token", + ) + + responses.add( + responses.POST, + "https://example.com/token", + json={ + "access_token": "new-access-token", + "refresh_token": "new-refresh-token", + }, + status=200, + ) + + result = refresh_access_token(session) + + assert result == session + assert get_oidc_refresh_token(session) == "new-refresh-token" + + +def test_refresh_access_token_missing_refresh_token(mock_oidc_settings): # pylint: disable=unused-argument + """Test that AuthenticationFailed is raised when refresh token is missing.""" + session = {} + + with pytest.raises(AuthenticationFailed) as exc_info: + refresh_access_token(session) + + assert exc_info.value.detail == {"error": "Refresh token is missing from session"} + + +@responses.activate +def test_refresh_access_token_http_error(mock_oidc_settings): # pylint: disable=unused-argument + """Test that HTTP errors are propagated when token endpoint fails.""" + session = {} + store_tokens( + session, + access_token="old-access-token", + id_token=None, + refresh_token="valid-refresh-token", + ) + + responses.add( + responses.POST, + "https://example.com/token", + json={"error": "invalid_grant"}, + status=401, + ) + + with pytest.raises(HTTPError): + refresh_access_token(session)