test_utils.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. from types import SimpleNamespace
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.test import TestCase
  4. from extras.models import ExportTemplate
  5. from extras.utils import filename_from_model, image_upload
  6. from tenancy.models import ContactGroup, TenantGroup
  7. from wireless.models import WirelessLANGroup
  8. class FilenameFromModelTests(TestCase):
  9. def test_expected_output(self):
  10. cases = (
  11. (ExportTemplate, 'netbox_export_templates'),
  12. (ContactGroup, 'netbox_contact_groups'),
  13. (TenantGroup, 'netbox_tenant_groups'),
  14. (WirelessLANGroup, 'netbox_wireless_lan_groups'),
  15. )
  16. for model, expected in cases:
  17. self.assertEqual(filename_from_model(model), expected)
  18. class ImageUploadTests(TestCase):
  19. @classmethod
  20. def setUpTestData(cls):
  21. # We only need a ContentType with model="rack" for the prefix;
  22. # this doesn't require creating a Rack object.
  23. cls.ct_rack = ContentType.objects.get_by_natural_key('dcim', 'rack')
  24. def _stub_instance(self, object_id=12, name=None):
  25. """
  26. Creates a minimal stub for use with the `image_upload()` function.
  27. This method generates an instance of `SimpleNamespace` containing a set
  28. of attributes required to simulate the expected input for the
  29. `image_upload()` method.
  30. It is designed to simplify testing or processing by providing a
  31. lightweight representation of an object.
  32. """
  33. return SimpleNamespace(object_type=self.ct_rack, object_id=object_id, name=name)
  34. def _second_segment(self, path: str):
  35. """
  36. Extracts and returns the portion of the input string after the
  37. first '/' character.
  38. """
  39. return path.split('/', 1)[1]
  40. def test_windows_fake_path_and_extension_lowercased(self):
  41. """
  42. Tests handling of a Windows file path with a fake directory and extension.
  43. """
  44. inst = self._stub_instance(name=None)
  45. path = image_upload(inst, r'C:\fake_path\MyPhoto.JPG')
  46. # Base directory and single-level path
  47. seg2 = self._second_segment(path)
  48. self.assertTrue(path.startswith('image-attachments/rack_12_'))
  49. self.assertNotIn('/', seg2, 'should not create nested directories')
  50. # Extension from the uploaded file, lowercased
  51. self.assertTrue(seg2.endswith('.jpg'))
  52. def test_name_with_slashes_is_flattened_no_subdirectories(self):
  53. """
  54. Tests that a name with slashes is flattened and does not
  55. create subdirectories.
  56. """
  57. inst = self._stub_instance(name='5/31/23')
  58. path = image_upload(inst, 'image.png')
  59. seg2 = self._second_segment(path)
  60. self.assertTrue(seg2.startswith('rack_12_'))
  61. self.assertNotIn('/', seg2)
  62. self.assertNotIn('\\', seg2)
  63. self.assertTrue(seg2.endswith('.png'))
  64. def test_name_with_backslashes_is_flattened_no_subdirectories(self):
  65. """
  66. Tests that a name including backslashes is correctly flattened
  67. into a single directory name without creating subdirectories.
  68. """
  69. inst = self._stub_instance(name=r'5\31\23')
  70. path = image_upload(inst, 'image_name.png')
  71. seg2 = self._second_segment(path)
  72. self.assertTrue(seg2.startswith('rack_12_'))
  73. self.assertNotIn('/', seg2)
  74. self.assertNotIn('\\', seg2)
  75. self.assertTrue(seg2.endswith('.png'))
  76. def test_prefix_format_is_as_expected(self):
  77. """
  78. Tests the output path format generated by the `image_upload` function.
  79. """
  80. inst = self._stub_instance(object_id=99, name='label')
  81. path = image_upload(inst, 'a.webp')
  82. # The second segment must begin with "rack_99_"
  83. seg2 = self._second_segment(path)
  84. self.assertTrue(seg2.startswith('rack_99_'))
  85. self.assertTrue(seg2.endswith('.webp'))
  86. def test_unsupported_file_extension(self):
  87. """
  88. Test that when the file extension is not allowed, the extension
  89. is omitted.
  90. """
  91. inst = self._stub_instance(name='test')
  92. path = image_upload(inst, 'document.txt')
  93. seg2 = self._second_segment(path)
  94. self.assertTrue(seg2.startswith('rack_12_test'))
  95. self.assertFalse(seg2.endswith('.txt'))
  96. # When not allowed, no extension should be appended
  97. self.assertNotRegex(seg2, r'\.txt$')
  98. def test_instance_name_with_whitespace_and_special_chars(self):
  99. """
  100. Test that an instance name with leading/trailing whitespace and
  101. special characters is sanitized properly.
  102. """
  103. # Suppose the instance name has surrounding whitespace and
  104. # extra slashes.
  105. inst = self._stub_instance(name=' my/complex\\name ')
  106. path = image_upload(inst, 'irrelevant.png')
  107. # The output should be flattened and sanitized.
  108. # We expect the name to be transformed into a valid filename without
  109. # path separators.
  110. seg2 = self._second_segment(path)
  111. self.assertNotIn(' ', seg2)
  112. self.assertNotIn('/', seg2)
  113. self.assertNotIn('\\', seg2)
  114. self.assertTrue(seg2.endswith('.png'))
  115. def test_separator_variants_with_subTest(self):
  116. """
  117. Tests that both forward slash and backslash in file paths are
  118. handled consistently by the `image_upload` function and
  119. processed into a sanitized uniform format.
  120. """
  121. for name in ['2025/09/12', r'2025\09\12']:
  122. with self.subTest(name=name):
  123. inst = self._stub_instance(name=name)
  124. path = image_upload(inst, 'x.jpeg')
  125. seg2 = self._second_segment(path)
  126. self.assertTrue(seg2.startswith('rack_12_'))
  127. self.assertNotIn('/', seg2)
  128. self.assertNotIn('\\', seg2)
  129. self.assertTrue(seg2.endswith('.jpeg') or seg2.endswith('.jpg'))
  130. def test_fallback_on_suspicious_file_operation(self):
  131. """
  132. Test that when default_storage.get_valid_name() raises a
  133. SuspiciousFileOperation, the fallback default is used.
  134. """
  135. inst = self._stub_instance(name=' ')
  136. path = image_upload(inst, 'sample.png')
  137. # Expect the fallback name 'unnamed' to be used.
  138. self.assertIn('unnamed', path)
  139. self.assertTrue(path.startswith('image-attachments/rack_12_'))