55import re
66import uuid
77
8- from django .core .files .base import ContentFile
8+ from django .core .files .storage import default_storage
99from django .core .files .uploadedfile import SimpleUploadedFile
1010
1111import pytest
1616
1717pytestmark = pytest .mark .django_db
1818
19+ PIXEL = (
20+ b"\x89 PNG\r \n \x1a \n \x00 \x00 \x00 \r IHDR\x00 \x00 \x00 \x01 \x00 \x00 \x00 \x01 \x08 \x06 \x00 "
21+ b"\x00 \x00 \x1f \x15 \xc4 \x89 \x00 \x00 \x00 \n IDATx\x9c c\xf8 \xff \xff ?\x00 \x05 \xfe \x02 \xfe "
22+ b"\xa7 V\xbd \xfa \x00 \x00 \x00 \x00 IEND\xae B`\x82 "
23+ )
24+
1925
2026@pytest .mark .parametrize (
2127 "reach, role" ,
@@ -33,7 +39,7 @@ def test_api_documents_attachment_upload_anonymous_forbidden(reach, role):
3339 and role don't allow it.
3440 """
3541 document = factories .DocumentFactory (link_reach = reach , link_role = role )
36- file = SimpleUploadedFile ("test_file.jpg " , b"Dummy content" )
42+ file = SimpleUploadedFile (name = "test.png " , content = PIXEL , content_type = "image/png " )
3743
3844 url = f"/api/v1.0/documents/{ document .id !s} /attachment-upload/"
3945 response = APIClient ().post (url , {"file" : file }, format = "multipart" )
@@ -50,14 +56,14 @@ def test_api_documents_attachment_upload_anonymous_success():
5056 if the link reach and role permit it.
5157 """
5258 document = factories .DocumentFactory (link_reach = "public" , link_role = "editor" )
53- file = SimpleUploadedFile ("test_file.jpg " , b"Dummy content" )
59+ file = SimpleUploadedFile (name = "test.png " , content = PIXEL , content_type = "image/png " )
5460
5561 url = f"/api/v1.0/documents/{ document .id !s} /attachment-upload/"
5662 response = APIClient ().post (url , {"file" : file }, format = "multipart" )
5763
5864 assert response .status_code == 201
5965
60- pattern = re .compile (rf"^/media/{ document .id !s} /attachments/(.*)\.jpg " )
66+ pattern = re .compile (rf"^/media/{ document .id !s} /attachments/(.*)\.png " )
6167 match = pattern .search (response .json ()["file" ])
6268 file_id = match .group (1 )
6369
@@ -85,7 +91,7 @@ def test_api_documents_attachment_upload_authenticated_forbidden(reach, role):
8591 client .force_login (user )
8692
8793 document = factories .DocumentFactory (link_reach = reach , link_role = role )
88- file = SimpleUploadedFile ("test_file.jpg " , b"Dummy content" )
94+ file = SimpleUploadedFile (name = "test.png " , content = PIXEL , content_type = "image/png " )
8995
9096 url = f"/api/v1.0/documents/{ document .id !s} /attachment-upload/"
9197 response = client .post (url , {"file" : file }, format = "multipart" )
@@ -114,14 +120,14 @@ def test_api_documents_attachment_upload_authenticated_success(reach, role):
114120 client .force_login (user )
115121
116122 document = factories .DocumentFactory (link_reach = reach , link_role = role )
117- file = SimpleUploadedFile ("test_file.jpg " , b"Dummy content" )
123+ file = SimpleUploadedFile (name = "test.png " , content = PIXEL , content_type = "image/png " )
118124
119125 url = f"/api/v1.0/documents/{ document .id !s} /attachment-upload/"
120126 response = client .post (url , {"file" : file }, format = "multipart" )
121127
122128 assert response .status_code == 201
123129
124- pattern = re .compile (rf"^/media/{ document .id !s} /attachments/(.*)\.jpg " )
130+ pattern = re .compile (rf"^/media/{ document .id !s} /attachments/(.*)\.png " )
125131 match = pattern .search (response .json ()["file" ])
126132 file_id = match .group (1 )
127133
@@ -148,7 +154,7 @@ def test_api_documents_attachment_upload_reader(via, mock_user_teams):
148154 document = document , team = "lasuite" , role = "reader"
149155 )
150156
151- file = SimpleUploadedFile ("test_file.jpg " , b"Dummy content" )
157+ file = SimpleUploadedFile (name = "test.png " , content = PIXEL , content_type = "image/png " )
152158
153159 url = f"/api/v1.0/documents/{ document .id !s} /attachment-upload/"
154160 response = client .post (url , {"file" : file }, format = "multipart" )
@@ -179,20 +185,28 @@ def test_api_documents_attachment_upload_success(via, role, mock_user_teams):
179185 document = document , team = "lasuite" , role = role
180186 )
181187
182- file = SimpleUploadedFile ("test_file.jpg " , b"Dummy content" )
188+ file = SimpleUploadedFile (name = "test.png " , content = PIXEL , content_type = "image/png " )
183189
184190 url = f"/api/v1.0/documents/{ document .id !s} /attachment-upload/"
185191 response = client .post (url , {"file" : file }, format = "multipart" )
186192
187193 assert response .status_code == 201
188194
189- pattern = re .compile (rf"^/media/{ document .id !s} /attachments/(.*)\.jpg" )
190- match = pattern .search (response .json ()["file" ])
195+ file_path = response .json ()["file" ]
196+ pattern = re .compile (rf"^/media/{ document .id !s} /attachments/(.*)\.png" )
197+ match = pattern .search (file_path )
191198 file_id = match .group (1 )
192199
193200 # Validate that file_id is a valid UUID
194201 uuid .UUID (file_id )
195202
203+ # Now, check the metadata of the uploaded file
204+ key = file_path .replace ("/media" , "" )
205+ file_head = default_storage .connection .meta .client .head_object (
206+ Bucket = default_storage .bucket_name , Key = key
207+ )
208+ assert file_head ["Metadata" ] == {"owner" : str (user .id )}
209+
196210
197211def test_api_documents_attachment_upload_invalid (client ):
198212 """Attempt to upload without a file should return an explicit error."""
@@ -222,34 +236,102 @@ def test_api_documents_attachment_upload_size_limit_exceeded(settings):
222236 url = f"/api/v1.0/documents/{ document .id !s} /attachment-upload/"
223237
224238 # Create a temporary file larger than the allowed size
225- content = b"a" * (1048576 + 1 )
226- file = ContentFile (content , name = "test.jpg" )
239+ file = SimpleUploadedFile (
240+ name = "test.txt" , content = b"a" * (1048576 + 1 ), content_type = "text/plain"
241+ )
227242
228243 response = client .post (url , {"file" : file }, format = "multipart" )
229244
230245 assert response .status_code == 400
231246 assert response .json () == {"file" : ["File size exceeds the maximum limit of 1 MB." ]}
232247
233248
234- def test_api_documents_attachment_upload_type_not_allowed (settings ):
235- """The uploaded file should be of a whitelisted type."""
236- settings .DOCUMENT_IMAGE_ALLOWED_MIME_TYPES = ["image/jpeg" , "image/png" ]
237-
249+ @pytest .mark .parametrize (
250+ "name,content,extension" ,
251+ [
252+ ("test.exe" , b"text" , "exe" ),
253+ ("test" , b"text" , "txt" ),
254+ ("test.aaaaaa" , b"test" , "txt" ),
255+ ("test.txt" , PIXEL , "txt" ),
256+ ("test.py" , b"#!/usr/bin/python" , "py" ),
257+ ],
258+ )
259+ def test_api_documents_attachment_upload_fix_extension (name , content , extension ):
260+ """
261+ A file with no extension or a wrong extension is accepted and the extension
262+ is corrected in storage.
263+ """
238264 user = factories .UserFactory ()
239265 client = APIClient ()
240266 client .force_login (user )
241267
242268 document = factories .DocumentFactory (users = [(user , "owner" )])
243269 url = f"/api/v1.0/documents/{ document .id !s} /attachment-upload/"
244270
245- # Create a temporary file with a not allowed type (e.g., text file)
246- file = ContentFile (b"a" * 1048576 , name = "test.txt" )
271+ file = SimpleUploadedFile (name = name , content = content )
272+ response = client .post (url , {"file" : file }, format = "multipart" )
273+
274+ assert response .status_code == 201
275+
276+ file_path = response .json ()["file" ]
277+ pattern = re .compile (rf"^/media/{ document .id !s} /attachments/(.*)\.{ extension :s} " )
278+ match = pattern .search (file_path )
279+ file_id = match .group (1 )
280+
281+ # Validate that file_id is a valid UUID
282+ uuid .UUID (file_id )
283+
284+ # Now, check the metadata of the uploaded file
285+ key = file_path .replace ("/media" , "" )
286+ file_head = default_storage .connection .meta .client .head_object (
287+ Bucket = default_storage .bucket_name , Key = key
288+ )
289+ assert file_head ["Metadata" ] == {"owner" : str (user .id ), "is_unsafe" : "true" }
290+
291+
292+ def test_api_documents_attachment_upload_empty_file ():
293+ """An empty file should be rejected."""
294+ user = factories .UserFactory ()
295+ client = APIClient ()
296+ client .force_login (user )
297+
298+ document = factories .DocumentFactory (users = [(user , "owner" )])
299+ url = f"/api/v1.0/documents/{ document .id !s} /attachment-upload/"
247300
301+ file = SimpleUploadedFile (name = "test.png" , content = b"" )
248302 response = client .post (url , {"file" : file }, format = "multipart" )
249303
250304 assert response .status_code == 400
251- assert response .json () == {
252- "file" : [
253- "File type 'text/plain' is not allowed. Allowed types are: image/jpeg, image/png"
254- ]
255- }
305+ assert response .json () == {"file" : ["The submitted file is empty." ]}
306+
307+
308+ def test_api_documents_attachment_upload_unsafe ():
309+ """A file with an unsafe mime type should be tagged as such."""
310+ user = factories .UserFactory ()
311+ client = APIClient ()
312+ client .force_login (user )
313+
314+ document = factories .DocumentFactory (users = [(user , "owner" )])
315+ url = f"/api/v1.0/documents/{ document .id !s} /attachment-upload/"
316+
317+ file = SimpleUploadedFile (
318+ name = "script.exe" , content = b"\x4d \x5a \x90 \x00 \x03 \x00 \x00 \x00 "
319+ )
320+ response = client .post (url , {"file" : file }, format = "multipart" )
321+
322+ assert response .status_code == 201
323+
324+ file_path = response .json ()["file" ]
325+ pattern = re .compile (rf"^/media/{ document .id !s} /attachments/(.*)\.exe" )
326+ match = pattern .search (file_path )
327+ file_id = match .group (1 )
328+
329+ # Validate that file_id is a valid UUID
330+ uuid .UUID (file_id )
331+
332+ # Now, check the metadata of the uploaded file
333+ key = file_path .replace ("/media" , "" )
334+ file_head = default_storage .connection .meta .client .head_object (
335+ Bucket = default_storage .bucket_name , Key = key
336+ )
337+ assert file_head ["Metadata" ] == {"owner" : str (user .id ), "is_unsafe" : "true" }
0 commit comments