mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-03 04:28:44 +02:00
🔥(backend) remove CollaborationService and can-edit endpoint
The CollaborationService was doing nothing since we started the migration to yhub, all the code using it is now removed. Also the `can-edit` endpoint and all the safeguard mechanism relying on the presence of other users connected to the websocket will not be used anymore, it will be possible to replace all of this with yhub, so all this code is also removed.
This commit is contained in:
@@ -85,6 +85,8 @@ and this project adheres to
|
||||
the dev stack gains dedicated valkey and postgres services for yhub, and
|
||||
the kick (reset-connections) and get-connections APIs have no yhub
|
||||
equivalent yet — they are deferred with TODO(yhub) stubs
|
||||
- 🔥(backend) remove the unused `CollaborationService`
|
||||
- 💥(backend) remove the `documents/{id}/can-edit/` endpoint
|
||||
- 💥(y-provider) the published `lasuite/impress-y-provider` image becomes
|
||||
converter-only and no longer serves `/collaboration/ws/`; deployments using
|
||||
the existing helm values lose collaboration until the helm chart routes
|
||||
|
||||
@@ -1,36 +1,13 @@
|
||||
# Collaboration
|
||||
|
||||
By default with Docs, collaboration is enabled. To allow the collaboration between users, a connection to a websocket server is made (the y-provider service), you only have to configure the Django backend URL in your y-provider service:
|
||||
By default with Docs, collaboration is enabled. To allow the collaboration between users, a connection to a websocket server is made (the y-provider service), you only have to configure the Django backend URL and the allowed origin in your y-provider service:
|
||||
|
||||
```yaml
|
||||
COLLABORATION_BACKEND_BASE_URL: https://{yourdocsdomain.tld}
|
||||
COLLABORATION_SERVER_ORIGIN: https://{yourdocsdomain.tld}
|
||||
```
|
||||
|
||||
An advanced configuration can be used in some cases when your users are not allowed to use websocket on their network.
|
||||
|
||||
## What happens when connection to the websocket is not allowed?
|
||||
|
||||
When multiple users access a Docs and the connection to the websocket is not allowed, then they will be in a situation where they can lose data.
|
||||
They will lose data because they will erase each other modifications. You can also have a scenario with a mix of users connected to the websocket and some other not.
|
||||
|
||||
## Safeguard configuration
|
||||
|
||||
We have imagined a safeguard scenario, not enabled by default.
|
||||
The idea is to give the priority to users connected to the websocket. While there is at least one user connected to the websocket, all other users not connected to the websocket can access the Docs in **read-only** mode.
|
||||
|
||||
To enable this safeguard, the Django application will have to fetch the `y-provider` service to retrieve some information in it.
|
||||
|
||||
In the Django configuration, you have to set these environment variables:
|
||||
|
||||
```yaml
|
||||
COLLABORATION_WS_NOT_CONNECTED_READ_ONLY: True
|
||||
COLLABORATION_API_URL: https://{yourdocsdomain.tld}/collaboration/api/
|
||||
COLLABORATION_SERVER_SECRET: A-shared-secret-with-y-provider-service
|
||||
```
|
||||
|
||||
In the y-provider service, you have to set these environment variables:
|
||||
|
||||
```yaml
|
||||
COLLABORATION_SERVER_SECRET: A-shared-secret-with-y-provider-service
|
||||
COLLABORATION_SERVER_ORIGIN: https://{yourdocsdomain.tld}
|
||||
```
|
||||
|
||||
@@ -32,10 +32,7 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| AWS_STORAGE_BUCKET_NAME | Bucket name for s3 endpoint | impress-media-storage |
|
||||
| CACHES_DEFAULT_TIMEOUT | Cache default timeout | 30 |
|
||||
| CACHES_DEFAULT_KEY_PREFIX | The prefix used to every cache keys. | docs |
|
||||
| COLLABORATION_API_URL | Collaboration api host | |
|
||||
| COLLABORATION_SERVER_SECRET | Collaboration api secret | |
|
||||
| COLLABORATION_WS_INACTIVITY_TIMEOUT | Timeout (in seconds) after which the user is considered inactive when there is no activity. The WebSocket is closed after this inactivity period. `None` means disabled. | None |
|
||||
| COLLABORATION_WS_NOT_CONNECTED_READ_ONLY | Users not connected to the collaboration server cannot edit | false |
|
||||
| COLLABORATION_WS_URL | Collaboration websocket url | |
|
||||
| CONVERSION_API_CONTENT_FIELD | Conversion api content field | content |
|
||||
| CONVERSION_API_ENDPOINT | Conversion API endpoint | convert |
|
||||
@@ -98,7 +95,6 @@ These are the environment variables you can set for the `impress-backend` contai
|
||||
| MALWARE_DETECTION_PARAMETERS | A dict containing all the parameters to initiate the malware detection backend | {"callback_path": "core.malware_detection.malware_detection_callback",} |
|
||||
| MEDIA_BASE_URL | | |
|
||||
| MEDIA_AUTH_ORIGINAL_URL_HEADER | Parameter containing the original request URL, as seen at the media auth endpoint, in CGI/WSGI form (HTTP_HEADER_NAME_ALL_CAPS_WITH_UNDERSCORES) | HTTP_X_ORIGINAL_URL |
|
||||
| NO_WEBSOCKET_CACHE_TIMEOUT | Cache used to store current editor session key when only users without websocket are editing a document | 120 |
|
||||
| OIDC_ALLOW_DUPLICATE_EMAILS | Allow duplicate emails | false |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | OIDC extra auth parameters | {} |
|
||||
| OIDC_CREATE_USER | Create used on OIDC | false |
|
||||
|
||||
@@ -15,7 +15,6 @@ image:
|
||||
backend:
|
||||
replicas: 1
|
||||
envVars:
|
||||
COLLABORATION_SERVER_SECRET: my-secret
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://docs.127.0.0.1.nip.io
|
||||
DJANGO_CONFIGURATION: Feature
|
||||
DJANGO_ALLOWED_HOSTS: docs.127.0.0.1.nip.io
|
||||
|
||||
@@ -81,7 +81,6 @@ USER_RECONCILIATION_FORM_URL=http://localhost:3000
|
||||
COLLABORATION_BACKEND_BASE_URL=http://app-dev:8000
|
||||
COLLABORATION_SERVER_ORIGIN=http://localhost:3000
|
||||
COLLABORATION_SERVER_SECRET=my-secret
|
||||
COLLABORATION_WS_NOT_CONNECTED_READ_ONLY=true
|
||||
COLLABORATION_WS_URL=ws://localhost:3002/ws/docs
|
||||
COLLABORATION_WS_INACTIVITY_TIMEOUT=15 # Seconds
|
||||
|
||||
|
||||
@@ -2,6 +2,5 @@ Y_PROVIDER_API_BASE_URL=http://${YPROVIDER_HOST}:4444/api/
|
||||
Y_PROVIDER_API_KEY=<generate a random key>
|
||||
COLLABORATION_SERVER_SECRET=<generate a random key>
|
||||
COLLABORATION_SERVER_ORIGIN=https://${DOCS_HOST}
|
||||
COLLABORATION_API_URL=https://${DOCS_HOST}/collaboration/api/
|
||||
COLLABORATION_BACKEND_BASE_URL=https://${DOCS_HOST}
|
||||
COLLABORATION_LOGGING=true
|
||||
@@ -180,7 +180,6 @@ class DocumentLightSerializer(serializers.ModelSerializer):
|
||||
class DocumentSerializer(ListDocumentSerializer):
|
||||
"""Serialize documents with all fields for display in detail views."""
|
||||
|
||||
websocket = serializers.BooleanField(required=False, write_only=True)
|
||||
file = serializers.FileField(
|
||||
required=False, write_only=True, allow_null=True, max_length=255
|
||||
)
|
||||
@@ -210,7 +209,6 @@ class DocumentSerializer(ListDocumentSerializer):
|
||||
"title",
|
||||
"updated_at",
|
||||
"user_role",
|
||||
"websocket",
|
||||
]
|
||||
read_only_fields = [
|
||||
"id",
|
||||
@@ -312,7 +310,6 @@ class DocumentContentSerializer(serializers.Serializer):
|
||||
"""Serializer for updating only the raw content of a document stored in S3."""
|
||||
|
||||
content = serializers.CharField(required=True)
|
||||
websocket = serializers.BooleanField(required=False)
|
||||
|
||||
def validate_content(self, value):
|
||||
"""Validate the content field."""
|
||||
|
||||
@@ -53,7 +53,6 @@ from core.api.filters import remove_accents
|
||||
from core.services import mime_types
|
||||
from core.services.ai_services.blocknote import AIService
|
||||
from core.services.ai_services.legacy import get_legacy_ai_service
|
||||
from core.services.collaboration_services import CollaborationService
|
||||
from core.services.converter_services import (
|
||||
ConversionError,
|
||||
Converter,
|
||||
@@ -72,7 +71,6 @@ from core.services.search_indexers import (
|
||||
get_document_indexer,
|
||||
get_visited_document_ids_of,
|
||||
)
|
||||
from core.tasks.access import reset_service_connections_in_cascade
|
||||
from core.tasks.mail import send_ask_for_access_mail
|
||||
from core.utils.analytics import PosthogEventName, posthog_capture
|
||||
from core.utils.dicts import lowercase_keys
|
||||
@@ -763,81 +761,6 @@ class DocumentViewSet(
|
||||
PosthogEventName.DOC_DELETED, self.request.user, {}, document=instance
|
||||
)
|
||||
|
||||
def _can_user_edit_document(self, document_id, set_cache=False):
|
||||
"""Check if the user can edit the document."""
|
||||
try:
|
||||
count, exists = CollaborationService().get_document_connection_info(
|
||||
document_id,
|
||||
self.request.session.session_key,
|
||||
)
|
||||
except requests.HTTPError as e:
|
||||
logger.exception("Failed to call collaboration server: %s", e)
|
||||
count = 0
|
||||
exists = False
|
||||
|
||||
if count == 0:
|
||||
# Nobody is connected to the websocket server
|
||||
logger.debug("update without connection found in the websocket server")
|
||||
cache_key = f"docs:no-websocket:{document_id}"
|
||||
current_editor = cache.get(cache_key)
|
||||
|
||||
if not current_editor:
|
||||
if set_cache:
|
||||
cache.set(
|
||||
cache_key,
|
||||
self.request.session.session_key,
|
||||
settings.NO_WEBSOCKET_CACHE_TIMEOUT,
|
||||
)
|
||||
return True
|
||||
|
||||
if current_editor != self.request.session.session_key:
|
||||
return False
|
||||
|
||||
if set_cache:
|
||||
cache.touch(cache_key, settings.NO_WEBSOCKET_CACHE_TIMEOUT)
|
||||
return True
|
||||
|
||||
if exists:
|
||||
# Current user is connected to the websocket server
|
||||
logger.debug("session key found in the websocket server")
|
||||
return True
|
||||
|
||||
logger.debug(
|
||||
"Users connected to the websocket but current editor not connected to it. Can not edit."
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Check rules about collaboration."""
|
||||
if (
|
||||
not serializer.validated_data.get("websocket", False)
|
||||
and settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY
|
||||
and not self._can_user_edit_document(serializer.instance.id, set_cache=True)
|
||||
):
|
||||
raise drf.exceptions.PermissionDenied(
|
||||
"You are not allowed to edit this document."
|
||||
)
|
||||
|
||||
return super().perform_update(serializer)
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=True,
|
||||
methods=["get"],
|
||||
url_path="can-edit",
|
||||
)
|
||||
def can_edit(self, request, *args, **kwargs):
|
||||
"""Check if the current user can edit the document."""
|
||||
document = self.get_object()
|
||||
|
||||
can_edit = (
|
||||
True
|
||||
if not settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY
|
||||
else self._can_user_edit_document(document.id)
|
||||
)
|
||||
|
||||
return drf.response.Response({"can_edit": can_edit})
|
||||
|
||||
@drf.decorators.action(
|
||||
detail=False,
|
||||
methods=["get"],
|
||||
@@ -1831,9 +1754,6 @@ class DocumentViewSet(
|
||||
|
||||
serializer.save()
|
||||
|
||||
# Notify collaboration server about the link updated
|
||||
reset_service_connections_in_cascade.delay(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")
|
||||
@@ -2081,15 +2001,6 @@ class DocumentViewSet(
|
||||
serializer = serializers.DocumentContentSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
if (
|
||||
not serializer.validated_data.get("websocket", False)
|
||||
and settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY
|
||||
and not self._can_user_edit_document(document.id, set_cache=True)
|
||||
):
|
||||
raise drf.exceptions.PermissionDenied(
|
||||
"You are not allowed to edit this document."
|
||||
)
|
||||
|
||||
content = serializer.validated_data["content"]
|
||||
try:
|
||||
extracted_attachments = set(extract_attachments(content))
|
||||
@@ -2856,26 +2767,12 @@ class DocumentAccessViewSet(
|
||||
or settings.LANGUAGE_CODE,
|
||||
)
|
||||
|
||||
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
|
||||
reset_service_connections_in_cascade.delay(
|
||||
str(access.document.id), access_user_id
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Delete an access to the document and notify the collaboration server."""
|
||||
"""Delete an access to the document."""
|
||||
# Snapshot the identifiers before deletion as Django resets the primary key
|
||||
# on the instance once it is deleted.
|
||||
access_id = str(instance.id)
|
||||
document_id = str(instance.document_id)
|
||||
user_id = str(instance.user.id)
|
||||
|
||||
instance.delete()
|
||||
|
||||
@@ -2885,9 +2782,6 @@ class DocumentAccessViewSet(
|
||||
{"access_id": access_id, "document_id": document_id},
|
||||
)
|
||||
|
||||
# Notify collaboration server about the access removed
|
||||
reset_service_connections_in_cascade.delay(document_id, user_id)
|
||||
|
||||
|
||||
class InvitationViewset(
|
||||
drf.mixins.CreateModelMixin,
|
||||
@@ -3105,7 +2999,6 @@ class ConfigView(drf.views.APIView):
|
||||
"AI_FEATURE_LEGACY_ENABLED",
|
||||
"API_USERS_SEARCH_QUERY_MIN_LENGTH",
|
||||
"COLLABORATION_WS_URL",
|
||||
"COLLABORATION_WS_NOT_CONNECTED_READ_ONLY",
|
||||
"COLLABORATION_WS_INACTIVITY_TIMEOUT",
|
||||
"CONVERSION_FILE_EXTENSIONS_ALLOWED",
|
||||
"CONVERSION_FILE_MAX_SIZE",
|
||||
|
||||
@@ -1392,7 +1392,6 @@ class Document(MP_Node, BaseModel):
|
||||
"ai_translate": ai_access,
|
||||
"attachment_upload": can_update,
|
||||
"media_check": can_get,
|
||||
"can_edit": can_update,
|
||||
"children_list": can_get,
|
||||
"children_create": can_create_children,
|
||||
"collaboration_auth": can_get,
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"""Collaboration services."""
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class CollaborationService:
|
||||
"""Service class for Collaboration related operations."""
|
||||
|
||||
def reset_connections(self, document_id, user_id=None):
|
||||
"""
|
||||
Reset the connections of a document and all its descendants in the
|
||||
collaboration server.
|
||||
|
||||
TODO(yhub): yhub exposes no kick API, so this is a no-op. The regression
|
||||
is stronger than losing the hocuspocus disconnect: a revoked user keeps
|
||||
their already-authorized websocket until it closes on its own, and the
|
||||
edits they push in the meantime are durably persisted and re-served by
|
||||
yhub (hocuspocus lost them with the room). Until yhub grows a kick API,
|
||||
the manual remediation is yhub's rollback endpoint — per document:
|
||||
`POST /rollback/{org}/{docid}` with a lib0-encoded body containing
|
||||
`{"by": "<userid>"}` (see yhub API.md "Rollback"), authenticated as a
|
||||
user with update ability on the document.
|
||||
"""
|
||||
logger.info(
|
||||
"reset_connections is a no-op (no yhub kick API), document %s, user %s",
|
||||
document_id,
|
||||
user_id,
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_document_connection_info(self, room, session_key):
|
||||
"""
|
||||
Get the connection info for a document.
|
||||
|
||||
TODO(yhub): yhub exposes no connection-info API, so pretend nobody is
|
||||
connected. Callers fall back to the cache-lock no-websocket path.
|
||||
"""
|
||||
return 0, False
|
||||
@@ -1,14 +0,0 @@
|
||||
"""Tasks dedicated to document's accesses."""
|
||||
|
||||
from core.services.collaboration_services import CollaborationService
|
||||
|
||||
from impress.celery_app import app
|
||||
|
||||
|
||||
@app.task
|
||||
def reset_service_connections_in_cascade(document_id, user_id=None):
|
||||
"""
|
||||
For a given document_id, reset the connections of the document and all its
|
||||
descendants by delegating to the CollaborationService.
|
||||
"""
|
||||
CollaborationService().reset_connections(document_id, user_id)
|
||||
@@ -4,7 +4,6 @@ Test document accesses API endpoints for users in impress's core app.
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import random
|
||||
from contextlib import contextmanager
|
||||
from unittest import mock
|
||||
from uuid import uuid4
|
||||
|
||||
@@ -19,25 +18,6 @@ from core.utils.analytics import PosthogEventName
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_reset_connections")
|
||||
def mock_reset_connections_fixture():
|
||||
"""
|
||||
Provide a context manager that patches the ``reset_service_connections_in_cascade``
|
||||
Celery task and asserts its ``delay`` method is called exactly once for the given
|
||||
document and user when leaving the context.
|
||||
"""
|
||||
|
||||
@contextmanager
|
||||
def _mock_reset_connections(document_id, user_id=None):
|
||||
with mock.patch(
|
||||
"core.api.viewsets.reset_service_connections_in_cascade.delay"
|
||||
) as mock_delay:
|
||||
yield mock_delay
|
||||
mock_delay.assert_called_once_with(str(document_id), user_id)
|
||||
|
||||
return _mock_reset_connections
|
||||
|
||||
|
||||
def test_api_document_accesses_list_anonymous():
|
||||
"""Anonymous users should not be allowed to list document accesses."""
|
||||
document = factories.DocumentFactory()
|
||||
@@ -754,7 +734,6 @@ def test_api_document_accesses_update_administrator_except_owner(
|
||||
create_for,
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections,
|
||||
):
|
||||
"""
|
||||
A user who is a direct administrator in a document should be allowed to update a user
|
||||
@@ -793,13 +772,12 @@ def test_api_document_accesses_update_administrator_except_owner(
|
||||
|
||||
for field, value in new_values.items():
|
||||
new_data = {**old_values, field: value}
|
||||
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
|
||||
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
|
||||
@@ -864,7 +842,6 @@ def test_api_document_accesses_update_administrator_from_owner(via, mock_user_te
|
||||
def test_api_document_accesses_update_administrator_to_owner(
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections,
|
||||
):
|
||||
"""
|
||||
A user who is an administrator in a document, should not be allowed to update
|
||||
@@ -912,13 +889,12 @@ def test_api_document_accesses_update_administrator_to_owner(
|
||||
|
||||
assert response.status_code == 403
|
||||
else:
|
||||
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
|
||||
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
|
||||
@@ -931,7 +907,6 @@ def test_api_document_accesses_update_owner(
|
||||
create_for,
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections,
|
||||
):
|
||||
"""
|
||||
A user who is an owner in a document should be allowed to update
|
||||
@@ -968,14 +943,13 @@ def test_api_document_accesses_update_owner(
|
||||
|
||||
for field, value in new_values.items():
|
||||
new_data = {**old_values, field: value}
|
||||
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",
|
||||
)
|
||||
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
|
||||
assert response.status_code == 200
|
||||
|
||||
access.refresh_from_db()
|
||||
updated_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||
@@ -994,7 +968,6 @@ def test_api_document_accesses_update_owner(
|
||||
def test_api_document_accesses_update_owner_self_root(
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections,
|
||||
):
|
||||
"""
|
||||
A user who is owner of a document should be allowed to update
|
||||
@@ -1033,30 +1006,27 @@ def test_api_document_accesses_update_owner_self_root(
|
||||
# Add another owner and it should now work
|
||||
factories.UserDocumentAccessFactory(document=document, role="owner")
|
||||
|
||||
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",
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_document_accesses_update_owner_self_child(
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections,
|
||||
):
|
||||
"""
|
||||
A user who is owner of a document should be allowed to update
|
||||
@@ -1084,13 +1054,11 @@ def test_api_document_accesses_update_owner_self_child(
|
||||
old_values = serializers.DocumentAccessSerializer(instance=access).data
|
||||
new_role = random.choice(["administrator", "editor", "reader"])
|
||||
|
||||
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},
|
||||
format="json",
|
||||
)
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
data={**old_values, "role": new_role},
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
access.refresh_from_db()
|
||||
@@ -1170,7 +1138,6 @@ def test_api_document_accesses_delete_reader_or_editor(via, role, mock_user_team
|
||||
def test_api_document_accesses_delete_administrators_except_owners(
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections,
|
||||
):
|
||||
"""
|
||||
Users who are administrators in a document should be allowed to delete an access
|
||||
@@ -1199,14 +1166,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()
|
||||
|
||||
with mock_reset_connections(document.id, str(access.user_id)):
|
||||
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
|
||||
response = client.delete(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
)
|
||||
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
|
||||
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
|
||||
|
||||
# The access deletion should be tracked in PostHog
|
||||
mock_capture.assert_called_once_with(
|
||||
@@ -1255,7 +1221,6 @@ def test_api_document_accesses_delete_administrator_on_owners(via, mock_user_tea
|
||||
def test_api_document_accesses_delete_owners(
|
||||
via,
|
||||
mock_user_teams,
|
||||
mock_reset_connections,
|
||||
):
|
||||
"""
|
||||
Users should be able to delete the document access of another user
|
||||
@@ -1280,11 +1245,10 @@ def test_api_document_accesses_delete_owners(
|
||||
assert models.DocumentAccess.objects.count() == 2
|
||||
assert models.DocumentAccess.objects.filter(user=access.user).exists()
|
||||
|
||||
with mock_reset_connections(document.id, str(access.user_id)):
|
||||
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
|
||||
response = client.delete(
|
||||
f"/api/v1.0/documents/{document.id!s}/accesses/{access.id!s}/",
|
||||
)
|
||||
with mock.patch("core.api.viewsets.posthog_capture") as mock_capture:
|
||||
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
|
||||
@@ -1327,9 +1291,7 @@ def test_api_document_accesses_delete_owners_last_owner_root(via, mock_user_team
|
||||
assert models.DocumentAccess.objects.count() == 2
|
||||
|
||||
|
||||
def test_api_document_accesses_delete_owners_last_owner_child_user(
|
||||
mock_reset_connections,
|
||||
):
|
||||
def test_api_document_accesses_delete_owners_last_owner_child_user():
|
||||
"""
|
||||
It should be possible to delete the last owner access from a document that is not a root.
|
||||
"""
|
||||
@@ -1345,10 +1307,9 @@ def test_api_document_accesses_delete_owners_last_owner_child_user(
|
||||
)
|
||||
|
||||
assert models.DocumentAccess.objects.count() == 2
|
||||
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}/",
|
||||
)
|
||||
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
|
||||
@@ -1359,7 +1320,6 @@ def test_api_document_accesses_delete_owners_last_owner_child_user(
|
||||
)
|
||||
def test_api_document_accesses_delete_owners_last_owner_child_team(
|
||||
mock_user_teams,
|
||||
mock_reset_connections,
|
||||
):
|
||||
"""
|
||||
It should be possible to delete the last owner access from a document that
|
||||
@@ -1378,10 +1338,9 @@ def test_api_document_accesses_delete_owners_last_owner_child_team(
|
||||
)
|
||||
|
||||
assert models.DocumentAccess.objects.count() == 2
|
||||
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}/",
|
||||
)
|
||||
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
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
"""Test the can_edit endpoint in the viewset DocumentViewSet."""
|
||||
|
||||
from django.core.cache import cache
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import factories
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@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()
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = ws_not_connected_ready_only
|
||||
|
||||
response = client.get(f"/api/v1.0/documents/{document.id!s}/can-edit/")
|
||||
|
||||
if role == "reader":
|
||||
assert response.status_code == 401
|
||||
else:
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": True}
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = ws_not_connected_ready_only
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
assert response.json() == {"can_edit": True}
|
||||
|
||||
|
||||
def test_api_documents_can_edit_authenticated_no_websocket_user_already_editing(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
A user not connected to the websocket and another user have already updated the document,
|
||||
the document can not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": False}
|
||||
|
||||
|
||||
# TODO(yhub): removed test_api_documents_can_edit_no_websocket_other_user_connected_to_websocket
|
||||
# here. yhub has no connection-info API: get_document_connection_info is stubbed to report
|
||||
# nobody connected, so another user connected to the websocket can no longer block edition.
|
||||
# Re-add the test once yhub exposes a connection-info API.
|
||||
|
||||
|
||||
def test_api_documents_can_edit_user_connected_to_websocket(settings):
|
||||
"""
|
||||
A user connected to the websocket, the document can be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": True}
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
|
||||
def test_api_documents_can_edit_websocket_server_unreachable_fallback_to_no_websocket(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the document can be updated like if the user was
|
||||
not connected to the websocket.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": True}
|
||||
|
||||
|
||||
def test_api_documents_can_edit_websocket_server_unreachable_fallback_to_no_websocket_other_users(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the behavior fallback to the no websocket one.
|
||||
If an other user is already editing, the document can not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": False}
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||
|
||||
|
||||
def test_api_documents_can_edit_websocket_server_room_not_found(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server returns a 404, the document can be updated like if the user was
|
||||
not connected to the websocket.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": True}
|
||||
|
||||
|
||||
def test_api_documents_can_edit_websocket_server_room_not_found_other_already_editing(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server returns a 404 and another user is editing the document,
|
||||
the response should be can-edit=False.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/documents/{document.id!s}/can-edit/",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"can_edit": False}
|
||||
@@ -6,7 +6,6 @@ import base64
|
||||
from functools import cache
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.cache import cache as django_cache
|
||||
from django.core.files.storage import default_storage
|
||||
|
||||
import pycrdt
|
||||
@@ -101,7 +100,7 @@ def test_api_documents_content_update_success(role, via, mock_user_teams):
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": get_sample_ydoc(), "websocket": True},
|
||||
{"content": get_sample_ydoc()},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
@@ -180,7 +179,7 @@ def test_api_documents_content_update_replaces_existing():
|
||||
new_content = get_sample_ydoc()
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": new_content, "websocket": True},
|
||||
{"content": new_content},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
@@ -245,7 +244,7 @@ def test_api_documents_content_update_link_editor():
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": get_sample_ydoc(), "websocket": True},
|
||||
{"content": get_sample_ydoc()},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
@@ -253,213 +252,6 @@ def test_api_documents_content_update_link_editor():
|
||||
assert models.Document.objects.filter(id=document.id).exists()
|
||||
|
||||
|
||||
def test_api_documents_content_update_authenticated_no_websocket(settings):
|
||||
"""
|
||||
When a user updates the document content, not connected to the websocket and is the first
|
||||
to update, the content should be updated.
|
||||
"""
|
||||
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")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": get_sample_ydoc(), "websocket": False},
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert get_s3_content(document) == get_sample_ydoc()
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
|
||||
|
||||
def test_api_documents_content_update_authenticated_no_websocket_user_already_editing(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When a user updates the document content, not connected to the websocket and another session
|
||||
is already editing, the update should be denied.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
django_cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": get_sample_ydoc(), "websocket": False},
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert response.json() == {"detail": "You are not allowed to edit this document."}
|
||||
|
||||
|
||||
# TODO(yhub): removed
|
||||
# test_api_documents_content_update_no_websocket_other_user_connected_to_websocket
|
||||
# here. yhub has no connection-info API: get_document_connection_info is stubbed to report
|
||||
# nobody connected, so another user connected to the websocket can no longer block the update.
|
||||
# Re-add the test once yhub exposes a connection-info API.
|
||||
|
||||
|
||||
def test_api_documents_content_update_user_connected_to_websocket(settings):
|
||||
"""
|
||||
When a user updates document content and is connected to the websocket,
|
||||
the content should be updated.
|
||||
"""
|
||||
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")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": get_sample_ydoc(), "websocket": False},
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert get_s3_content(document) == get_sample_ydoc()
|
||||
# TODO(yhub): the stubbed connection info reports nobody connected, so the
|
||||
# no-websocket cache lock is taken even though the user is connected.
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
|
||||
|
||||
def test_api_documents_content_update_websocket_server_unreachable_fallback_to_no_websocket(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the content should be updated like if the user
|
||||
was not connected to the websocket.
|
||||
"""
|
||||
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")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": get_sample_ydoc(), "websocket": False},
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert get_s3_content(document) == get_sample_ydoc()
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
|
||||
|
||||
def test_api_content_update_websocket_server_unreachable_fallback_to_no_websocket_other_users(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the behavior fallback to the no websocket one.
|
||||
If another user is already editing, the content update should be denied.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
django_cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": get_sample_ydoc(), "websocket": False},
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||
|
||||
|
||||
def test_api_content_update_websocket_server_room_not_found_fallback_to_no_websocket_other_users(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the WebSocket server does not have the room created, the logic should fallback to
|
||||
no-WebSocket. If another user is already editing, the update must be denied.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
django_cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": get_sample_ydoc(), "websocket": False},
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||
|
||||
|
||||
def test_api_documents_content_update_force_websocket_param_to_true(settings):
|
||||
"""
|
||||
When the websocket parameter is set to true, the content should be updated without any check.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": get_sample_ydoc(), "websocket": True},
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert get_s3_content(document) == get_sample_ydoc()
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
|
||||
def test_api_documents_content_update_feature_flag_disabled(settings):
|
||||
"""
|
||||
When the feature flag is disabled, the content should be updated without any check.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = False
|
||||
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{"content": get_sample_ydoc(), "websocket": False},
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
assert get_s3_content(document) == get_sample_ydoc()
|
||||
assert django_cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
|
||||
def test_api_documents_content_upadte_invalid_yjs_doc():
|
||||
"""sending an invalid yjs doc as content should return a 400."""
|
||||
user = factories.UserFactory()
|
||||
@@ -473,10 +265,7 @@ def test_api_documents_content_upadte_invalid_yjs_doc():
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/content/",
|
||||
{
|
||||
"content": base64.b64encode(b"invalid yjs").decode("utf-8"),
|
||||
"websocket": True,
|
||||
},
|
||||
{"content": base64.b64encode(b"invalid yjs").decode("utf-8")},
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"""Tests for link configuration of documents on API endpoint"""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
@@ -13,25 +10,6 @@ from core.tests.conftest import TEAM, USER, VIA
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
@pytest.fixture(name="mock_reset_connections")
|
||||
def mock_reset_connections_fixture():
|
||||
"""
|
||||
Provide a context manager that patches the ``reset_service_connections_in_cascade``
|
||||
Celery task and asserts its ``delay`` method is called exactly once for the given
|
||||
document when leaving the context.
|
||||
"""
|
||||
|
||||
@contextmanager
|
||||
def _mock_reset_connections(document_id):
|
||||
with mock.patch(
|
||||
"core.api.viewsets.reset_service_connections_in_cascade.delay"
|
||||
) as mock_delay:
|
||||
yield mock_delay
|
||||
mock_delay.assert_called_once_with(str(document_id))
|
||||
|
||||
return _mock_reset_connections
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", models.LinkRoleChoices.values)
|
||||
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
|
||||
def test_api_documents_link_configuration_update_anonymous(reach, role):
|
||||
@@ -141,7 +119,6 @@ def test_api_documents_link_configuration_update_authenticated_related_success(
|
||||
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
|
||||
@@ -171,18 +148,17 @@ def test_api_documents_link_configuration_update_authenticated_related_success(
|
||||
)
|
||||
).data
|
||||
|
||||
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
|
||||
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]
|
||||
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]
|
||||
|
||||
|
||||
def test_api_documents_link_configuration_update_role_restricted_forbidden():
|
||||
@@ -254,9 +230,7 @@ def test_api_documents_link_configuration_update_link_reach_required():
|
||||
assert "This field is required" in response.json()["link_reach"][0]
|
||||
|
||||
|
||||
def test_api_documents_link_configuration_update_restricted_without_role_success(
|
||||
mock_reset_connections, # pylint: disable=redefined-outer-name
|
||||
):
|
||||
def test_api_documents_link_configuration_update_restricted_without_role_success():
|
||||
"""
|
||||
Test that setting link_reach to restricted without specifying link_role succeeds.
|
||||
"""
|
||||
@@ -278,16 +252,15 @@ def test_api_documents_link_configuration_update_restricted_without_role_success
|
||||
"link_reach": models.LinkReachChoices.RESTRICTED,
|
||||
}
|
||||
|
||||
with mock_reset_connections(document.id):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
|
||||
new_data,
|
||||
format="json",
|
||||
)
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
|
||||
new_data,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
document.refresh_from_db()
|
||||
assert document.link_reach == models.LinkReachChoices.RESTRICTED
|
||||
assert response.status_code == 200
|
||||
document.refresh_from_db()
|
||||
assert document.link_reach == models.LinkReachChoices.RESTRICTED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -297,7 +270,6 @@ def test_api_documents_link_configuration_update_restricted_without_role_success
|
||||
def test_api_documents_link_configuration_update_non_restricted_with_valid_role_success(
|
||||
reach,
|
||||
role,
|
||||
mock_reset_connections, # pylint: disable=redefined-outer-name
|
||||
):
|
||||
"""
|
||||
Test that setting non-restricted link_reach with valid link_role succeeds.
|
||||
@@ -320,17 +292,16 @@ def test_api_documents_link_configuration_update_non_restricted_with_valid_role_
|
||||
"link_role": role,
|
||||
}
|
||||
|
||||
with mock_reset_connections(document.id):
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
|
||||
new_data,
|
||||
format="json",
|
||||
)
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/link-configuration/",
|
||||
new_data,
|
||||
format="json",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
document.refresh_from_db()
|
||||
assert document.link_reach == reach
|
||||
assert document.link_role == role
|
||||
assert response.status_code == 200
|
||||
document.refresh_from_db()
|
||||
assert document.link_reach == reach
|
||||
assert document.link_role == role
|
||||
|
||||
|
||||
def test_api_documents_link_configuration_update_with_ancestor_constraints():
|
||||
|
||||
@@ -33,7 +33,6 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": document.link_role == "editor",
|
||||
"can_edit": document.link_role == "editor",
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -114,7 +113,6 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": grand_parent.link_role == "editor",
|
||||
"can_edit": grand_parent.link_role == "editor",
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -225,7 +223,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"ai_transform": document.link_role == "editor",
|
||||
"ai_translate": document.link_role == "editor",
|
||||
"attachment_upload": document.link_role == "editor",
|
||||
"can_edit": document.link_role == "editor",
|
||||
"children_create": document.link_role == "editor",
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -313,7 +310,6 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"ai_transform": grand_parent.link_role == "editor",
|
||||
"ai_translate": grand_parent.link_role == "editor",
|
||||
"attachment_upload": grand_parent.link_role == "editor",
|
||||
"can_edit": grand_parent.link_role == "editor",
|
||||
"children_create": grand_parent.link_role == "editor",
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -513,7 +509,6 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"ai_transform": access.role not in ["reader", "commenter"],
|
||||
"ai_translate": access.role not in ["reader", "commenter"],
|
||||
"attachment_upload": access.role not in ["reader", "commenter"],
|
||||
"can_edit": access.role not in ["reader", "commenter"],
|
||||
"children_create": access.role not in ["reader", "commenter"],
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
|
||||
@@ -83,7 +83,6 @@ def test_api_documents_trashbin_format():
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": False,
|
||||
"collaboration_auth": False,
|
||||
@@ -151,7 +150,6 @@ def test_api_documents_trashbin_format():
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": False,
|
||||
"collaboration_auth": False,
|
||||
|
||||
@@ -7,7 +7,6 @@ import random
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.cache import cache
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
@@ -47,7 +46,6 @@ def test_api_documents_update_anonymous_forbidden(reach, role, via_parent):
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = APIClient().put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -96,7 +94,6 @@ def test_api_documents_update_authenticated_unrelated_forbidden(
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory(),
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -148,7 +145,6 @@ def test_api_documents_update_anonymous_or_authenticated_unrelated(
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory(),
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -216,7 +212,6 @@ def test_api_documents_update_authenticated_reader(via, via_parent, mock_user_te
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -269,7 +264,6 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -303,265 +297,6 @@ def test_api_documents_update_authenticated_editor_administrator_or_owner(
|
||||
assert value == new_document_values[key]
|
||||
|
||||
|
||||
def test_api_documents_update_authenticated_no_websocket(settings):
|
||||
"""
|
||||
When a user updates the document, not connected to the websocket and is the first to update,
|
||||
the document should be updated.
|
||||
"""
|
||||
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_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
old_path = document.path
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.path == old_path
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
|
||||
|
||||
def test_api_documents_update_authenticated_no_websocket_user_already_editing(settings):
|
||||
"""
|
||||
When a user updates the document, not connected to the websocket and is not the first to update,
|
||||
the document should not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "You are not allowed to edit this document."}
|
||||
|
||||
|
||||
# TODO(yhub): removed test_api_documents_update_no_websocket_other_user_connected_to_websocket
|
||||
# here. yhub has no connection-info API: get_document_connection_info is stubbed to report
|
||||
# nobody connected, so another user connected to the websocket can no longer block the update.
|
||||
# Re-add the test once yhub exposes a connection-info API.
|
||||
|
||||
|
||||
def test_api_documents_update_user_connected_to_websocket(settings):
|
||||
"""
|
||||
When a user updates the document, connected to the websocket, the document should be updated.
|
||||
"""
|
||||
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_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
old_path = document.path
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.path == old_path
|
||||
# TODO(yhub): the stubbed connection info reports nobody connected, so the
|
||||
# no-websocket cache lock is taken even though the user is connected.
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
|
||||
|
||||
def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websocket(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the document should be updated like if the user was
|
||||
not connected to the websocket.
|
||||
"""
|
||||
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_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
old_path = document.path
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.path == old_path
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
|
||||
|
||||
def test_api_documents_update_websocket_server_unreachable_fallback_to_no_websocket_other_users(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the behavior fallback to the no websocket one.
|
||||
If an other user is already editing, the document should not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||
|
||||
|
||||
def test_api_documents_update_websocket_server_room_not_found_fallback_to_no_websocket_other_users(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the WebSocket server does not have the room created, the logic should fallback to
|
||||
no-WebSocket. If another user is already editing, the update must be denied.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||
|
||||
|
||||
def test_api_documents_update_force_websocket_param_to_true():
|
||||
"""
|
||||
When the websocket parameter is set to true, the document should be updated without any check.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
old_path = document.path
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.path == old_path
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
|
||||
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)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = False
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = False
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
old_path = document.path
|
||||
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
new_document_values,
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
document.refresh_from_db()
|
||||
assert document.path == old_path
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_documents_update_administrator_or_owner_of_another(via, mock_user_teams):
|
||||
"""
|
||||
@@ -592,7 +327,6 @@ def test_api_documents_update_administrator_or_owner_of_another(via, mock_user_t
|
||||
new_document_values = serializers.DocumentSerializer(
|
||||
instance=factories.DocumentFactory()
|
||||
).data
|
||||
new_document_values["websocket"] = True
|
||||
response = client.put(
|
||||
f"/api/v1.0/documents/{other_document.id!s}/",
|
||||
new_document_values,
|
||||
@@ -728,7 +462,7 @@ def test_api_documents_patch_anonymous_or_authenticated_unrelated(
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"title": "new title", "websocket": True},
|
||||
{"title": "new title"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -832,7 +566,7 @@ def test_api_documents_patch_authenticated_editor_administrator_or_owner(
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"title": "new title", "websocket": True},
|
||||
{"title": "new title"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -857,248 +591,6 @@ def test_api_documents_patch_authenticated_editor_administrator_or_owner(
|
||||
assert document_values[key] == old_document_values[key]
|
||||
|
||||
|
||||
def test_api_documents_patch_authenticated_no_websocket(settings):
|
||||
"""
|
||||
When a user patches the document, not connected to the websocket and is the first to update,
|
||||
the document should be updated.
|
||||
"""
|
||||
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")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
old_path = document.path
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"title": "new title"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Using document.refresh_from_db does not work because the content is cached.
|
||||
# Force reloading it by fetching the document from the database.
|
||||
document = models.Document.objects.get(id=document.id)
|
||||
assert document.path == old_path
|
||||
assert document.title == "new title"
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
|
||||
|
||||
def test_api_documents_patch_authenticated_no_websocket_user_already_editing(settings):
|
||||
"""
|
||||
When a user patches the document, not connected to the websocket and is not the first to
|
||||
update, the document should not be updated.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"title": "new title"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {"detail": "You are not allowed to edit this document."}
|
||||
|
||||
|
||||
# TODO(yhub): removed test_api_documents_patch_no_websocket_other_user_connected_to_websocket
|
||||
# here. yhub has no connection-info API: get_document_connection_info is stubbed to report
|
||||
# nobody connected, so another user connected to the websocket can no longer block the patch.
|
||||
# Re-add the test once yhub exposes a connection-info API.
|
||||
|
||||
|
||||
def test_api_documents_patch_user_connected_to_websocket(settings):
|
||||
"""
|
||||
When a user patches the document while connected to the websocket, the document should be
|
||||
updated.
|
||||
"""
|
||||
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")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
old_path = document.path
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"title": "new title"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Using document.refresh_from_db does not wirk because the content is in cache.
|
||||
# Force reloading it by fetching the document in the database.
|
||||
document = models.Document.objects.get(id=document.id)
|
||||
assert document.path == old_path
|
||||
assert document.title == "new title"
|
||||
# TODO(yhub): the stubbed connection info reports nobody connected, so the
|
||||
# no-websocket cache lock is taken even though the user is connected.
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
|
||||
|
||||
def test_api_documents_patch_websocket_server_unreachable_fallback_to_no_websocket(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the patch should be applied like if the user was
|
||||
not connected to the websocket.
|
||||
"""
|
||||
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")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
old_path = document.path
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"title": "new title"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Using document.refresh_from_db does not work because the content is cached.
|
||||
# Force reloading it by fetching the document from the database.
|
||||
document = models.Document.objects.get(id=document.id)
|
||||
assert document.path == old_path
|
||||
assert document.title == "new title"
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
|
||||
|
||||
def test_api_documents_patch_websocket_server_unreachable_fallback_to_no_websocket_other_users(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the websocket server is unreachable, the behavior falls back to no-websocket.
|
||||
If another user is already editing, the patch must be denied.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"title": "new title"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||
|
||||
|
||||
def test_api_documents_patch_websocket_server_room_not_found_fallback_to_no_websocket_other_users(
|
||||
settings,
|
||||
):
|
||||
"""
|
||||
When the WebSocket server does not have the room created, the logic should fallback to
|
||||
no-WebSocket. If another user is already editing, the patch must be denied.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
cache.set(f"docs:no-websocket:{document.id}", "other_session_key")
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"title": "new title"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == "other_session_key"
|
||||
|
||||
|
||||
def test_api_documents_patch_force_websocket_param_to_true():
|
||||
"""
|
||||
When the websocket parameter is set to true, the patch should be applied without any check.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
old_path = document.path
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"title": "new title", "websocket": True},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Using document.refresh_from_db does not work because the content is cached.
|
||||
# Force reloading it by fetching the document from the database.
|
||||
document = models.Document.objects.get(id=document.id)
|
||||
assert document.path == old_path
|
||||
assert document.title == "new title"
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
|
||||
def test_api_documents_patch_feature_flag_disabled(settings):
|
||||
"""
|
||||
When the feature flag is disabled, the patch should be applied without any check.
|
||||
"""
|
||||
user = factories.UserFactory(with_owned_document=True)
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "editor")])
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = False
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
old_path = document.path
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1.0/documents/{document.id!s}/",
|
||||
{"title": "new title"},
|
||||
format="json",
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Using document.refresh_from_db does not work because the content is cached.
|
||||
# Force reloading it by fetching the document from the database.
|
||||
document = models.Document.objects.get(id=document.id)
|
||||
assert document.path == old_path
|
||||
assert document.title == "new title"
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("via", VIA)
|
||||
def test_api_documents_patch_administrator_or_owner_of_another(via, mock_user_teams):
|
||||
"""
|
||||
@@ -1141,7 +633,7 @@ def test_api_documents_patch_administrator_or_owner_of_another(via, mock_user_te
|
||||
)
|
||||
|
||||
|
||||
def test_api_documents_patch_empty_body(settings):
|
||||
def test_api_documents_patch_empty_body():
|
||||
"""
|
||||
Test when data is empty the document should not be updated.
|
||||
The `updated_at` property should not change asserting that no update in the database is made.
|
||||
@@ -1150,15 +642,10 @@ def test_api_documents_patch_empty_body(settings):
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
session_key = client.session.session_key
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "owner")], creator=user)
|
||||
document_updated_at = document.updated_at
|
||||
|
||||
settings.COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = True
|
||||
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") is None
|
||||
|
||||
old_document_values = serializers.DocumentSerializer(instance=document).data
|
||||
|
||||
with patch("core.models.Document.save") as mock_document_save:
|
||||
@@ -1173,6 +660,3 @@ def test_api_documents_patch_empty_body(settings):
|
||||
new_document_values = serializers.DocumentSerializer(instance=document).data
|
||||
assert new_document_values == old_document_values
|
||||
assert document_updated_at == document.updated_at
|
||||
# TODO(yhub): the stubbed connection info reports nobody connected, so the
|
||||
# no-websocket cache lock is taken even for an empty body.
|
||||
assert cache.get(f"docs:no-websocket:{document.id}") == session_key
|
||||
|
||||
+1
-7
@@ -6,8 +6,6 @@ because the resource server viewsets inherit from the api viewsets.
|
||||
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.test import override_settings
|
||||
|
||||
import pytest
|
||||
@@ -61,9 +59,8 @@ def test_external_api_documents_link_configuration_not_allowed(
|
||||
},
|
||||
},
|
||||
)
|
||||
@patch("core.api.viewsets.reset_service_connections_in_cascade.delay")
|
||||
def test_external_api_documents_link_configuration_can_be_allowed(
|
||||
mock_reset, user_token, resource_server_backend, user_specific_sub
|
||||
user_token, resource_server_backend, user_specific_sub
|
||||
):
|
||||
"""
|
||||
Connected users SHOULD be allowed to update the link configuration of a document
|
||||
@@ -101,6 +98,3 @@ def test_external_api_documents_link_configuration_can_be_allowed(
|
||||
document.refresh_from_db()
|
||||
assert document.link_reach == models.LinkReachChoices.PUBLIC
|
||||
assert document.link_role == models.LinkRoleChoices.EDITOR
|
||||
|
||||
# the collaboration server should be notified through the Celery task
|
||||
mock_reset.assert_called_once_with(str(document.id))
|
||||
|
||||
@@ -25,7 +25,6 @@ pytestmark = pytest.mark.django_db
|
||||
AI_FEATURE_LEGACY_ENABLED=False,
|
||||
API_USERS_SEARCH_QUERY_MIN_LENGTH=6,
|
||||
COLLABORATION_WS_URL="http://testcollab/",
|
||||
COLLABORATION_WS_NOT_CONNECTED_READ_ONLY=True,
|
||||
COLLABORATION_WS_INACTIVITY_TIMEOUT=300,
|
||||
CONVERSION_UPLOAD_ENABLED=False,
|
||||
FRONTEND_CSS_URL="http://testcss/",
|
||||
@@ -56,7 +55,6 @@ def test_api_config(is_authenticated):
|
||||
"AI_FEATURE_LEGACY_ENABLED": False,
|
||||
"API_USERS_SEARCH_QUERY_MIN_LENGTH": 6,
|
||||
"COLLABORATION_WS_URL": "http://testcollab/",
|
||||
"COLLABORATION_WS_NOT_CONNECTED_READ_ONLY": True,
|
||||
"COLLABORATION_WS_INACTIVITY_TIMEOUT": 300,
|
||||
"CONVERSION_FILE_EXTENSIONS_ALLOWED": [".docx", ".md"],
|
||||
"CONVERSION_FILE_MAX_SIZE": 20971520,
|
||||
|
||||
@@ -159,7 +159,6 @@ def test_models_documents_get_abilities_forbidden(
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": False,
|
||||
"collaboration_auth": False,
|
||||
@@ -228,7 +227,6 @@ def test_models_documents_get_abilities_reader(
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -302,7 +300,6 @@ def test_models_documents_get_abilities_commenter(
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -373,7 +370,6 @@ def test_models_documents_get_abilities_editor(
|
||||
"ai_transform": is_authenticated,
|
||||
"ai_translate": is_authenticated,
|
||||
"attachment_upload": True,
|
||||
"can_edit": True,
|
||||
"children_create": is_authenticated,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -433,7 +429,6 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
||||
"ai_transform": True,
|
||||
"ai_translate": True,
|
||||
"attachment_upload": True,
|
||||
"can_edit": True,
|
||||
"children_create": True,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -479,7 +474,6 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": False,
|
||||
"collaboration_auth": False,
|
||||
@@ -529,7 +523,6 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
|
||||
"ai_transform": True,
|
||||
"ai_translate": True,
|
||||
"attachment_upload": True,
|
||||
"can_edit": True,
|
||||
"children_create": True,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -589,7 +582,6 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
|
||||
"ai_transform": True,
|
||||
"ai_translate": True,
|
||||
"attachment_upload": True,
|
||||
"can_edit": True,
|
||||
"children_create": True,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -656,7 +648,6 @@ def test_models_documents_get_abilities_reader_user(
|
||||
"ai_transform": access_from_link and ai_access_setting != "restricted",
|
||||
"ai_translate": access_from_link and ai_access_setting != "restricted",
|
||||
"attachment_upload": access_from_link,
|
||||
"can_edit": access_from_link,
|
||||
"children_create": access_from_link,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -726,7 +717,6 @@ def test_models_documents_get_abilities_commenter_user(
|
||||
"ai_transform": access_from_link and ai_access_setting != "restricted",
|
||||
"ai_translate": access_from_link and ai_access_setting != "restricted",
|
||||
"attachment_upload": access_from_link,
|
||||
"can_edit": access_from_link,
|
||||
"children_create": access_from_link,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
@@ -791,7 +781,6 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
|
||||
"ai_transform": False,
|
||||
"ai_translate": False,
|
||||
"attachment_upload": False,
|
||||
"can_edit": False,
|
||||
"children_create": False,
|
||||
"children_list": True,
|
||||
"collaboration_auth": True,
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
"""
|
||||
This module contains tests for the CollaborationService class in the
|
||||
core.services.collaboration_services module.
|
||||
"""
|
||||
|
||||
import responses
|
||||
|
||||
from core.services.collaboration_services import CollaborationService
|
||||
|
||||
|
||||
def test_reset_connections_makes_no_http_call():
|
||||
"""
|
||||
TODO(yhub): yhub has no kick API, so reset_connections is a no-op. It must
|
||||
neither make any HTTP call nor raise, even without any collaboration
|
||||
settings configured.
|
||||
"""
|
||||
with responses.RequestsMock():
|
||||
CollaborationService().reset_connections("document-id")
|
||||
CollaborationService().reset_connections("document-id", user_id="user-id")
|
||||
|
||||
|
||||
def test_get_document_connection_info_makes_no_http_call():
|
||||
"""
|
||||
TODO(yhub): yhub has no connection-info API, so get_document_connection_info
|
||||
always reports nobody connected, without making any HTTP call.
|
||||
"""
|
||||
with responses.RequestsMock():
|
||||
assert CollaborationService().get_document_connection_info(
|
||||
"room", "session-key"
|
||||
) == (0, False)
|
||||
@@ -1,31 +0,0 @@
|
||||
"""
|
||||
Tests for the `reset_service_connections_in_cascade` Celery task in the
|
||||
core.tasks.access module.
|
||||
"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from core.tasks.access import reset_service_connections_in_cascade
|
||||
|
||||
|
||||
@mock.patch("core.tasks.access.CollaborationService")
|
||||
def test_reset_service_connections_delegates_to_service(mock_service):
|
||||
"""
|
||||
The task should delegate the whole reset to the CollaborationService,
|
||||
forwarding both the document id and the user id.
|
||||
"""
|
||||
reset_service_connections_in_cascade("document-id", "user-id")
|
||||
|
||||
mock_service.return_value.reset_connections.assert_called_once_with(
|
||||
"document-id", "user-id"
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("core.tasks.access.CollaborationService")
|
||||
def test_reset_service_connections_defaults_user_id_to_none(mock_service):
|
||||
"""When no user id is provided, the task should forward None to the service."""
|
||||
reset_service_connections_in_cascade("document-id")
|
||||
|
||||
mock_service.return_value.reset_connections.assert_called_once_with(
|
||||
"document-id", None
|
||||
)
|
||||
@@ -521,28 +521,9 @@ class Base(Configuration):
|
||||
SENTRY_DSN = values.Value(None, environ_name="SENTRY_DSN", environ_prefix=None)
|
||||
|
||||
# Collaboration
|
||||
# TODO(yhub): unused since the yhub migration — yhub has no management API
|
||||
# (reset-connections / get-connections). Kept until a yhub kick and
|
||||
# connection-info API exist and CollaborationService is reinstated.
|
||||
COLLABORATION_API_URL = values.Value(
|
||||
None, environ_name="COLLABORATION_API_URL", environ_prefix=None
|
||||
)
|
||||
# TODO(yhub): unused since the yhub migration, see COLLABORATION_API_URL.
|
||||
COLLABORATION_SERVER_SECRET = SecretFileValue(
|
||||
None, environ_name="COLLABORATION_SERVER_SECRET", environ_prefix=None
|
||||
)
|
||||
COLLABORATION_WS_URL = values.Value(
|
||||
None, environ_name="COLLABORATION_WS_URL", environ_prefix=None
|
||||
)
|
||||
COLLABORATION_WS_NOT_CONNECTED_READ_ONLY = values.BooleanValue(
|
||||
default=values.BooleanValue( # COLLABORATION_WS_NOT_CONNECTED_READY_ONLY compat
|
||||
default=False,
|
||||
environ_name="COLLABORATION_WS_NOT_CONNECTED_READY_ONLY",
|
||||
environ_prefix=None,
|
||||
),
|
||||
environ_name="COLLABORATION_WS_NOT_CONNECTED_READ_ONLY",
|
||||
environ_prefix=None,
|
||||
)
|
||||
COLLABORATION_WS_INACTIVITY_TIMEOUT = values.IntegerValue(
|
||||
None,
|
||||
environ_name="COLLABORATION_WS_INACTIVITY_TIMEOUT",
|
||||
@@ -992,12 +973,6 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
NO_WEBSOCKET_CACHE_TIMEOUT = values.Value(
|
||||
default=120,
|
||||
environ_name="NO_WEBSOCKET_CACHE_TIMEOUT",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# Logging
|
||||
# We want to make it easy to log to console but by default we log production
|
||||
# to Sentry and don't want to log to console.
|
||||
|
||||
@@ -15,9 +15,6 @@ image:
|
||||
backend:
|
||||
replicas: 1
|
||||
envVars:
|
||||
COLLABORATION_SERVER_SECRET: my-secret
|
||||
COLLABORATION_API_URL: https://docs.127.0.0.1.nip.io/collaboration/api/
|
||||
COLLABORATION_WS_NOT_CONNECTED_READ_ONLY: False
|
||||
CONVERSION_UPLOAD_ENABLED: True
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://docs.127.0.0.1.nip.io
|
||||
DJANGO_CONFIGURATION: Feature
|
||||
|
||||
@@ -16,10 +16,7 @@ image:
|
||||
backend:
|
||||
replicas: 1
|
||||
envVars:
|
||||
COLLABORATION_SERVER_SECRET: my-secret
|
||||
CONVERSION_UPLOAD_ENABLED: True
|
||||
COLLABORATION_API_URL: https://{{ .Values.feature }}-docs.{{ .Values.domain }}/collaboration/api/
|
||||
COLLABORATION_WS_NOT_CONNECTED_READ_ONLY: True
|
||||
DJANGO_CSRF_TRUSTED_ORIGINS: https://{{ .Values.feature }}-docs.{{ .Values.domain }}
|
||||
DJANGO_CONFIGURATION: Feature
|
||||
DJANGO_ALLOWED_HOSTS: {{ .Values.feature }}-docs.{{ .Values.domain }}
|
||||
|
||||
Reference in New Issue
Block a user