1
+ from types import SimpleNamespace
2
+
3
+ from django .contrib .contenttypes .models import ContentType
1
4
from django .test import TestCase
2
5
3
6
from extras .models import ExportTemplate
4
- from extras .utils import filename_from_model
7
+ from extras .utils import filename_from_model , image_upload
5
8
from tenancy .models import ContactGroup , TenantGroup
6
9
from wireless .models import WirelessLANGroup
7
10
@@ -17,3 +20,141 @@ def test_expected_output(self):
17
20
18
21
for model , expected in cases :
19
22
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