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