|
1 | 1 | import uuid |
| 2 | +from unittest import mock |
2 | 3 |
|
3 | 4 | from django.urls import reverse |
4 | 5 | from le_utils.constants import content_kinds |
|
12 | 13 | from contentcuration.tests.viewsets.base import generate_delete_event |
13 | 14 | from contentcuration.tests.viewsets.base import generate_update_event |
14 | 15 | from contentcuration.tests.viewsets.base import SyncTestMixin |
| 16 | +from contentcuration.viewsets.file import FileUploadURLSerializer |
| 17 | +from contentcuration.viewsets.file import MAX_NON_RESUMABLE_UPLOAD_SIZE |
15 | 18 | from contentcuration.viewsets.sync.constants import CONTENTNODE |
16 | 19 | from contentcuration.viewsets.sync.constants import FILE |
17 | 20 |
|
@@ -546,6 +549,9 @@ def test_mismatched_preset_upload(self): |
546 | 549 |
|
547 | 550 | def test_insufficient_storage(self): |
548 | 551 | self.file["size"] = 100000000000000 |
| 552 | + self.file[ |
| 553 | + "resumable" |
| 554 | + ] = True # resumable bypasses the >500MB guard so this still exercises the quota (412) path |
549 | 555 |
|
550 | 556 | self.client.force_authenticate(user=self.user) |
551 | 557 | response = self.client.post( |
@@ -590,6 +596,26 @@ def test_duration_zero(self): |
590 | 596 |
|
591 | 597 | self.assertEqual(response.status_code, 400) |
592 | 598 |
|
| 599 | + def test_fractional_size_rejected(self): |
| 600 | + s = FileUploadURLSerializer(data={**self.file, "size": 1000.5}) |
| 601 | + assert not s.is_valid() |
| 602 | + assert "size" in s.errors |
| 603 | + |
| 604 | + def test_large_non_resumable_rejected(self): |
| 605 | + self.file["size"] = MAX_NON_RESUMABLE_UPLOAD_SIZE + 1 |
| 606 | + self.client.force_authenticate(user=self.user) |
| 607 | + resp = self.client.post(reverse("file-upload-url"), self.file, format="json") |
| 608 | + assert resp.status_code == 400 |
| 609 | + |
| 610 | + def test_large_resumable_allowed(self): |
| 611 | + self.user.disk_space = 10 * 1024 * 1024 * 1024 |
| 612 | + self.user.save() |
| 613 | + self.file["size"] = MAX_NON_RESUMABLE_UPLOAD_SIZE + 1 |
| 614 | + self.file["resumable"] = True |
| 615 | + self.client.force_authenticate(user=self.user) |
| 616 | + resp = self.client.post(reverse("file-upload-url"), self.file, format="json") |
| 617 | + assert resp.status_code == 200 |
| 618 | + |
593 | 619 |
|
594 | 620 | class ContentIDTestCase(SyncTestMixin, StudioAPITestCase): |
595 | 621 | def setUp(self): |
@@ -763,3 +789,60 @@ def test_content_id__thumbnails_dont_update_content_id(self): |
763 | 789 | self.assertEqual( |
764 | 790 | copied_node_content_id_before_upload, copied_node_content_id_after_upload |
765 | 791 | ) |
| 792 | + |
| 793 | + |
| 794 | +class ResumableUploadURLTestCase(StudioAPITestCase): |
| 795 | + def setUp(self): |
| 796 | + super(ResumableUploadURLTestCase, self).setUp() |
| 797 | + self.user = testdata.user() |
| 798 | + # Give user enough quota to handle resumable uploads |
| 799 | + self.user.disk_space = 10 * 1024 * 1024 * 1024 |
| 800 | + self.user.save() |
| 801 | + self.file = { |
| 802 | + "size": 1000, |
| 803 | + "checksum": uuid.uuid4().hex, |
| 804 | + "name": "le_studio", |
| 805 | + "file_format": file_formats.MP3, |
| 806 | + "preset": format_presets.AUDIO, |
| 807 | + "duration": 10.123, |
| 808 | + "resumable": True, |
| 809 | + } |
| 810 | + |
| 811 | + @mock.patch("contentcuration.viewsets.file.default_storage") |
| 812 | + def test_resumable_returns_session_when_not_stored(self, mock_storage): |
| 813 | + mock_storage.get_stored_object_md5.return_value = None |
| 814 | + mock_storage.create_resumable_upload_session.return_value = ( |
| 815 | + "https://session.url" |
| 816 | + ) |
| 817 | + self.client.force_authenticate(user=self.user) |
| 818 | + resp = self.client.post(reverse("file-upload-url"), self.file, format="json") |
| 819 | + assert resp.status_code == 200 |
| 820 | + data = resp.json() |
| 821 | + assert data["resumable"] is True |
| 822 | + assert data["uploadURL"] == "https://session.url" |
| 823 | + assert data["alreadyUploaded"] is False |
| 824 | + assert "file" in data |
| 825 | + assert data["file"]["id"] |
| 826 | + mock_storage.create_resumable_upload_session.assert_called_once() |
| 827 | + |
| 828 | + @mock.patch("contentcuration.viewsets.file.default_storage") |
| 829 | + def test_resumable_skips_when_already_stored(self, mock_storage): |
| 830 | + mock_storage.get_stored_object_md5.return_value = self.file["checksum"] |
| 831 | + self.client.force_authenticate(user=self.user) |
| 832 | + resp = self.client.post(reverse("file-upload-url"), self.file, format="json") |
| 833 | + data = resp.json() |
| 834 | + assert data["resumable"] is True and data["alreadyUploaded"] is True |
| 835 | + assert data["uploadURL"] is None |
| 836 | + assert "file" in data |
| 837 | + assert data["file"]["id"] |
| 838 | + mock_storage.create_resumable_upload_session.assert_not_called() |
| 839 | + |
| 840 | + def test_resumable_falls_back_to_single_put_on_s3(self): |
| 841 | + # default_storage is S3 in the test env → no resumable support |
| 842 | + self.client.force_authenticate(user=self.user) |
| 843 | + resp = self.client.post(reverse("file-upload-url"), self.file, format="json") |
| 844 | + data = resp.json() |
| 845 | + assert data["resumable"] is False |
| 846 | + assert "uploadURL" in data |
| 847 | + assert "file" in data |
| 848 | + assert data["file"]["id"] |
0 commit comments