|
@@ -1,10 +1,12 @@
|
|
|
from types import SimpleNamespace
|
|
from types import SimpleNamespace
|
|
|
|
|
+from unittest.mock import patch
|
|
|
|
|
|
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.contrib.contenttypes.models import ContentType
|
|
|
|
|
+from django.core.files.storage import Storage
|
|
|
from django.test import TestCase
|
|
from django.test import TestCase
|
|
|
|
|
|
|
|
-from extras.models import ExportTemplate
|
|
|
|
|
-from extras.utils import filename_from_model, image_upload
|
|
|
|
|
|
|
+from extras.models import ExportTemplate, ImageAttachment
|
|
|
|
|
+from extras.utils import _build_image_attachment_path, filename_from_model, image_upload
|
|
|
from tenancy.models import ContactGroup, TenantGroup
|
|
from tenancy.models import ContactGroup, TenantGroup
|
|
|
from wireless.models import WirelessLANGroup
|
|
from wireless.models import WirelessLANGroup
|
|
|
|
|
|
|
@@ -22,6 +24,25 @@ class FilenameFromModelTests(TestCase):
|
|
|
self.assertEqual(filename_from_model(model), expected)
|
|
self.assertEqual(filename_from_model(model), expected)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+class OverwriteStyleStorage(Storage):
|
|
|
|
|
+ """
|
|
|
|
|
+ Mimic an overwrite-style backend (for example, S3 with file_overwrite=True),
|
|
|
|
|
+ where get_available_name() returns the incoming name unchanged.
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ def __init__(self, existing_names=None):
|
|
|
|
|
+ self.existing_names = set(existing_names or [])
|
|
|
|
|
+
|
|
|
|
|
+ def exists(self, name):
|
|
|
|
|
+ return name in self.existing_names
|
|
|
|
|
+
|
|
|
|
|
+ def get_available_name(self, name, max_length=None):
|
|
|
|
|
+ return name
|
|
|
|
|
+
|
|
|
|
|
+ def get_alternative_name(self, file_root, file_ext):
|
|
|
|
|
+ return f'{file_root}_sdmmer4{file_ext}'
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class ImageUploadTests(TestCase):
|
|
class ImageUploadTests(TestCase):
|
|
|
@classmethod
|
|
@classmethod
|
|
|
def setUpTestData(cls):
|
|
def setUpTestData(cls):
|
|
@@ -31,16 +52,18 @@ class ImageUploadTests(TestCase):
|
|
|
|
|
|
|
|
def _stub_instance(self, object_id=12, name=None):
|
|
def _stub_instance(self, object_id=12, name=None):
|
|
|
"""
|
|
"""
|
|
|
- Creates a minimal stub for use with the `image_upload()` function.
|
|
|
|
|
-
|
|
|
|
|
- This method generates an instance of `SimpleNamespace` containing a set
|
|
|
|
|
- of attributes required to simulate the expected input for the
|
|
|
|
|
- `image_upload()` method.
|
|
|
|
|
- It is designed to simplify testing or processing by providing a
|
|
|
|
|
- lightweight representation of an object.
|
|
|
|
|
|
|
+ Creates a minimal stub for use with image attachment path generation.
|
|
|
"""
|
|
"""
|
|
|
return SimpleNamespace(object_type=self.ct_rack, object_id=object_id, name=name)
|
|
return SimpleNamespace(object_type=self.ct_rack, object_id=object_id, name=name)
|
|
|
|
|
|
|
|
|
|
+ def _bound_instance(self, *, storage, object_id=12, name=None, max_length=100):
|
|
|
|
|
+ return SimpleNamespace(
|
|
|
|
|
+ object_type=self.ct_rack,
|
|
|
|
|
+ object_id=object_id,
|
|
|
|
|
+ name=name,
|
|
|
|
|
+ image=SimpleNamespace(field=SimpleNamespace(storage=storage, max_length=max_length)),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
def _second_segment(self, path: str):
|
|
def _second_segment(self, path: str):
|
|
|
"""
|
|
"""
|
|
|
Extracts and returns the portion of the input string after the
|
|
Extracts and returns the portion of the input string after the
|
|
@@ -53,7 +76,7 @@ class ImageUploadTests(TestCase):
|
|
|
Tests handling of a Windows file path with a fake directory and extension.
|
|
Tests handling of a Windows file path with a fake directory and extension.
|
|
|
"""
|
|
"""
|
|
|
inst = self._stub_instance(name=None)
|
|
inst = self._stub_instance(name=None)
|
|
|
- path = image_upload(inst, r'C:\fake_path\MyPhoto.JPG')
|
|
|
|
|
|
|
+ path = _build_image_attachment_path(inst, r'C:\fake_path\MyPhoto.JPG')
|
|
|
# Base directory and single-level path
|
|
# Base directory and single-level path
|
|
|
seg2 = self._second_segment(path)
|
|
seg2 = self._second_segment(path)
|
|
|
self.assertTrue(path.startswith('image-attachments/rack_12_'))
|
|
self.assertTrue(path.startswith('image-attachments/rack_12_'))
|
|
@@ -67,7 +90,7 @@ class ImageUploadTests(TestCase):
|
|
|
create subdirectories.
|
|
create subdirectories.
|
|
|
"""
|
|
"""
|
|
|
inst = self._stub_instance(name='5/31/23')
|
|
inst = self._stub_instance(name='5/31/23')
|
|
|
- path = image_upload(inst, 'image.png')
|
|
|
|
|
|
|
+ path = _build_image_attachment_path(inst, 'image.png')
|
|
|
seg2 = self._second_segment(path)
|
|
seg2 = self._second_segment(path)
|
|
|
self.assertTrue(seg2.startswith('rack_12_'))
|
|
self.assertTrue(seg2.startswith('rack_12_'))
|
|
|
self.assertNotIn('/', seg2)
|
|
self.assertNotIn('/', seg2)
|
|
@@ -80,7 +103,7 @@ class ImageUploadTests(TestCase):
|
|
|
into a single directory name without creating subdirectories.
|
|
into a single directory name without creating subdirectories.
|
|
|
"""
|
|
"""
|
|
|
inst = self._stub_instance(name=r'5\31\23')
|
|
inst = self._stub_instance(name=r'5\31\23')
|
|
|
- path = image_upload(inst, 'image_name.png')
|
|
|
|
|
|
|
+ path = _build_image_attachment_path(inst, 'image_name.png')
|
|
|
|
|
|
|
|
seg2 = self._second_segment(path)
|
|
seg2 = self._second_segment(path)
|
|
|
self.assertTrue(seg2.startswith('rack_12_'))
|
|
self.assertTrue(seg2.startswith('rack_12_'))
|
|
@@ -93,7 +116,7 @@ class ImageUploadTests(TestCase):
|
|
|
Tests the output path format generated by the `image_upload` function.
|
|
Tests the output path format generated by the `image_upload` function.
|
|
|
"""
|
|
"""
|
|
|
inst = self._stub_instance(object_id=99, name='label')
|
|
inst = self._stub_instance(object_id=99, name='label')
|
|
|
- path = image_upload(inst, 'a.webp')
|
|
|
|
|
|
|
+ path = _build_image_attachment_path(inst, 'a.webp')
|
|
|
# The second segment must begin with "rack_99_"
|
|
# The second segment must begin with "rack_99_"
|
|
|
seg2 = self._second_segment(path)
|
|
seg2 = self._second_segment(path)
|
|
|
self.assertTrue(seg2.startswith('rack_99_'))
|
|
self.assertTrue(seg2.startswith('rack_99_'))
|
|
@@ -105,7 +128,7 @@ class ImageUploadTests(TestCase):
|
|
|
is omitted.
|
|
is omitted.
|
|
|
"""
|
|
"""
|
|
|
inst = self._stub_instance(name='test')
|
|
inst = self._stub_instance(name='test')
|
|
|
- path = image_upload(inst, 'document.txt')
|
|
|
|
|
|
|
+ path = _build_image_attachment_path(inst, 'document.txt')
|
|
|
|
|
|
|
|
seg2 = self._second_segment(path)
|
|
seg2 = self._second_segment(path)
|
|
|
self.assertTrue(seg2.startswith('rack_12_test'))
|
|
self.assertTrue(seg2.startswith('rack_12_test'))
|
|
@@ -121,7 +144,7 @@ class ImageUploadTests(TestCase):
|
|
|
# Suppose the instance name has surrounding whitespace and
|
|
# Suppose the instance name has surrounding whitespace and
|
|
|
# extra slashes.
|
|
# extra slashes.
|
|
|
inst = self._stub_instance(name=' my/complex\\name ')
|
|
inst = self._stub_instance(name=' my/complex\\name ')
|
|
|
- path = image_upload(inst, 'irrelevant.png')
|
|
|
|
|
|
|
+ path = _build_image_attachment_path(inst, 'irrelevant.png')
|
|
|
|
|
|
|
|
# The output should be flattened and sanitized.
|
|
# The output should be flattened and sanitized.
|
|
|
# We expect the name to be transformed into a valid filename without
|
|
# We expect the name to be transformed into a valid filename without
|
|
@@ -141,7 +164,7 @@ class ImageUploadTests(TestCase):
|
|
|
for name in ['2025/09/12', r'2025\09\12']:
|
|
for name in ['2025/09/12', r'2025\09\12']:
|
|
|
with self.subTest(name=name):
|
|
with self.subTest(name=name):
|
|
|
inst = self._stub_instance(name=name)
|
|
inst = self._stub_instance(name=name)
|
|
|
- path = image_upload(inst, 'x.jpeg')
|
|
|
|
|
|
|
+ path = _build_image_attachment_path(inst, 'x.jpeg')
|
|
|
seg2 = self._second_segment(path)
|
|
seg2 = self._second_segment(path)
|
|
|
self.assertTrue(seg2.startswith('rack_12_'))
|
|
self.assertTrue(seg2.startswith('rack_12_'))
|
|
|
self.assertNotIn('/', seg2)
|
|
self.assertNotIn('/', seg2)
|
|
@@ -154,7 +177,49 @@ class ImageUploadTests(TestCase):
|
|
|
SuspiciousFileOperation, the fallback default is used.
|
|
SuspiciousFileOperation, the fallback default is used.
|
|
|
"""
|
|
"""
|
|
|
inst = self._stub_instance(name=' ')
|
|
inst = self._stub_instance(name=' ')
|
|
|
- path = image_upload(inst, 'sample.png')
|
|
|
|
|
|
|
+ path = _build_image_attachment_path(inst, 'sample.png')
|
|
|
# Expect the fallback name 'unnamed' to be used.
|
|
# Expect the fallback name 'unnamed' to be used.
|
|
|
self.assertIn('unnamed', path)
|
|
self.assertIn('unnamed', path)
|
|
|
self.assertTrue(path.startswith('image-attachments/rack_12_'))
|
|
self.assertTrue(path.startswith('image-attachments/rack_12_'))
|
|
|
|
|
+
|
|
|
|
|
+ def test_image_upload_preserves_original_name_when_available(self):
|
|
|
|
|
+ inst = self._bound_instance(
|
|
|
|
|
+ storage=OverwriteStyleStorage(),
|
|
|
|
|
+ name='action-buttons',
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ path = image_upload(inst, 'action-buttons.png')
|
|
|
|
|
+
|
|
|
|
|
+ self.assertEqual(path, 'image-attachments/rack_12_action-buttons.png')
|
|
|
|
|
+
|
|
|
|
|
+ def test_image_upload_uses_base_collision_handling_with_overwrite_style_storage(self):
|
|
|
|
|
+ inst = self._bound_instance(
|
|
|
|
|
+ storage=OverwriteStyleStorage(existing_names={'image-attachments/rack_12_action-buttons.png'}),
|
|
|
|
|
+ name='action-buttons',
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ path = image_upload(inst, 'action-buttons.png')
|
|
|
|
|
+
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ path,
|
|
|
|
|
+ 'image-attachments/rack_12_action-buttons_sdmmer4.png',
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def test_image_field_generate_filename_uses_image_upload_collision_handling(self):
|
|
|
|
|
+ field = ImageAttachment._meta.get_field('image')
|
|
|
|
|
+ instance = ImageAttachment(
|
|
|
|
|
+ object_type=self.ct_rack,
|
|
|
|
|
+ object_id=12,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ with patch.object(
|
|
|
|
|
+ field,
|
|
|
|
|
+ 'storage',
|
|
|
|
|
+ OverwriteStyleStorage(existing_names={'image-attachments/rack_12_action-buttons.png'}),
|
|
|
|
|
+ ):
|
|
|
|
|
+ path = field.generate_filename(instance, 'action-buttons.png')
|
|
|
|
|
+
|
|
|
|
|
+ self.assertEqual(
|
|
|
|
|
+ path,
|
|
|
|
|
+ 'image-attachments/rack_12_action-buttons_sdmmer4.png',
|
|
|
|
|
+ )
|