✨(backend) add API endpoint to move a document in the document tree
Only administrators or owners of a document can move it to a target document for which they are also administrator or owner. We allow different moving modes: - first-child: move the document as the first child of the target - last-child: move the document as the last child of the target - first-sibling: move the document as the first sibling of the target - last-sibling: move the document as the last sibling of the target - left: move the document as sibling ordered just before the target - right: move the document as sibling ordered just after the target The whole subtree below the document that is being moved, moves as well and remains below the document after it is moved.
This commit is contained in:
committed by
Anthony LC
parent
2e8a399668
commit
4de03d292a
339
src/backend/core/tests/documents/test_api_documents_move.py
Normal file
339
src/backend/core/tests/documents/test_api_documents_move.py
Normal file
@@ -0,0 +1,339 @@
|
||||
"""
|
||||
Test moving documents within the document tree via an detail action API endpoint.
|
||||
"""
|
||||
|
||||
import random
|
||||
from uuid import uuid4
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from core import enums, factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_api_documents_move_anonymous_user():
|
||||
"""Anonymous users should not be able to move documents."""
|
||||
document = factories.DocumentFactory()
|
||||
target = factories.DocumentFactory()
|
||||
|
||||
response = APIClient().post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json() == {
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("role", [None, "reader", "editor"])
|
||||
def test_api_documents_move_authenticated_document_no_permission(role):
|
||||
"""
|
||||
Authenticated users should not be able to move documents with insufficient
|
||||
permissions on the origin document.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory()
|
||||
target = factories.UserDocumentAccessFactory(user=user, role="owner").document
|
||||
|
||||
if role:
|
||||
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id)},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_move_invalid_target_string():
|
||||
"""Test for moving a document to an invalid target as a random string."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.UserDocumentAccessFactory(user=user, role="owner").document
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": "non-existent-id"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {"target_document_id": ["Must be a valid UUID."]}
|
||||
|
||||
|
||||
def test_api_documents_move_invalid_target_uuid():
|
||||
"""Test for moving a document to an invalid target that looks like a UUID."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.UserDocumentAccessFactory(user=user, role="owner").document
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(uuid4())},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"target_document_id": "Target parent document does not exist."
|
||||
}
|
||||
|
||||
|
||||
def test_api_documents_move_invalid_position():
|
||||
"""Test moving a document to an invalid position."""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.UserDocumentAccessFactory(user=user, role="owner").document
|
||||
target = factories.UserDocumentAccessFactory(user=user, role="owner").document
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={
|
||||
"target_document_id": str(target.id),
|
||||
"position": "invalid-position",
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"position": ['"invalid-position" is not a valid choice.']
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("position", enums.MoveNodePositionChoices.values)
|
||||
@pytest.mark.parametrize("target_parent_role", models.RoleChoices.values)
|
||||
@pytest.mark.parametrize("target_role", models.RoleChoices.values)
|
||||
def test_api_documents_move_authenticated_target_roles_mocked(
|
||||
target_role, target_parent_role, position
|
||||
):
|
||||
"""
|
||||
Authenticated users with insufficient permissions on the target document (or its
|
||||
parent depending on the position chosen), should not be allowed to move documents.
|
||||
"""
|
||||
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
power_roles = ["administrator", "owner"]
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, random.choice(power_roles))])
|
||||
children = factories.DocumentFactory.create_batch(3, parent=document)
|
||||
|
||||
target_parent = factories.DocumentFactory(users=[(user, target_parent_role)])
|
||||
sibling1, target, sibling2 = factories.DocumentFactory.create_batch(
|
||||
3, parent=target_parent
|
||||
)
|
||||
models.DocumentAccess.objects.create(document=target, user=user, role=target_role)
|
||||
target_children = factories.DocumentFactory.create_batch(2, parent=target)
|
||||
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id), "position": position},
|
||||
)
|
||||
|
||||
document.refresh_from_db()
|
||||
|
||||
if (
|
||||
position in ["first-child", "last-child"]
|
||||
and (target_role in power_roles or target_parent_role in power_roles)
|
||||
) or (
|
||||
position in ["first-sibling", "last-sibling", "left", "right"]
|
||||
and target_parent_role in power_roles
|
||||
):
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Document moved successfully."}
|
||||
|
||||
match position:
|
||||
case "first-child":
|
||||
assert list(target.get_children()) == [document, *target_children]
|
||||
case "last-child":
|
||||
assert list(target.get_children()) == [*target_children, document]
|
||||
case "first-sibling":
|
||||
assert list(target.get_siblings()) == [
|
||||
document,
|
||||
sibling1,
|
||||
target,
|
||||
sibling2,
|
||||
]
|
||||
case "last-sibling":
|
||||
assert list(target.get_siblings()) == [
|
||||
sibling1,
|
||||
target,
|
||||
sibling2,
|
||||
document,
|
||||
]
|
||||
case "left":
|
||||
assert list(target.get_siblings()) == [
|
||||
sibling1,
|
||||
document,
|
||||
target,
|
||||
sibling2,
|
||||
]
|
||||
case "right":
|
||||
assert list(target.get_siblings()) == [
|
||||
sibling1,
|
||||
target,
|
||||
document,
|
||||
sibling2,
|
||||
]
|
||||
case _:
|
||||
raise ValueError(f"Invalid position: {position}")
|
||||
|
||||
# Verify that the document's children have also been moved
|
||||
assert list(document.get_children()) == children
|
||||
else:
|
||||
assert response.status_code == 400
|
||||
assert (
|
||||
"You do not have permission to move documents"
|
||||
in response.json()["target_document_id"]
|
||||
)
|
||||
assert document.is_root() is True
|
||||
|
||||
|
||||
def test_api_documents_move_authenticated_deleted_document():
|
||||
"""
|
||||
It should not be possible to move a deleted document or its descendants, even
|
||||
for an owner.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(
|
||||
users=[(user, "owner")], deleted_at=timezone.now()
|
||||
)
|
||||
child = factories.DocumentFactory(parent=document, users=[(user, "owner")])
|
||||
|
||||
target = factories.DocumentFactory(users=[(user, "owner")])
|
||||
|
||||
# Try moving the deleted document
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id)},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
# Verify that the document has not moved
|
||||
document.refresh_from_db()
|
||||
assert document.is_root() is True
|
||||
|
||||
# Try moving the child of the deleted document
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{child.id!s}/move/",
|
||||
data={"target_document_id": str(target.id)},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
assert response.json() == {
|
||||
"detail": "You do not have permission to perform this action."
|
||||
}
|
||||
|
||||
# Verify that the child has not moved
|
||||
child.refresh_from_db()
|
||||
assert child.is_child_of(document) is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"position",
|
||||
enums.MoveNodePositionChoices.values,
|
||||
)
|
||||
def test_api_documents_move_authenticated_deleted_target_as_child(position):
|
||||
"""
|
||||
It should not be possible to move a document as a child of a deleted target
|
||||
even for a owner.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "owner")])
|
||||
|
||||
target = factories.DocumentFactory(
|
||||
users=[(user, "owner")], deleted_at=timezone.now()
|
||||
)
|
||||
child = factories.DocumentFactory(parent=target, users=[(user, "owner")])
|
||||
|
||||
# Try moving the document to the deleted target
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id), "position": position},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"target_document_id": "Target parent document does not exist."
|
||||
}
|
||||
|
||||
# Verify that the document has not moved
|
||||
document.refresh_from_db()
|
||||
assert document.is_root() is True
|
||||
|
||||
# Try moving the document to the child of the deleted target
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(child.id), "position": position},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"target_document_id": "Target parent document does not exist."
|
||||
}
|
||||
|
||||
# Verify that the document has not moved
|
||||
document.refresh_from_db()
|
||||
assert document.is_root() is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"position",
|
||||
["first-sibling", "last-sibling", "left", "right"],
|
||||
)
|
||||
def test_api_documents_move_authenticated_deleted_target_as_sibling(position):
|
||||
"""
|
||||
It should not be possible to move a document as a sibling of a deleted target document
|
||||
if the user has no rigths on its parent.
|
||||
"""
|
||||
user = factories.UserFactory()
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
document = factories.DocumentFactory(users=[(user, "owner")])
|
||||
|
||||
target_parent = factories.DocumentFactory(
|
||||
users=[(user, "owner")], deleted_at=timezone.now()
|
||||
)
|
||||
target = factories.DocumentFactory(users=[(user, "owner")], parent=target_parent)
|
||||
|
||||
# Try moving the document as a sibling of the target
|
||||
response = client.post(
|
||||
f"/api/v1.0/documents/{document.id!s}/move/",
|
||||
data={"target_document_id": str(target.id), "position": position},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json() == {
|
||||
"target_document_id": "Target parent document does not exist."
|
||||
}
|
||||
|
||||
# Verify that the document has not moved
|
||||
document.refresh_from_db()
|
||||
assert document.is_root() is True
|
||||
@@ -36,6 +36,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"media_auth": True,
|
||||
"move": False,
|
||||
"partial_update": document.link_role == "editor",
|
||||
"retrieve": True,
|
||||
"update": document.link_role == "editor",
|
||||
@@ -90,6 +91,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"media_auth": True,
|
||||
"move": False,
|
||||
"partial_update": grand_parent.link_role == "editor",
|
||||
"retrieve": True,
|
||||
"update": grand_parent.link_role == "editor",
|
||||
@@ -175,8 +177,9 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
|
||||
"destroy": False,
|
||||
"favorite": True,
|
||||
"invite_owner": False,
|
||||
"media_auth": True,
|
||||
"link_configuration": False,
|
||||
"media_auth": True,
|
||||
"move": False,
|
||||
"partial_update": document.link_role == "editor",
|
||||
"retrieve": True,
|
||||
"update": document.link_role == "editor",
|
||||
@@ -237,6 +240,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
|
||||
"favorite": True,
|
||||
"invite_owner": False,
|
||||
"link_configuration": False,
|
||||
"move": False,
|
||||
"media_auth": True,
|
||||
"partial_update": grand_parent.link_role == "editor",
|
||||
"retrieve": True,
|
||||
@@ -408,6 +412,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
|
||||
"invite_owner": access.role == "owner",
|
||||
"link_configuration": access.role in ["administrator", "owner"],
|
||||
"media_auth": True,
|
||||
"move": access.role in ["administrator", "owner"],
|
||||
"partial_update": access.role != "reader",
|
||||
"retrieve": True,
|
||||
"update": access.role != "reader",
|
||||
@@ -753,4 +758,3 @@ def test_api_documents_retrieve_numqueries_with_link_trace(django_assert_num_que
|
||||
assert response.status_code == 200
|
||||
|
||||
assert response.json()["id"] == str(document.id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user