From 1510a7550ed80ee9907fd8dfb8d0fe6bfdfe6115 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Wed, 5 Nov 2025 20:02:19 -0800 Subject: [PATCH] Backend tests --- .../tests/test_api_share_link_bundles.py | 182 ++++++++++++++++++ 1 file changed, 182 insertions(+) create mode 100644 src/documents/tests/test_api_share_link_bundles.py diff --git a/src/documents/tests/test_api_share_link_bundles.py b/src/documents/tests/test_api_share_link_bundles.py new file mode 100644 index 000000000..a8b5a91fd --- /dev/null +++ b/src/documents/tests/test_api_share_link_bundles.py @@ -0,0 +1,182 @@ +from __future__ import annotations + +from datetime import timedelta +from pathlib import Path +from unittest import mock + +from django.contrib.auth.models import User +from django.utils import timezone +from rest_framework import status +from rest_framework.test import APITestCase + +from documents.models import ShareLink +from documents.models import ShareLinkBundle +from documents.tasks import cleanup_expired_share_link_bundles +from documents.tests.factories import DocumentFactory +from documents.tests.utils import DirectoriesMixin + + +class ShareLinkBundleAPITests(DirectoriesMixin, APITestCase): + ENDPOINT = "/api/share_link_bundles/" + + def setUp(self): + super().setUp() + self.user = User.objects.create_superuser(username="bundle_admin") + self.client.force_authenticate(self.user) + self.document = DocumentFactory.create() + + @mock.patch("documents.views.build_share_link_bundle.delay") + def test_create_bundle_triggers_build_job(self, delay_mock): + payload = { + "document_ids": [self.document.pk], + "file_version": ShareLink.FileVersion.ARCHIVE, + "expiration_days": 7, + } + + response = self.client.post(self.ENDPOINT, payload, format="json") + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + bundle = ShareLinkBundle.objects.get(pk=response.data["id"]) + self.assertEqual(bundle.documents.count(), 1) + self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING) + delay_mock.assert_called_once_with(bundle.pk) + + def test_create_bundle_rejects_missing_documents(self): + payload = { + "document_ids": [9999], + "file_version": ShareLink.FileVersion.ARCHIVE, + "expiration_days": 7, + } + + response = self.client.post(self.ENDPOINT, payload, format="json") + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("document_ids", response.data) + + @mock.patch("documents.views.build_share_link_bundle.delay") + def test_rebuild_bundle_resets_state(self, delay_mock): + bundle = ShareLinkBundle.objects.create( + slug="rebuild-slug", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.FAILED, + ) + bundle.documents.set([self.document]) + bundle.last_error = "Something went wrong" + bundle.size_bytes = 100 + bundle.file_path = "path/to/file.zip" + bundle.save() + + response = self.client.post(f"{self.ENDPOINT}{bundle.pk}/rebuild/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + bundle.refresh_from_db() + self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING) + self.assertEqual(bundle.last_error, "") + self.assertIsNone(bundle.size_bytes) + self.assertEqual(bundle.file_path, "") + delay_mock.assert_called_once_with(bundle.pk) + + def test_rebuild_bundle_rejects_processing_status(self): + bundle = ShareLinkBundle.objects.create( + slug="processing-slug", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.PROCESSING, + ) + bundle.documents.set([self.document]) + + response = self.client.post(f"{self.ENDPOINT}{bundle.pk}/rebuild/") + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("detail", response.data) + + def test_download_ready_bundle_streams_file(self): + bundle_file = Path(self.dirs.media_dir) / "bundles" / "ready.zip" + bundle_file.parent.mkdir(parents=True, exist_ok=True) + bundle_file.write_bytes(b"binary-zip-content") + + bundle = ShareLinkBundle.objects.create( + slug="readyslug", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.READY, + file_path=str(bundle_file), + ) + bundle.documents.set([self.document]) + + self.client.logout() + response = self.client.get(f"/share/{bundle.slug}/") + content = b"".join(response.streaming_content) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response["Content-Type"], "application/zip") + self.assertEqual(content, b"binary-zip-content") + self.assertIn("attachment;", response["Content-Disposition"]) + + def test_download_pending_bundle_returns_202(self): + bundle = ShareLinkBundle.objects.create( + slug="pendingslug", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.PENDING, + ) + bundle.documents.set([self.document]) + + self.client.logout() + response = self.client.get(f"/share/{bundle.slug}/") + + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + + @mock.patch("documents.views.build_share_link_bundle.delay") + def test_download_missing_file_triggers_rebuild(self, delay_mock): + bundle = ShareLinkBundle.objects.create( + slug="missingfileslug", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.READY, + file_path=str(Path(self.dirs.media_dir) / "does-not-exist.zip"), + ) + bundle.documents.set([self.document]) + + self.client.logout() + response = self.client.get(f"/share/{bundle.slug}/") + + self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) + bundle.refresh_from_db() + self.assertEqual(bundle.status, ShareLinkBundle.Status.PENDING) + delay_mock.assert_called_once_with(bundle.pk) + + +class ShareLinkBundleTaskTests(DirectoriesMixin, APITestCase): + def setUp(self): + super().setUp() + self.document = DocumentFactory.create() + + def test_cleanup_expired_share_link_bundles(self): + expired_path = Path(self.dirs.media_dir) / "expired.zip" + expired_path.parent.mkdir(parents=True, exist_ok=True) + expired_path.write_bytes(b"expired") + + active_path = Path(self.dirs.media_dir) / "active.zip" + active_path.write_bytes(b"active") + + expired_bundle = ShareLinkBundle.objects.create( + slug="expired-bundle", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.READY, + expiration=timezone.now() - timedelta(days=1), + file_path=str(expired_path), + ) + expired_bundle.documents.set([self.document]) + + active_bundle = ShareLinkBundle.objects.create( + slug="active-bundle", + file_version=ShareLink.FileVersion.ARCHIVE, + status=ShareLinkBundle.Status.READY, + expiration=timezone.now() + timedelta(days=1), + file_path=str(active_path), + ) + active_bundle.documents.set([self.document]) + + cleanup_expired_share_link_bundles() + + self.assertFalse(ShareLinkBundle.objects.filter(pk=expired_bundle.pk).exists()) + self.assertTrue(ShareLinkBundle.objects.filter(pk=active_bundle.pk).exists()) + self.assertFalse(expired_path.exists()) + self.assertTrue(active_path.exists())