From 9a8f952210bd9ce1b939b6b33eb3ff55b1a9eca9 Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Fri, 4 Jul 2025 10:56:24 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A9(back)=20use=20existing=20no=20webs?= =?UTF-8?q?ocket=20feature=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An already existing feature flag COLLABORATION_WS_NOT_CONNECTED_READY_ONLY was used bu the frontend application to disable or not the edition for a user not connected to the websocket. We want to reuse it in the backend application to disable or not the no websocket feature. --- env.d/development/common.dist | 1 + src/backend/core/api/viewsets.py | 14 ++++-- .../documents/test_api_documents_can_edit.py | 40 ++++++++++++++--- .../documents/test_api_documents_update.py | 44 +++++++++++++++++++ .../apps/e2e/__tests__/app-impress/common.ts | 2 +- 5 files changed, 90 insertions(+), 11 deletions(-) diff --git a/env.d/development/common.dist b/env.d/development/common.dist index b1c44194..a0cf0fe5 100644 --- a/env.d/development/common.dist +++ b/env.d/development/common.dist @@ -60,6 +60,7 @@ COLLABORATION_API_URL=http://y-provider-development:4444/collaboration/api/ COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000 COLLABORATION_SERVER_ORIGIN=http://localhost:3000 COLLABORATION_SERVER_SECRET=my-secret +COLLABORATION_WS_NOT_CONNECTED_READY_ONLY=true COLLABORATION_WS_URL=ws://localhost:4444/collaboration/ws/ DJANGO_SERVER_TO_SERVER_API_TOKENS=server-api-token diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 03777ad6..03c1752c 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -32,7 +32,6 @@ from rest_framework import filters, status, viewsets from rest_framework import response as drf_response from rest_framework.permissions import AllowAny from rest_framework.throttling import UserRateThrottle -from sentry_sdk import capture_exception from core import authentication, enums, models from core.services.ai_services import AIService @@ -682,7 +681,10 @@ class DocumentViewSet( def perform_update(self, serializer): """Check rules about collaboration.""" - if serializer.validated_data.get("websocket", False): + if ( + serializer.validated_data.get("websocket", False) + or not settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY + ): return super().perform_update(serializer) if self._can_user_edit_document(serializer.instance.id, set_cache=True): @@ -701,10 +703,14 @@ class DocumentViewSet( """Check if the current user can edit the document.""" document = self.get_object() - return drf.response.Response( - {"can_edit": self._can_user_edit_document(document.id)} + can_edit = ( + True + if not settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY + else self._can_user_edit_document(document.id) ) + return drf.response.Response({"can_edit": can_edit}) + @drf.decorators.action( detail=False, methods=["get"], diff --git a/src/backend/core/tests/documents/test_api_documents_can_edit.py b/src/backend/core/tests/documents/test_api_documents_can_edit.py index 83695873..7db0a13c 100644 --- a/src/backend/core/tests/documents/test_api_documents_can_edit.py +++ b/src/backend/core/tests/documents/test_api_documents_can_edit.py @@ -11,16 +11,38 @@ from core import factories pytestmark = pytest.mark.django_db -def test_api_documents_can_edit_anonymous(): - """Anonymous users can not edit documents.""" - document = factories.DocumentFactory() +@responses.activate +@pytest.mark.parametrize("ws_not_connected_ready_only", [True, False]) +@pytest.mark.parametrize("role", ["editor", "reader"]) +def test_api_documents_can_edit_anonymous(settings, ws_not_connected_ready_only, role): + """Anonymous users can edit documents when link_role is editor.""" + document = factories.DocumentFactory(link_reach="public", link_role=role) client = APIClient() + session_key = client.session.session_key + settings.COLLABORATION_API_URL = "http://example.com/" + settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = ws_not_connected_ready_only + endpoint_url = ( + f"{settings.COLLABORATION_API_URL}get-connections/" + f"?room={document.id}&sessionKey={session_key}" + ) + ws_resp = responses.get(endpoint_url, json={"count": 0, "exists": False}) + response = client.get(f"/api/v1.0/documents/{document.id!s}/can-edit/") - assert response.status_code == 401 + + if role == "reader": + assert response.status_code == 401 + else: + assert response.status_code == 200 + assert response.json() == {"can_edit": True} + assert ws_resp.call_count == (1 if ws_not_connected_ready_only else 0) @responses.activate -def test_api_documents_can_edit_authenticated_no_websocket(settings): +@pytest.mark.parametrize("ws_not_connected_ready_only", [True, False]) +def test_api_documents_can_edit_authenticated_no_websocket( + settings, ws_not_connected_ready_only +): """ A user not connected to the websocket and no other user have already updated the document, the document can be updated. @@ -34,6 +56,7 @@ def test_api_documents_can_edit_authenticated_no_websocket(settings): settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = ws_not_connected_ready_only endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -49,7 +72,7 @@ def test_api_documents_can_edit_authenticated_no_websocket(settings): assert response.status_code == 200 assert response.json() == {"can_edit": True} - assert ws_resp.call_count == 1 + assert ws_resp.call_count == (1 if ws_not_connected_ready_only else 0) @responses.activate @@ -69,6 +92,7 @@ def test_api_documents_can_edit_authenticated_no_websocket_user_already_editing( settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -103,6 +127,7 @@ def test_api_documents_can_edit_no_websocket_other_user_connected_to_websocket( settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -134,6 +159,7 @@ def test_api_documents_can_edit_user_connected_to_websocket(settings): settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -168,6 +194,7 @@ def test_api_documents_can_edit_websocket_server_unreachable_fallback_to_no_webs settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -202,6 +229,7 @@ def test_api_documents_can_edit_websocket_server_unreachable_fallback_to_no_webs settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" diff --git a/src/backend/core/tests/documents/test_api_documents_update.py b/src/backend/core/tests/documents/test_api_documents_update.py index b24b8e73..03b1891b 100644 --- a/src/backend/core/tests/documents/test_api_documents_update.py +++ b/src/backend/core/tests/documents/test_api_documents_update.py @@ -313,6 +313,7 @@ def test_api_documents_update_authenticated_no_websocket(settings): new_document_values["websocket"] = False settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -352,6 +353,7 @@ def test_api_documents_update_authenticated_no_websocket_user_already_editing(se new_document_values["websocket"] = False settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -390,6 +392,7 @@ def test_api_documents_update_no_websocket_other_user_connected_to_websocket(set new_document_values["websocket"] = False settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -427,6 +430,7 @@ def test_api_documents_update_user_connected_to_websocket(settings): new_document_values["websocket"] = False settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -466,6 +470,7 @@ def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websoc new_document_values["websocket"] = False settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -506,6 +511,7 @@ def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websoc new_document_values["websocket"] = False settings.COLLABORATION_API_URL = "http://example.com/" settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = True endpoint_url = ( f"{settings.COLLABORATION_API_URL}get-connections/" f"?room={document.id}&sessionKey={session_key}" @@ -562,6 +568,44 @@ def test_api_documents_update_force_websocket_param_to_true(settings): assert ws_resp.call_count == 0 +@responses.activate +def test_api_documents_update_feature_flag_disabled(settings): + """ + When the feature flag is disabled, the document should be updated without any check. + """ + user = factories.UserFactory(with_owned_document=True) + client = APIClient() + client.force_login(user) + session_key = client.session.session_key + + document = factories.DocumentFactory(users=[(user, "editor")]) + + new_document_values = serializers.DocumentSerializer( + instance=factories.DocumentFactory() + ).data + new_document_values["websocket"] = False + settings.COLLABORATION_API_URL = "http://example.com/" + settings.COLLABORATION_SERVER_SECRET = "secret-token" + settings.COLLABORATION_WS_NOT_CONNECTED_READY_ONLY = False + endpoint_url = ( + f"{settings.COLLABORATION_API_URL}get-connections/" + f"?room={document.id}&sessionKey={session_key}" + ) + ws_resp = responses.get(endpoint_url, status=500) + + assert cache.get(f"docs:no-websocket:{document.id}") is None + + response = client.put( + f"/api/v1.0/documents/{document.id!s}/", + new_document_values, + format="json", + ) + assert response.status_code == 200 + + assert cache.get(f"docs:no-websocket:{document.id}") is None + assert ws_resp.call_count == 0 + + @pytest.mark.parametrize("via", VIA) def test_api_documents_update_administrator_or_owner_of_another(via, mock_user_teams): """ diff --git a/src/frontend/apps/e2e/__tests__/app-impress/common.ts b/src/frontend/apps/e2e/__tests__/app-impress/common.ts index c91c42d7..fede1a9b 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/common.ts @@ -6,7 +6,7 @@ export const CONFIG = { AI_FEATURE_ENABLED: true, CRISP_WEBSITE_ID: null, COLLABORATION_WS_URL: 'ws://localhost:4444/collaboration/ws/', - COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: false, + COLLABORATION_WS_NOT_CONNECTED_READY_ONLY: true, ENVIRONMENT: 'development', FRONTEND_CSS_URL: null, FRONTEND_HOMEPAGE_FEATURE_ENABLED: true,