diff --git a/docs/source/endpoints/index.md b/docs/source/endpoints/index.md
index 3d33783b4..8aaefc0b1 100644
--- a/docs/source/endpoints/index.md
+++ b/docs/source/endpoints/index.md
@@ -42,6 +42,7 @@ portrait
principals
querystring
querystringsearch
+recycle-bin
registry
relations
roles
diff --git a/docs/source/endpoints/recycle-bin.md b/docs/source/endpoints/recycle-bin.md
new file mode 100644
index 000000000..930200208
--- /dev/null
+++ b/docs/source/endpoints/recycle-bin.md
@@ -0,0 +1,147 @@
+# Recycle Bin
+
+The Recycle Bin REST API provides endpoints to interact with the Plone Recycle Bin functionality.
+
+## List recycle bin contents
+
+To list all items in the recycle bin, send a GET request to the `@recyclebin` endpoint:
+
+```http-example
+GET /@recyclebin HTTP/1.1
+Accept: application/json
+```
+
+Response:
+
+```json
+{
+ "@id": "http://localhost:8080/Plone/@recyclebin",
+ "items": [
+ {
+ "@id": "http://localhost:8080/Plone/@recyclebin/6d6d626f-8c85-4f22-8747-adb979bbe3b1",
+ "id": "document-1",
+ "title": "My Document",
+ "type": "Document",
+ "path": "/Plone/folder/document-1",
+ "parent_path": "/Plone/folder",
+ "deletion_date": "2025-04-27T10:30:45.123456",
+ "size": 1024,
+ "recycle_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1",
+ "actions": {
+ "restore": "http://localhost:8080/Plone/@recyclebin-restore",
+ "purge": "http://localhost:8080/Plone/@recyclebin-purge"
+ }
+ }
+ ],
+ "items_total": 1
+}
+```
+
+## Restore an item from the recycle bin
+
+To restore an item from the recycle bin, send a POST request to the `@recyclebin-restore` endpoint:
+
+```http-example
+POST /@recyclebin-restore HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+
+{
+ "item_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1"
+}
+```
+
+You can optionally specify a target path to restore to:
+
+```json
+{
+ "item_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1",
+ "target_path": "/Plone/another-folder"
+}
+```
+
+Response:
+
+```json
+{
+ "status": "success",
+ "message": "Item document-1 restored successfully",
+ "restored_item": {
+ "@id": "http://localhost:8080/Plone/document-1",
+ "id": "document-1",
+ "title": "My Document",
+ "type": "Document"
+ }
+}
+```
+
+## Purge an item from the recycle bin
+
+To permanently delete an item from the recycle bin, send a POST request to the `@recyclebin-purge` endpoint:
+
+```http-example
+POST /@recyclebin-purge HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+
+{
+ "item_id": "6d6d626f-8c85-4f22-8747-adb979bbe3b1"
+}
+```
+
+Response:
+
+```json
+{
+ "status": "success",
+ "message": "Item document-1 purged successfully"
+}
+```
+
+## Purge all items from the recycle bin
+
+To purge all items from the recycle bin:
+
+```http-example
+POST /@recyclebin-purge HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+
+{
+ "purge_all": true
+}
+```
+
+Response:
+
+```json
+{
+ "status": "success",
+ "purged_count": 5,
+ "message": "Purged 5 items from recycle bin"
+}
+```
+
+## Purge expired items from the recycle bin
+
+To purge only expired items (based on the retention period):
+
+```http-example
+POST /@recyclebin-purge HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+
+{
+ "purge_expired": true
+}
+```
+
+Response:
+
+```json
+{
+ "status": "success",
+ "purged_count": 2,
+ "message": "Purged 2 expired items from recycle bin"
+}
+```
diff --git a/news/1919.feature b/news/1919.feature
new file mode 100644
index 000000000..4f1a01205
--- /dev/null
+++ b/news/1919.feature
@@ -0,0 +1 @@
+Add endpoint for managing recycle bin. @rohnsha0
\ No newline at end of file
diff --git a/src/plone/restapi/services/configure.zcml b/src/plone/restapi/services/configure.zcml
index 0f42573cd..2a37051b6 100644
--- a/src/plone/restapi/services/configure.zcml
+++ b/src/plone/restapi/services/configure.zcml
@@ -34,6 +34,7 @@
+
diff --git a/src/plone/restapi/services/recyclebin/__init__.py b/src/plone/restapi/services/recyclebin/__init__.py
new file mode 100644
index 000000000..a000b69d0
--- /dev/null
+++ b/src/plone/restapi/services/recyclebin/__init__.py
@@ -0,0 +1 @@
+# Empty init file to make the directory a Python package
diff --git a/src/plone/restapi/services/recyclebin/configure.zcml b/src/plone/restapi/services/recyclebin/configure.zcml
new file mode 100644
index 000000000..13b918171
--- /dev/null
+++ b/src/plone/restapi/services/recyclebin/configure.zcml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/plone/restapi/services/recyclebin/get.py b/src/plone/restapi/services/recyclebin/get.py
new file mode 100644
index 000000000..a7a30685f
--- /dev/null
+++ b/src/plone/restapi/services/recyclebin/get.py
@@ -0,0 +1,50 @@
+from plone.restapi.services import Service
+from Products.CMFPlone.interfaces.recyclebin import IRecycleBin
+from zope.component import getUtility
+
+
+class RecycleBinGet(Service):
+ """GET /@recyclebin - List items in the recycle bin"""
+
+ def reply(self):
+ recycle_bin = getUtility(IRecycleBin)
+
+ # Check if recycle bin is enabled
+ if not recycle_bin.is_enabled():
+ self.request.response.setStatus(404)
+ return {
+ "error": {
+ "type": "NotFound",
+ "message": "Recycle bin is disabled",
+ }
+ }
+
+ # Get all items from recycle bin
+ items = recycle_bin.get_items()
+
+ # Format items for response
+ results = []
+ for item in items:
+ results.append(
+ {
+ "@id": f"{self.context.absolute_url()}/@recyclebin/{item['recycle_id']}",
+ "id": item["id"],
+ "title": item["title"],
+ "type": item["type"],
+ "path": item["path"],
+ "parent_path": item["parent_path"],
+ "deletion_date": item["deletion_date"].isoformat(),
+ "size": item["size"],
+ "recycle_id": item["recycle_id"],
+ "actions": {
+ "restore": f"{self.context.absolute_url()}/@recyclebin-restore",
+ "purge": f"{self.context.absolute_url()}/@recyclebin-purge",
+ },
+ }
+ )
+
+ return {
+ "@id": f"{self.context.absolute_url()}/@recyclebin",
+ "items": results,
+ "items_total": len(results),
+ }
diff --git a/src/plone/restapi/services/recyclebin/purge.py b/src/plone/restapi/services/recyclebin/purge.py
new file mode 100644
index 000000000..a62bc74cc
--- /dev/null
+++ b/src/plone/restapi/services/recyclebin/purge.py
@@ -0,0 +1,93 @@
+from plone.restapi.deserializer import json_body
+from plone.restapi.services import Service
+from Products.CMFPlone.interfaces.recyclebin import IRecycleBin
+from zope.component import getUtility
+from zope.interface import alsoProvides
+
+import plone.protect.interfaces
+
+
+class RecycleBinPurge(Service):
+ """POST /@recyclebin-purge - Permanently delete an item from the recycle bin"""
+
+ def reply(self):
+ # Disable CSRF protection for this request
+ alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection)
+
+ data = json_body(self.request)
+ item_id = data.get("item_id", None)
+ purge_all = data.get("purge_all", False)
+ purge_expired = data.get("purge_expired", False)
+
+ recycle_bin = getUtility(IRecycleBin)
+
+ # Check if recycle bin is enabled
+ if not recycle_bin.is_enabled():
+ self.request.response.setStatus(404)
+ return {
+ "error": {
+ "type": "NotFound",
+ "message": "Recycle bin is disabled",
+ }
+ }
+
+ # Handle purging all items
+ if purge_all:
+ purged_count = 0
+ for item in recycle_bin.get_items():
+ if recycle_bin.purge_item(item["recycle_id"]):
+ purged_count += 1
+
+ return {
+ "status": "success",
+ "purged_count": purged_count,
+ "message": f"Purged {purged_count} items from recycle bin",
+ }
+
+ # Handle purging expired items
+ if purge_expired:
+ purged_count = recycle_bin.purge_expired_items()
+
+ return {
+ "status": "success",
+ "purged_count": purged_count,
+ "message": f"Purged {purged_count} expired items from recycle bin",
+ }
+
+ # Handle purging a specific item
+ if not item_id:
+ self.request.response.setStatus(400)
+ return {
+ "error": {
+ "type": "BadRequest",
+ "message": "Missing required parameter: item_id, purge_all, or purge_expired",
+ }
+ }
+
+ # Get the item to purge
+ item_data = recycle_bin.get_item(item_id)
+ if not item_data:
+ self.request.response.setStatus(404)
+ return {
+ "error": {
+ "type": "NotFound",
+ "message": f"Item with ID {item_id} not found in recycle bin",
+ }
+ }
+
+ # Purge the item
+ success = recycle_bin.purge_item(item_id)
+
+ if not success:
+ self.request.response.setStatus(500)
+ return {
+ "error": {
+ "type": "InternalServerError",
+ "message": "Failed to purge item",
+ }
+ }
+
+ return {
+ "status": "success",
+ "message": f"Item {item_data['id']} purged successfully",
+ }
diff --git a/src/plone/restapi/services/recyclebin/restore.py b/src/plone/restapi/services/recyclebin/restore.py
new file mode 100644
index 000000000..0a38314bd
--- /dev/null
+++ b/src/plone/restapi/services/recyclebin/restore.py
@@ -0,0 +1,95 @@
+from plone.restapi.deserializer import json_body
+from plone.restapi.services import Service
+from Products.CMFPlone.interfaces.recyclebin import IRecycleBin
+from zope.component import getUtility
+from zope.interface import alsoProvides
+
+import plone.protect.interfaces
+
+
+class RecycleBinRestore(Service):
+ """POST /@recyclebin-restore - Restore an item from the recycle bin"""
+
+ def __init__(self, context, request):
+ super().__init__(context, request)
+ self.params = {}
+
+ def reply(self):
+ # Disable CSRF protection for this request
+ alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection)
+
+ data = json_body(self.request)
+ item_id = data.get("item_id", None)
+
+ if not item_id:
+ self.request.response.setStatus(400)
+ return {
+ "error": {
+ "type": "BadRequest",
+ "message": "Missing required parameter: item_id",
+ }
+ }
+
+ recycle_bin = getUtility(IRecycleBin)
+
+ # Check if recycle bin is enabled
+ if not recycle_bin.is_enabled():
+ self.request.response.setStatus(404)
+ return {
+ "error": {
+ "type": "NotFound",
+ "message": "Recycle bin is disabled",
+ }
+ }
+
+ # Get the item to restore
+ item_data = recycle_bin.get_item(item_id)
+ if not item_data:
+ self.request.response.setStatus(404)
+ return {
+ "error": {
+ "type": "NotFound",
+ "message": f"Item with ID {item_id} not found in recycle bin",
+ }
+ }
+
+ # Get optional target container path
+ target_path = data.get("target_path", None)
+ target_container = None
+
+ if target_path:
+ try:
+ portal = self.context.portal_url.getPortalObject()
+ target_container = portal.unrestrictedTraverse(target_path)
+ except (KeyError, AttributeError):
+ self.request.response.setStatus(400)
+ return {
+ "error": {
+ "type": "BadRequest",
+ "message": f"Target path {target_path} not found",
+ }
+ }
+
+ # Restore the item
+ restored_obj = recycle_bin.restore_item(item_id, target_container)
+
+ if not restored_obj:
+ self.request.response.setStatus(500)
+ return {
+ "error": {
+ "type": "InternalServerError",
+ "message": "Failed to restore item",
+ }
+ }
+
+ self.request.response.setStatus(200)
+ return {
+ "status": "success",
+ "message": f"Item {item_data['id']} restored successfully",
+ "restored_item": {
+ "@id": restored_obj.absolute_url(),
+ "id": restored_obj.getId(),
+ "title": restored_obj.Title(),
+ "type": restored_obj.portal_type,
+ },
+ }
diff --git a/src/plone/restapi/tests/test_recyclebin.py b/src/plone/restapi/tests/test_recyclebin.py
new file mode 100644
index 000000000..056d8933f
--- /dev/null
+++ b/src/plone/restapi/tests/test_recyclebin.py
@@ -0,0 +1,420 @@
+from datetime import datetime
+from plone.app.testing import setRoles
+from plone.app.testing import SITE_OWNER_NAME
+from plone.app.testing import SITE_OWNER_PASSWORD
+from plone.app.testing import TEST_USER_ID
+from plone.restapi.testing import RelativeSession
+from unittest import mock
+
+import plone.restapi.testing
+import transaction
+import unittest
+
+
+class TestRecycleBin(unittest.TestCase):
+ layer = plone.restapi.testing.PLONE_RESTAPI_DX_FUNCTIONAL_TESTING
+
+ def setUp(self):
+ self.portal = self.layer["portal"]
+ self.request = self.layer["request"]
+ self.api_session = RelativeSession(self.portal.absolute_url())
+ self.api_session.headers.update({"Accept": "application/json"})
+ self.api_session.auth = (SITE_OWNER_NAME, SITE_OWNER_PASSWORD)
+ # For POST requests, set Content-Type header
+ self.api_session.headers.update({"Content-Type": "application/json"})
+ setRoles(self.portal, TEST_USER_ID, ["Manager"])
+ transaction.commit()
+
+ def tearDown(self):
+ self.api_session.close()
+
+ # GET tests (from TestRecycleBinGet)
+ def test_recyclebin_get_disabled(self):
+ """Test GET /@recyclebin when recycle bin is disabled"""
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = False
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.get.getUtility", return_value=recycle_bin
+ ):
+ response = self.api_session.get("/@recyclebin")
+
+ self.assertEqual(404, response.status_code)
+ self.assertEqual("NotFound", response.json()["error"]["type"])
+ self.assertEqual("Recycle bin is disabled", response.json()["error"]["message"])
+
+ def test_recyclebin_get_enabled_empty(self):
+ """Test GET /@recyclebin when recycle bin is enabled but empty"""
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ recycle_bin.get_items.return_value = []
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.get.getUtility", return_value=recycle_bin
+ ):
+ response = self.api_session.get("/@recyclebin")
+
+ self.assertEqual(200, response.status_code)
+ result = response.json()
+ self.assertEqual(self.portal.absolute_url() + "/@recyclebin", result["@id"])
+ self.assertEqual(0, result["items_total"])
+ self.assertEqual([], result["items"])
+
+ def test_recyclebin_get_enabled_with_items(self):
+ """Test GET /@recyclebin when recycle bin has items"""
+ sample_date = datetime(2023, 1, 1, 12, 0, 0)
+ mock_items = [
+ {
+ "id": "document1",
+ "title": "Test Document",
+ "type": "Document",
+ "path": "/plone/document1",
+ "parent_path": "/plone",
+ "deletion_date": sample_date,
+ "size": 1024,
+ "recycle_id": "123456789",
+ },
+ {
+ "id": "folder1",
+ "title": "Test Folder",
+ "type": "Folder",
+ "path": "/plone/folder1",
+ "parent_path": "/plone",
+ "deletion_date": sample_date,
+ "size": 512,
+ "recycle_id": "987654321",
+ },
+ ]
+
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ recycle_bin.get_items.return_value = mock_items
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.get.getUtility", return_value=recycle_bin
+ ):
+ response = self.api_session.get("/@recyclebin")
+
+ self.assertEqual(200, response.status_code)
+ result = response.json()
+ self.assertEqual(self.portal.absolute_url() + "/@recyclebin", result["@id"])
+ self.assertEqual(2, result["items_total"])
+
+ # Check first item
+ item1 = result["items"][0]
+ self.assertEqual("document1", item1["id"])
+ self.assertEqual("Test Document", item1["title"])
+ self.assertEqual("Document", item1["type"])
+ self.assertEqual("/plone/document1", item1["path"])
+ self.assertEqual("/plone", item1["parent_path"])
+ self.assertEqual(sample_date.isoformat(), item1["deletion_date"])
+ self.assertEqual(1024, item1["size"])
+ self.assertEqual("123456789", item1["recycle_id"])
+ self.assertEqual(
+ self.portal.absolute_url() + "/@recyclebin/123456789", item1["@id"]
+ )
+
+ # Verify actions
+ self.assertEqual(
+ self.portal.absolute_url() + "/@recyclebin-restore",
+ item1["actions"]["restore"],
+ )
+ self.assertEqual(
+ self.portal.absolute_url() + "/@recyclebin-purge", item1["actions"]["purge"]
+ )
+
+ # RESTORE tests (from TestRecycleBinRestore)
+ def test_restore_missing_item_id(self):
+ """Test restore with missing item_id parameter"""
+ response = self.api_session.post("/@recyclebin-restore", json={})
+
+ self.assertEqual(400, response.status_code)
+ self.assertEqual("BadRequest", response.json()["error"]["type"])
+ self.assertEqual(
+ "Missing required parameter: item_id", response.json()["error"]["message"]
+ )
+
+ def test_restore_disabled_recyclebin(self):
+ """Test restore when recyclebin is disabled"""
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = False
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.restore.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-restore", json={"item_id": "123456789"}
+ )
+
+ self.assertEqual(404, response.status_code)
+ self.assertEqual("NotFound", response.json()["error"]["type"])
+ self.assertEqual("Recycle bin is disabled", response.json()["error"]["message"])
+
+ def test_restore_nonexistent_item(self):
+ """Test restore for a non-existent item"""
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ recycle_bin.get_item.return_value = None
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.restore.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-restore", json={"item_id": "nonexistent"}
+ )
+
+ self.assertEqual(404, response.status_code)
+ self.assertEqual("NotFound", response.json()["error"]["type"])
+ self.assertEqual(
+ "Item with ID nonexistent not found in recycle bin",
+ response.json()["error"]["message"],
+ )
+
+ def test_restore_invalid_target_path(self):
+ """Test restore with an invalid target path"""
+ item_data = {
+ "id": "document1",
+ "title": "Test Document",
+ "recycle_id": "123456789",
+ }
+
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ recycle_bin.get_item.return_value = item_data
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.restore.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-restore",
+ json={"item_id": "123456789", "target_path": "/non/existent/path"},
+ )
+
+ self.assertEqual(400, response.status_code)
+ self.assertEqual("BadRequest", response.json()["error"]["type"])
+ self.assertEqual(
+ "Target path /non/existent/path not found",
+ response.json()["error"]["message"],
+ )
+
+ def test_restore_failure(self):
+ """Test when restore operation fails"""
+ item_data = {
+ "id": "document1",
+ "title": "Test Document",
+ "recycle_id": "123456789",
+ }
+
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ recycle_bin.get_item.return_value = item_data
+ recycle_bin.restore_item.return_value = None
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.restore.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-restore", json={"item_id": "123456789"}
+ )
+
+ self.assertEqual(500, response.status_code)
+ self.assertEqual("InternalServerError", response.json()["error"]["type"])
+ self.assertEqual("Failed to restore item", response.json()["error"]["message"])
+
+ def test_restore_success(self):
+ """Test successful item restoration"""
+ item_data = {
+ "id": "document1",
+ "title": "Test Document",
+ "recycle_id": "123456789",
+ }
+
+ # Mock restored object
+ mock_obj = mock.Mock()
+ mock_obj.absolute_url.return_value = "http://localhost:8080/plone/document1"
+ mock_obj.getId.return_value = "document1"
+ mock_obj.Title.return_value = "Test Document"
+ mock_obj.portal_type = "Document"
+
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ recycle_bin.get_item.return_value = item_data
+ recycle_bin.restore_item.return_value = mock_obj
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.restore.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-restore", json={"item_id": "123456789"}
+ )
+
+ self.assertEqual(200, response.status_code)
+ result = response.json()
+ self.assertEqual("success", result["status"])
+ self.assertEqual("Item document1 restored successfully", result["message"])
+
+ # Verify restored item data
+ restored = result["restored_item"]
+ self.assertEqual("http://localhost:8080/plone/document1", restored["@id"])
+ self.assertEqual("document1", restored["id"])
+ self.assertEqual("Test Document", restored["title"])
+ self.assertEqual("Document", restored["type"])
+
+ # PURGE tests (from TestRecycleBinPurge)
+ def test_purge_missing_parameters(self):
+ """Test purge with missing parameters"""
+ response = self.api_session.post("/@recyclebin-purge", json={})
+
+ self.assertEqual(400, response.status_code)
+ self.assertEqual("BadRequest", response.json()["error"]["type"])
+ self.assertEqual(
+ "Missing required parameter: item_id, purge_all, or purge_expired",
+ response.json()["error"]["message"],
+ )
+
+ def test_purge_disabled_recyclebin(self):
+ """Test purge when recyclebin is disabled"""
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = False
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.purge.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-purge", json={"item_id": "123456789"}
+ )
+
+ self.assertEqual(404, response.status_code)
+ self.assertEqual("NotFound", response.json()["error"]["type"])
+ self.assertEqual("Recycle bin is disabled", response.json()["error"]["message"])
+
+ def test_purge_nonexistent_item(self):
+ """Test purge for a non-existent item"""
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ recycle_bin.get_item.return_value = None
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.purge.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-purge", json={"item_id": "nonexistent"}
+ )
+
+ self.assertEqual(404, response.status_code)
+ self.assertEqual("NotFound", response.json()["error"]["type"])
+ self.assertEqual(
+ "Item with ID nonexistent not found in recycle bin",
+ response.json()["error"]["message"],
+ )
+
+ def test_purge_failure(self):
+ """Test when purge operation fails"""
+ item_data = {
+ "id": "document1",
+ "title": "Test Document",
+ "recycle_id": "123456789",
+ }
+
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ recycle_bin.get_item.return_value = item_data
+ recycle_bin.purge_item.return_value = False
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.purge.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-purge", json={"item_id": "123456789"}
+ )
+
+ self.assertEqual(500, response.status_code)
+ self.assertEqual("InternalServerError", response.json()["error"]["type"])
+ self.assertEqual("Failed to purge item", response.json()["error"]["message"])
+
+ def test_purge_success(self):
+ """Test successful item purge"""
+ item_data = {
+ "id": "document1",
+ "title": "Test Document",
+ "recycle_id": "123456789",
+ }
+
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ recycle_bin.get_item.return_value = item_data
+ recycle_bin.purge_item.return_value = True
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.purge.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-purge", json={"item_id": "123456789"}
+ )
+
+ self.assertEqual(200, response.status_code)
+ result = response.json()
+ self.assertEqual("success", result["status"])
+ self.assertEqual("Item document1 purged successfully", result["message"])
+
+ def test_purge_all(self):
+ """Test purging all items"""
+ mock_items = [
+ {"id": "document1", "recycle_id": "123"},
+ {"id": "document2", "recycle_id": "456"},
+ {"id": "document3", "recycle_id": "789"},
+ ]
+
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ recycle_bin.get_items.return_value = mock_items
+ # Configure purge_item to return True for successful purge
+ recycle_bin.purge_item.return_value = True
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.purge.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-purge", json={"purge_all": True}
+ )
+
+ self.assertEqual(200, response.status_code)
+ result = response.json()
+ self.assertEqual("success", result["status"])
+ self.assertEqual(3, result["purged_count"])
+ self.assertEqual("Purged 3 items from recycle bin", result["message"])
+ # Verify that purge_item was called for each item
+ recycle_bin.purge_item.assert_any_call("123")
+ recycle_bin.purge_item.assert_any_call("456")
+ recycle_bin.purge_item.assert_any_call("789")
+
+ def test_purge_expired(self):
+ """Test purging expired items"""
+ recycle_bin = mock.Mock()
+ recycle_bin.is_enabled.return_value = True
+ # Configure purge_expired_items to return the number of purged items
+ recycle_bin.purge_expired_items.return_value = 2
+
+ with mock.patch(
+ "plone.restapi.services.recyclebin.purge.getUtility",
+ return_value=recycle_bin,
+ ):
+ response = self.api_session.post(
+ "/@recyclebin-purge", json={"purge_expired": True}
+ )
+
+ self.assertEqual(200, response.status_code)
+ result = response.json()
+ self.assertEqual("success", result["status"])
+ self.assertEqual(2, result["purged_count"])
+ self.assertEqual("Purged 2 expired items from recycle bin", result["message"])