(backend) add API endpoint action to restore a soft deleted document

Only owners can see and restore deleted documents. They can only do
it during the grace period before the document is considered hard
deleted and hidden from everybody on the API.
This commit is contained in:
Samuel Paccoud - DINUM
2025-01-05 14:43:15 +01:00
committed by Anthony LC
parent 8ccfdb3c6a
commit 239342fbbd
6 changed files with 203 additions and 1 deletions

View File

@@ -0,0 +1,126 @@
"""
Test restoring documents after a soft delete via the detail action API endpoint.
"""
from datetime import timedelta
from django.utils import timezone
import pytest
from rest_framework.test import APIClient
from core import factories
pytestmark = pytest.mark.django_db
def test_api_documents_restore_anonymous_user():
"""Anonymous users should not be able to restore deleted documents."""
now = timezone.now() - timedelta(days=15)
document = factories.DocumentFactory(deleted_at=now)
response = APIClient().post(f"/api/v1.0/documents/{document.id!s}/restore/")
assert response.status_code == 404
assert response.json() == {"detail": "Not found."}
document.refresh_from_db()
assert document.deleted_at == now
assert document.ancestors_deleted_at == now
@pytest.mark.parametrize("role", [None, "reader", "editor", "administrator"])
def test_api_documents_restore_authenticated_no_permission(role):
"""
Authenticated users who are not owners of a deleted document should
not be allowed to restore it.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
now = timezone.now() - timedelta(days=15)
document = factories.DocumentFactory(
deleted_at=now, link_reach="public", link_role="editor"
)
if role:
factories.UserDocumentAccessFactory(document=document, user=user, role=role)
response = client.post(f"/api/v1.0/documents/{document.id!s}/restore/")
assert response.status_code == 404
assert response.json() == {"detail": "Not found."}
document.refresh_from_db()
assert document.deleted_at == now
assert document.ancestors_deleted_at == now
def test_api_documents_restore_authenticated_owner_success():
"""The owner of a deleted document should be able to restore it."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
now = timezone.now() - timedelta(days=15)
document = factories.DocumentFactory(deleted_at=now)
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
response = client.post(f"/api/v1.0/documents/{document.id!s}/restore/")
assert response.status_code == 200
assert response.json() == {"detail": "Document has been successfully restored."}
document.refresh_from_db()
assert document.deleted_at is None
assert document.ancestors_deleted_at is None
def test_api_documents_restore_authenticated_owner_ancestor_deleted():
"""
The restored document should still be marked as deleted if one of its
ancestors is soft deleted as well.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
grand_parent = factories.DocumentFactory()
parent = factories.DocumentFactory(parent=grand_parent)
document = factories.DocumentFactory(parent=parent)
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
document.soft_delete()
document_deleted_at = document.deleted_at
assert document_deleted_at is not None
grand_parent.soft_delete()
grand_parent_deleted_at = grand_parent.deleted_at
assert grand_parent_deleted_at is not None
response = client.post(f"/api/v1.0/documents/{document.id!s}/restore/")
assert response.status_code == 200
assert response.json() == {"detail": "Document has been successfully restored."}
document.refresh_from_db()
assert document.deleted_at is None
# document is still marked as deleted
assert document.ancestors_deleted_at == grand_parent_deleted_at
assert grand_parent_deleted_at > document_deleted_at
def test_api_documents_restore_authenticated_owner_expired():
"""It should not be possible to restore a document beyond the allowed time limit."""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
now = timezone.now() - timedelta(days=40)
document = factories.DocumentFactory(deleted_at=now)
factories.UserDocumentAccessFactory(document=document, user=user, role="owner")
response = client.post(f"/api/v1.0/documents/{document.id!s}/restore/")
assert response.status_code == 404
assert response.json() == {"detail": "Not found."}

View File

@@ -42,6 +42,7 @@ def test_api_documents_retrieve_anonymous_public_standalone():
"media_auth": True,
"move": False,
"partial_update": document.link_role == "editor",
"restore": False,
"retrieve": True,
"update": document.link_role == "editor",
"versions_destroy": False,
@@ -97,6 +98,7 @@ def test_api_documents_retrieve_anonymous_public_parent():
"media_auth": True,
"move": False,
"partial_update": grand_parent.link_role == "editor",
"restore": False,
"retrieve": True,
"update": grand_parent.link_role == "editor",
"versions_destroy": False,
@@ -185,6 +187,7 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"media_auth": True,
"move": False,
"partial_update": document.link_role == "editor",
"restore": False,
"retrieve": True,
"update": document.link_role == "editor",
"versions_destroy": False,
@@ -247,6 +250,7 @@ def test_api_documents_retrieve_authenticated_public_or_authenticated_parent(rea
"move": False,
"media_auth": True,
"partial_update": grand_parent.link_role == "editor",
"restore": False,
"retrieve": True,
"update": grand_parent.link_role == "editor",
"versions_destroy": False,
@@ -418,6 +422,7 @@ def test_api_documents_retrieve_authenticated_related_parent():
"media_auth": True,
"move": access.role in ["administrator", "owner"],
"partial_update": access.role != "reader",
"restore": access.role == "owner",
"retrieve": True,
"update": access.role != "reader",
"versions_destroy": access.role in ["administrator", "owner"],

View File

@@ -160,6 +160,7 @@ def test_models_documents_get_abilities_forbidden(
"move": False,
"link_configuration": False,
"partial_update": False,
"restore": False,
"retrieve": False,
"update": False,
"versions_destroy": False,
@@ -207,6 +208,7 @@ def test_models_documents_get_abilities_reader(
"media_auth": True,
"move": False,
"partial_update": False,
"restore": False,
"retrieve": True,
"update": False,
"versions_destroy": False,
@@ -254,6 +256,7 @@ def test_models_documents_get_abilities_editor(
"media_auth": True,
"move": False,
"partial_update": True,
"restore": False,
"retrieve": True,
"update": True,
"versions_destroy": False,
@@ -288,6 +291,7 @@ def test_models_documents_get_abilities_owner(django_assert_num_queries):
"media_auth": True,
"move": True,
"partial_update": True,
"restore": True,
"retrieve": True,
"update": True,
"versions_destroy": True,
@@ -322,6 +326,7 @@ def test_models_documents_get_abilities_administrator(django_assert_num_queries)
"media_auth": True,
"move": True,
"partial_update": True,
"restore": False,
"retrieve": True,
"update": True,
"versions_destroy": True,
@@ -355,6 +360,7 @@ def test_models_documents_get_abilities_editor_user(django_assert_num_queries):
"media_auth": True,
"move": False,
"partial_update": True,
"restore": False,
"retrieve": True,
"update": True,
"versions_destroy": False,
@@ -391,6 +397,7 @@ def test_models_documents_get_abilities_reader_user(django_assert_num_queries):
"media_auth": True,
"move": False,
"partial_update": access_from_link,
"restore": False,
"retrieve": True,
"update": access_from_link,
"versions_destroy": False,
@@ -430,6 +437,7 @@ def test_models_documents_get_abilities_preset_role(django_assert_num_queries):
"media_auth": True,
"move": False,
"partial_update": False,
"restore": False,
"retrieve": True,
"update": False,
"versions_destroy": False,