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