From 94a1ba7989cc13a096816a799f7b1c4240266053 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 28 Nov 2024 16:35:48 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20notify=20collaboration=20s?= =?UTF-8?q?erver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When an access is updated or removed, the collaboration server is notified to reset the access connection; by being disconnected, the accesses will automatically reconnect by passing by the ngnix subrequest, and so get the good rights. We do the same system when the document link is updated, except here we reset every access connection. --- docker/files/etc/nginx/conf.d/default.conf | 6 + env.d/development/common.dist | 3 +- src/backend/core/api/viewsets.py | 27 +++ .../core/services/collaboration_services.py | 42 ++++ .../documents/test_api_document_accesses.py | 151 +++++++++----- .../test_api_documents_link_configuration.py | 30 +-- .../test_services_collaboration_services.py | 185 ++++++++++++++++++ src/backend/impress/settings.py | 3 + .../e2e/__tests__/app-impress/config.spec.ts | 6 +- .../__tests__/app-impress/doc-editor.spec.ts | 57 +++++- src/helm/env.d/dev/values.impress.yaml.gotmpl | 1 + 11 files changed, 436 insertions(+), 75 deletions(-) create mode 100644 src/backend/core/services/collaboration_services.py create mode 100644 src/backend/core/tests/test_services_collaboration_services.py diff --git a/docker/files/etc/nginx/conf.d/default.conf b/docker/files/etc/nginx/conf.d/default.conf index b984787f..2644822c 100644 --- a/docker/files/etc/nginx/conf.d/default.conf +++ b/docker/files/etc/nginx/conf.d/default.conf @@ -46,6 +46,12 @@ server { proxy_set_header X-Original-Method $request_method; } + location /collaboration/api/ { + # Collaboration server + proxy_pass http://y-provider:4444; + proxy_set_header Host $host; + } + # Proxy auth for media location /media/ { # Auth request configuration diff --git a/env.d/development/common.dist b/env.d/development/common.dist index ed183578..29570b4a 100644 --- a/env.d/development/common.dist +++ b/env.d/development/common.dist @@ -53,9 +53,10 @@ AI_API_KEY=password AI_MODEL=llama # Collaboration +COLLABORATION_API_URL=http://nginx:8083/collaboration/api/ COLLABORATION_SERVER_ORIGIN=http://localhost:3000 COLLABORATION_SERVER_SECRET=my-secret -COLLABORATION_WS_URL=ws://localhost:8083/collaboration/ws +COLLABORATION_WS_URL=ws://localhost:8083/collaboration/ws/ # Frontend FRONTEND_THEME=dsfr diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 969d1861..4c71689c 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -30,6 +30,7 @@ from rest_framework.permissions import AllowAny from core import enums, models from core.services.ai_services import AIService +from core.services.collaboration_services import CollaborationService from . import permissions, serializers, utils from .filters import DocumentFilter @@ -520,6 +521,10 @@ class DocumentViewSet( serializer.is_valid(raise_exception=True) serializer.save() + + # Notify collaboration server about the link updated + CollaborationService().reset_connections(str(document.id)) + return drf.response.Response(serializer.data, status=drf.status.HTTP_200_OK) @drf.decorators.action(detail=True, methods=["post", "delete"], url_path="favorite") @@ -815,6 +820,28 @@ class DocumentAccessViewSet( self.request.user, ) + def perform_update(self, serializer): + """Update an access to the document and notify the collaboration server.""" + access = serializer.save() + + access_user_id = None + if access.user: + access_user_id = str(access.user.id) + + # Notify collaboration server about the access change + CollaborationService().reset_connections( + str(access.document.id), access_user_id + ) + + def perform_destroy(self, instance): + """Delete an access to the document and notify the collaboration server.""" + instance.delete() + + # Notify collaboration server about the access removed + CollaborationService().reset_connections( + str(instance.document.id), str(instance.user.id) + ) + class TemplateViewSet( drf.mixins.CreateModelMixin, diff --git a/src/backend/core/services/collaboration_services.py b/src/backend/core/services/collaboration_services.py new file mode 100644 index 00000000..9120321d --- /dev/null +++ b/src/backend/core/services/collaboration_services.py @@ -0,0 +1,42 @@ +"""Collaboration services.""" + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +import requests + + +class CollaborationService: + """Service class for Collaboration related operations.""" + + def __init__(self): + """Ensure that the collaboration configuration is set properly.""" + if settings.COLLABORATION_API_URL is None: + raise ImproperlyConfigured("Collaboration configuration not set") + + def reset_connections(self, room, user_id=None): + """ + Reset connections of a room in the collaboration server. + Reseting a connection means that the user will be disconnected and will + have to reconnect to the collaboration server, with updated rights. + """ + endpoint = "reset-connections" + + # room is necessary as a parameter, it is easier to stick to the + # same pod thanks to a parameter + endpoint_url = f"{settings.COLLABORATION_API_URL}{endpoint}/?room={room}" + + headers = {"Authorization": settings.COLLABORATION_SERVER_SECRET} + if user_id: + headers["X-User-Id"] = user_id + + try: + response = requests.post(endpoint_url, headers=headers, timeout=10) + except requests.RequestException as e: + raise requests.HTTPError("Failed to notify WebSocket server.") from e + + if response.status_code != 200: + raise requests.HTTPError( + f"Failed to notify WebSocket server. Status code: {response.status_code}, " + f"Response: {response.text}" + ) diff --git a/src/backend/core/tests/documents/test_api_document_accesses.py b/src/backend/core/tests/documents/test_api_document_accesses.py index 9d04d924..5b1ea283 100644 --- a/src/backend/core/tests/documents/test_api_document_accesses.py +++ b/src/backend/core/tests/documents/test_api_document_accesses.py @@ -11,6 +11,9 @@ from rest_framework.test import APIClient from core import factories, models from core.api import serializers from core.tests.conftest import TEAM, USER, VIA +from core.tests.test_services_collaboration_services import ( # pylint: disable=unused-import + mock_reset_connections, +) pytestmark = pytest.mark.django_db @@ -316,7 +319,11 @@ def test_api_document_accesses_update_authenticated_reader_or_editor( @pytest.mark.parametrize("via", VIA) -def test_api_document_accesses_update_administrator_except_owner(via, mock_user_teams): +def test_api_document_accesses_update_administrator_except_owner( + via, + mock_user_teams, + mock_reset_connections, # pylint: disable=redefined-outer-name +): """ A user who is a direct administrator in a document should be allowed to update a user access for this document, as long as they don't try to set the role to owner. @@ -351,18 +358,21 @@ def test_api_document_accesses_update_administrator_except_owner(via, mock_user_ for field, value in new_values.items(): new_data = {**old_values, field: value} - response = client.put( - f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", - data=new_data, - format="json", - ) - - if ( - new_data["role"] == old_values["role"] - ): # we are not really updating the role + if new_data["role"] == old_values["role"]: + response = client.put( + f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", + data=new_data, + format="json", + ) assert response.status_code == 403 else: - assert response.status_code == 200 + with mock_reset_connections(document.id, str(access.user_id)): + response = client.put( + f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", + data=new_data, + format="json", + ) + assert response.status_code == 200 access.refresh_from_db() updated_values = serializers.DocumentAccessSerializer(instance=access).data @@ -420,7 +430,11 @@ def test_api_document_accesses_update_administrator_from_owner(via, mock_user_te @pytest.mark.parametrize("via", VIA) -def test_api_document_accesses_update_administrator_to_owner(via, mock_user_teams): +def test_api_document_accesses_update_administrator_to_owner( + via, + mock_user_teams, + mock_reset_connections, # pylint: disable=redefined-outer-name +): """ A user who is an administrator in a document, should not be allowed to update the user access of another user to grant document ownership. @@ -457,16 +471,23 @@ def test_api_document_accesses_update_administrator_to_owner(via, mock_user_team for field, value in new_values.items(): new_data = {**old_values, field: value} - response = client.put( - f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", - data=new_data, - format="json", - ) # We are not allowed or not really updating the role if field == "role" or new_data["role"] == old_values["role"]: + response = client.put( + f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", + data=new_data, + format="json", + ) + assert response.status_code == 403 else: - assert response.status_code == 200 + with mock_reset_connections(document.id, str(access.user_id)): + response = client.put( + f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", + data=new_data, + format="json", + ) + assert response.status_code == 200 access.refresh_from_db() updated_values = serializers.DocumentAccessSerializer(instance=access).data @@ -474,7 +495,11 @@ def test_api_document_accesses_update_administrator_to_owner(via, mock_user_team @pytest.mark.parametrize("via", VIA) -def test_api_document_accesses_update_owner(via, mock_user_teams): +def test_api_document_accesses_update_owner( + via, + mock_user_teams, + mock_reset_connections, # pylint: disable=redefined-outer-name +): """ A user who is an owner in a document should be allowed to update a user access for this document whatever the role. @@ -507,18 +532,24 @@ def test_api_document_accesses_update_owner(via, mock_user_teams): for field, value in new_values.items(): new_data = {**old_values, field: value} - response = client.put( - f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", - data=new_data, - format="json", - ) - if ( new_data["role"] == old_values["role"] ): # we are not really updating the role + response = client.put( + f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", + data=new_data, + format="json", + ) assert response.status_code == 403 else: - assert response.status_code == 200 + with mock_reset_connections(document.id, str(access.user_id)): + response = client.put( + f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", + data=new_data, + format="json", + ) + + assert response.status_code == 200 access.refresh_from_db() updated_values = serializers.DocumentAccessSerializer(instance=access).data @@ -530,7 +561,11 @@ def test_api_document_accesses_update_owner(via, mock_user_teams): @pytest.mark.parametrize("via", VIA) -def test_api_document_accesses_update_owner_self(via, mock_user_teams): +def test_api_document_accesses_update_owner_self( + via, + mock_user_teams, + mock_reset_connections, # pylint: disable=redefined-outer-name +): """ A user who is owner of a document should be allowed to update their own user access provided there are other owners in the document. @@ -568,21 +603,23 @@ def test_api_document_accesses_update_owner_self(via, mock_user_teams): # Add another owner and it should now work factories.UserDocumentAccessFactory(document=document, role="owner") - response = client.put( - f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", - data={ - **old_values, - "role": new_role, - "user_id": old_values.get("user", {}).get("id") - if old_values.get("user") is not None - else None, - }, - format="json", - ) + user_id = str(access.user_id) if via == USER else None + with mock_reset_connections(document.id, user_id): + response = client.put( + f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", + data={ + **old_values, + "role": new_role, + "user_id": old_values.get("user", {}).get("id") + if old_values.get("user") is not None + else None, + }, + format="json", + ) - assert response.status_code == 200 - access.refresh_from_db() - assert access.role == new_role + assert response.status_code == 200 + access.refresh_from_db() + assert access.role == new_role # Delete @@ -656,7 +693,9 @@ def test_api_document_accesses_delete_reader_or_editor(via, role, mock_user_team @pytest.mark.parametrize("via", VIA) def test_api_document_accesses_delete_administrators_except_owners( - via, mock_user_teams + via, + mock_user_teams, + mock_reset_connections, # pylint: disable=redefined-outer-name ): """ Users who are administrators in a document should be allowed to delete an access @@ -685,12 +724,13 @@ def test_api_document_accesses_delete_administrators_except_owners( assert models.DocumentAccess.objects.count() == 2 assert models.DocumentAccess.objects.filter(user=access.user).exists() - response = client.delete( - f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", - ) + with mock_reset_connections(document.id, str(access.user_id)): + response = client.delete( + f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", + ) - assert response.status_code == 204 - assert models.DocumentAccess.objects.count() == 1 + assert response.status_code == 204 + assert models.DocumentAccess.objects.count() == 1 @pytest.mark.parametrize("via", VIA) @@ -729,7 +769,11 @@ def test_api_document_accesses_delete_administrator_on_owners(via, mock_user_tea @pytest.mark.parametrize("via", VIA) -def test_api_document_accesses_delete_owners(via, mock_user_teams): +def test_api_document_accesses_delete_owners( + via, + mock_user_teams, + mock_reset_connections, # pylint: disable=redefined-outer-name +): """ Users should be able to delete the document access of another user for a document of which they are owner. @@ -753,12 +797,13 @@ def test_api_document_accesses_delete_owners(via, mock_user_teams): assert models.DocumentAccess.objects.count() == 2 assert models.DocumentAccess.objects.filter(user=access.user).exists() - response = client.delete( - f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", - ) + with mock_reset_connections(document.id, str(access.user_id)): + response = client.delete( + f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/", + ) - assert response.status_code == 204 - assert models.DocumentAccess.objects.count() == 1 + assert response.status_code == 204 + assert models.DocumentAccess.objects.count() == 1 @pytest.mark.parametrize("via", VIA) diff --git a/src/backend/core/tests/documents/test_api_documents_link_configuration.py b/src/backend/core/tests/documents/test_api_documents_link_configuration.py index 91f4d7e6..76838805 100644 --- a/src/backend/core/tests/documents/test_api_documents_link_configuration.py +++ b/src/backend/core/tests/documents/test_api_documents_link_configuration.py @@ -6,6 +6,9 @@ from rest_framework.test import APIClient from core import factories, models from core.api import serializers from core.tests.conftest import TEAM, USER, VIA +from core.tests.test_services_collaboration_services import ( # pylint: disable=unused-import + mock_reset_connections, +) pytestmark = pytest.mark.django_db @@ -116,7 +119,10 @@ def test_api_documents_link_configuration_update_authenticated_related_forbidden @pytest.mark.parametrize("role", ["administrator", "owner"]) @pytest.mark.parametrize("via", VIA) def test_api_documents_link_configuration_update_authenticated_related_success( - via, role, mock_user_teams + via, + role, + mock_user_teams, + mock_reset_connections, # pylint: disable=redefined-outer-name ): """ A user who is administrator or owner of a document should be allowed to update @@ -139,14 +145,16 @@ def test_api_documents_link_configuration_update_authenticated_related_success( new_document_values = serializers.LinkDocumentSerializer( instance=factories.DocumentFactory() ).data - response = client.put( - f"/api/v1.0/documents/{document.id!s}/link-configuration/", - new_document_values, - format="json", - ) - assert response.status_code == 200 - document = models.Document.objects.get(pk=document.pk) - document_values = serializers.LinkDocumentSerializer(instance=document).data - for key, value in document_values.items(): - assert value == new_document_values[key] + with mock_reset_connections(document.id): + response = client.put( + f"/api/v1.0/documents/{document.id!s}/link-configuration/", + new_document_values, + format="json", + ) + assert response.status_code == 200 + + document = models.Document.objects.get(pk=document.pk) + document_values = serializers.LinkDocumentSerializer(instance=document).data + for key, value in document_values.items(): + assert value == new_document_values[key] diff --git a/src/backend/core/tests/test_services_collaboration_services.py b/src/backend/core/tests/test_services_collaboration_services.py new file mode 100644 index 00000000..7d02e252 --- /dev/null +++ b/src/backend/core/tests/test_services_collaboration_services.py @@ -0,0 +1,185 @@ +""" +This module contains tests for the CollaborationService class in the +core.services.collaboration_services module. +""" + +import json +import re +from contextlib import contextmanager + +from django.core.exceptions import ImproperlyConfigured + +import pytest +import requests +import responses + +from core.services.collaboration_services import CollaborationService + + +@pytest.fixture +def mock_reset_connections(settings): + """ + Creates a context manager to mock the reset-connections endpoint for collaboration services. + Args: + settings: A settings object that contains the configuration for the collaboration API. + Returns: + A context manager function that mocks the reset-connections endpoint. + The context manager function takes the following parameters: + document_id (str): The ID of the document for which connections are being reset. + user_id (str, optional): The ID of the user making the request. Defaults to None. + Usage: + with mock_reset_connections(settings)(document_id, user_id) as mock: + # Your test code here + The context manager performs the following actions: + - Mocks the reset-connections endpoint using responses.RequestsMock. + - Sets the COLLABORATION_API_URL and COLLABORATION_SERVER_SECRET in the settings. + - Verifies that the reset-connections endpoint is called exactly once. + - Checks that the request URL and headers are correct. + - If user_id is provided, checks that the X-User-Id header is correct. + """ + + @contextmanager + def _mock_reset_connections(document_id, user_id=None): + with responses.RequestsMock() as rsps: + # Mock the reset-connections endpoint + settings.COLLABORATION_API_URL = "http://example.com/" + settings.COLLABORATION_SERVER_SECRET = "secret-token" + endpoint_url = ( + f"{settings.COLLABORATION_API_URL}reset-connections/?room={document_id}" + ) + rsps.add( + responses.POST, + endpoint_url, + json={}, + status=200, + ) + yield + + assert ( + len(rsps.calls) == 1 + ), "Expected one call to reset-connections endpoint" + request = rsps.calls[0].request + assert request.url == endpoint_url, f"Unexpected URL called: {request.url}" + assert ( + request.headers.get("Authorization") + == settings.COLLABORATION_SERVER_SECRET + ), "Incorrect Authorization header" + + if user_id: + assert ( + request.headers.get("X-User-Id") == user_id + ), "Incorrect X-User-Id header" + + return _mock_reset_connections + + +def test_init_without_api_url(settings): + """Test that ImproperlyConfigured is raised when COLLABORATION_API_URL is None.""" + settings.COLLABORATION_API_URL = None + with pytest.raises(ImproperlyConfigured): + CollaborationService() + + +def test_init_with_api_url(settings): + """Test that the service initializes correctly when COLLABORATION_API_URL is set.""" + settings.COLLABORATION_API_URL = "http://example.com/" + service = CollaborationService() + assert isinstance(service, CollaborationService) + + +@responses.activate +def test_reset_connections_with_user_id(settings): + """Test reset_connections with a provided user_id.""" + settings.COLLABORATION_API_URL = "http://example.com/" + settings.COLLABORATION_SERVER_SECRET = "secret-token" + service = CollaborationService() + + room = "room1" + user_id = "user123" + endpoint_url = "http://example.com/reset-connections/?room=" + room + + responses.add(responses.POST, endpoint_url, json={}, status=200) + + service.reset_connections(room, user_id) + + assert len(responses.calls) == 1 + request = responses.calls[0].request + + assert request.url == endpoint_url + assert request.headers.get("Authorization") == "secret-token" + assert request.headers.get("X-User-Id") == "user123" + + +@responses.activate +def test_reset_connections_without_user_id(settings): + """Test reset_connections without a user_id.""" + settings.COLLABORATION_API_URL = "http://example.com/" + settings.COLLABORATION_SERVER_SECRET = "secret-token" + service = CollaborationService() + + room = "room1" + user_id = None + endpoint_url = "http://example.com/reset-connections/?room=" + room + + responses.add( + responses.POST, + endpoint_url, + json={}, + status=200, + ) + + service.reset_connections(room, user_id) + + assert len(responses.calls) == 1 + request = responses.calls[0].request + + assert request.url == endpoint_url + assert request.headers.get("Authorization") == "secret-token" + assert request.headers.get("X-User-Id") is None + + +@responses.activate +def test_reset_connections_non_200_response(settings): + """Test that an HTTPError is raised when the response status is not 200.""" + settings.COLLABORATION_API_URL = "http://example.com/" + settings.COLLABORATION_SERVER_SECRET = "secret-token" + service = CollaborationService() + + room = "room1" + user_id = "user123" + endpoint_url = "http://example.com/reset-connections/?room=" + room + response_body = {"error": "Internal Server Error"} + + responses.add(responses.POST, endpoint_url, json=response_body, status=500) + + expected_exception_message = re.escape( + "Failed to notify WebSocket server. Status code: 500, Response: " + ) + re.escape(json.dumps(response_body)) + + with pytest.raises(requests.HTTPError, match=expected_exception_message): + service.reset_connections(room, user_id) + + assert len(responses.calls) == 1 + + +@responses.activate +def test_reset_connections_request_exception(settings): + """Test that an HTTPError is raised when a RequestException occurs.""" + settings.COLLABORATION_API_URL = "http://example.com/" + settings.COLLABORATION_SERVER_SECRET = "secret-token" + service = CollaborationService() + + room = "room1" + user_id = "user123" + endpoint_url = "http://example.com/reset-connections?room=" + room + + responses.add( + responses.POST, + endpoint_url, + body=requests.exceptions.ConnectionError("Network error"), + ) + + with pytest.raises(requests.HTTPError, match="Failed to notify WebSocket server."): + service.reset_connections(room, user_id) + + assert len(responses.calls) == 1 diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 4f65a60f..0aa608e5 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -372,6 +372,9 @@ class Base(Configuration): SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN", environ_prefix=None) # Collaboration + COLLABORATION_API_URL = values.Value( + None, environ_name="COLLABORATION_API_URL", environ_prefix=None + ) COLLABORATION_SERVER_SECRET = values.Value( None, environ_name="COLLABORATION_SERVER_SECRET", environ_prefix=None ) diff --git a/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts index 1d8604df..16f1ffdb 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/config.spec.ts @@ -6,7 +6,7 @@ import { createDoc } from './common'; const config = { CRISP_WEBSITE_ID: null, - COLLABORATION_WS_URL: 'ws://localhost:4444', + COLLABORATION_WS_URL: 'ws://localhost:8083/collaboration/ws/', ENVIRONMENT: 'development', FRONTEND_THEME: 'dsfr', MEDIA_BASE_URL: 'http://localhost:8083', @@ -117,7 +117,7 @@ test.describe('Config', () => { browserName, }) => { const webSocketPromise = page.waitForEvent('websocket', (webSocket) => { - return webSocket.url().includes('ws://localhost:4444/'); + return webSocket.url().includes('ws://localhost:8083/collaboration/ws/'); }); await page.goto('/'); @@ -131,7 +131,7 @@ test.describe('Config', () => { await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible(); const webSocket = await webSocketPromise; - expect(webSocket.url()).toContain('ws://localhost:4444/'); + expect(webSocket.url()).toContain('ws://localhost:8083/collaboration/ws/'); }); test('it checks that Crisp is trying to init from config endpoint', async ({ diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts index d41660a5..221b0800 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-editor.spec.ts @@ -81,26 +81,69 @@ test.describe('Doc Editor', () => { ).toBeVisible(); }); - test('checks the Doc is connected to the provider server', async ({ + /** + * We check: + * - connection to the collaborative server + * - signal of the backend to the collaborative server (connection should close) + * - reconnection to the collaborative server + */ + test('checks the connection with collaborative server', async ({ page, browserName, }) => { - const webSocketPromise = page.waitForEvent('websocket', (webSocket) => { - return webSocket.url().includes('ws://localhost:4444/'); + let webSocketPromise = page.waitForEvent('websocket', (webSocket) => { + return webSocket + .url() + .includes('ws://localhost:8083/collaboration/ws/?room='); }); const randomDoc = await createDoc(page, 'doc-editor', browserName, 1); await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible(); - const webSocket = await webSocketPromise; - expect(webSocket.url()).toContain('ws://localhost:4444/'); + let webSocket = await webSocketPromise; + expect(webSocket.url()).toContain( + 'ws://localhost:8083/collaboration/ws/?room=', + ); - const framesentPromise = webSocket.waitForEvent('framesent'); + // Is connected + let framesentPromise = webSocket.waitForEvent('framesent'); await page.locator('.ProseMirror.bn-editor').click(); await page.locator('.ProseMirror.bn-editor').fill('Hello World'); - const framesent = await framesentPromise; + let framesent = await framesentPromise; + expect(framesent.payload).not.toBeNull(); + + await page.getByRole('button', { name: 'Share' }).click(); + + const selectVisibility = page.getByRole('combobox', { + name: 'Visibility', + }); + + // When the visibility is changed, the ws should closed the connection (backend signal) + const wsClosePromise = webSocket.waitForEvent('close'); + + await selectVisibility.click(); + await page + .getByRole('option', { + name: 'Authenticated', + }) + .click(); + + // Assert that the doc reconnects to the ws + const wsClose = await wsClosePromise; + expect(wsClose.isClosed()).toBeTruthy(); + + // Checkt the ws is connected again + webSocketPromise = page.waitForEvent('websocket', (webSocket) => { + return webSocket + .url() + .includes('ws://localhost:8083/collaboration/ws/?room='); + }); + + webSocket = await webSocketPromise; + framesentPromise = webSocket.waitForEvent('framesent'); + framesent = await framesentPromise; expect(framesent.payload).not.toBeNull(); }); diff --git a/src/helm/env.d/dev/values.impress.yaml.gotmpl b/src/helm/env.d/dev/values.impress.yaml.gotmpl index e7e1bca5..c49c39d5 100644 --- a/src/helm/env.d/dev/values.impress.yaml.gotmpl +++ b/src/helm/env.d/dev/values.impress.yaml.gotmpl @@ -6,6 +6,7 @@ image: backend: replicas: 1 envVars: + COLLABORATION_API_URL: https://impress.127.0.0.1.nip.io/collaboration/api/ COLLABORATION_SERVER_SECRET: my-secret DJANGO_CSRF_TRUSTED_ORIGINS: https://impress.127.0.0.1.nip.io,http://impress.127.0.0.1.nip.io DJANGO_CONFIGURATION: Feature