diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fcc108d..e0a3c298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to ### Fixed +- 🐛(backend) find deleted root items when searching the trashbin - 🐛(backend) exclude folders from file type search results - 🐛(frontend) keep uploaded items usable while malware analysis runs - 🐛(backend) stream export files from S3 without buffering diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index cfc9888a..d5945f49 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -1356,11 +1356,14 @@ class ItemViewSet( workspace = filterset.form.cleaned_data.get("workspace") - # First look for all top level items user has access to + # First look for all top level items user has access to. Soft deleted items + # are kept: a deleted root item is its own access holder and would become + # unreachable, even from the trashbin. The scope filter excludes them from + # the results. user = request.user item_access_queryset = models.ItemAccess.objects.select_related("item").filter( db.Q(user=user) | db.Q(team__in=user.teams), - item__deleted_at__isnull=True, + item__hard_deleted_at__isnull=True, ) # Remove items with upload_state SUSPICIOUS for non-creators diff --git a/src/backend/core/tests/items/test_api_items_search.py b/src/backend/core/tests/items/test_api_items_search.py index 8f9e5ab7..3595067d 100644 --- a/src/backend/core/tests/items/test_api_items_search.py +++ b/src/backend/core/tests/items/test_api_items_search.py @@ -825,3 +825,49 @@ def test_api_items_search_filter_category_excludes_folders(): assert response.status_code == 200 assert [item["id"] for item in response.json()["results"]] == [str(spreadsheet.id)] + + +def test_api_items_search_trashbin_finds_deleted_root_item(): + """A deleted root item should still be reachable from the trashbin location.""" + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + # A root item carries its own access: deleting it removes the only access + # pointing at it. + item = factories.ItemFactory( + title="ywh_export_report.pdf", + filename="ywh_export_report.pdf", + creator=user, + users=[(user, models.RoleChoices.OWNER)], + type=models.ItemTypeChoices.FILE, + update_upload_state=models.ItemUploadStateChoices.READY, + ) + item.soft_delete() + + response = client.get("/api/v1.0/items/search/?location=trashbin&title=ywh_export") + + assert response.status_code == 200 + assert [result["id"] for result in response.json()["results"]] == [str(item.id)] + + +def test_api_items_search_excludes_deleted_root_item_by_default(): + """A deleted root item should stay out of the search results without a scope.""" + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + item = factories.ItemFactory( + title="ywh_export_report.pdf", + filename="ywh_export_report.pdf", + creator=user, + users=[(user, models.RoleChoices.OWNER)], + type=models.ItemTypeChoices.FILE, + update_upload_state=models.ItemUploadStateChoices.READY, + ) + item.soft_delete() + + response = client.get("/api/v1.0/items/search/?title=ywh_export") + + assert response.status_code == 200 + assert response.json()["results"] == []