@@ -325,6 +325,232 @@ def test_upload_to_sg(self, mock_send_form):
325325 mock_send_form .assert_called_once ()
326326 self .sg .server_info ["s3_direct_uploads_enabled" ] = True
327327
328+ @unittest .mock .patch ("shotgun_api3.Shotgun._send_form" )
329+ def test_upload_to_sg_with_created_at (self , mock_send_form ):
330+ """
331+ Verify that created_at is passed (in ISO 8601 string form) as a form
332+ parameter when uploading non-thumbnail attachments via _upload_to_sg().
333+ """
334+ self .sg .server_info ["s3_direct_uploads_enabled" ] = False
335+ mock_send_form .return_value = "1\n :456\n asd"
336+ this_dir , _ = os .path .split (__file__ )
337+ u_path = os .path .abspath (
338+ os .path .expanduser (glob .glob (os .path .join (this_dir , "Noëlご.jpg" ))[0 ])
339+ )
340+ custom_time = datetime .datetime (2026 , 2 , 15 , 10 , 30 , 0 )
341+ self .sg .upload (
342+ "Version" ,
343+ self .version ["id" ],
344+ u_path ,
345+ "attachments" ,
346+ created_at = custom_time ,
347+ )
348+ mock_send_form .assert_called_once ()
349+ mock_send_form_args , _ = mock_send_form .call_args
350+ params = mock_send_form_args [1 ]
351+ self .assertIn ("created_at" , params )
352+ self .assertEqual (params ["created_at" ], custom_time .isoformat ())
353+ self .sg .server_info ["s3_direct_uploads_enabled" ] = True
354+
355+ @unittest .mock .patch ("shotgun_api3.Shotgun._send_form" )
356+ def test_upload_to_sg_without_created_at (self , mock_send_form ):
357+ """
358+ Verify that created_at is NOT included in form parameters when omitted.
359+ """
360+ self .sg .server_info ["s3_direct_uploads_enabled" ] = False
361+ mock_send_form .return_value = "1\n :456\n asd"
362+ this_dir , _ = os .path .split (__file__ )
363+ u_path = os .path .abspath (
364+ os .path .expanduser (glob .glob (os .path .join (this_dir , "Noëlご.jpg" ))[0 ])
365+ )
366+ self .sg .upload (
367+ "Version" ,
368+ self .version ["id" ],
369+ u_path ,
370+ "attachments" ,
371+ )
372+ mock_send_form .assert_called_once ()
373+ mock_send_form_args , _ = mock_send_form .call_args
374+ params = mock_send_form_args [1 ]
375+ self .assertNotIn ("created_at" , params )
376+ self .sg .server_info ["s3_direct_uploads_enabled" ] = True
377+
378+ @unittest .mock .patch ("shotgun_api3.Shotgun._send_form" )
379+ @unittest .mock .patch ("shotgun_api3.Shotgun._upload_file_to_storage" )
380+ @unittest .mock .patch ("shotgun_api3.Shotgun._get_attachment_upload_info" )
381+ def test_upload_to_storage_with_created_at (
382+ self , mock_get_info , mock_upload_file , mock_send_form
383+ ):
384+ """
385+ Verify that created_at is passed as a form parameter when uploading
386+ non-thumbnail attachments via _upload_to_storage() (S3/cloud path),
387+ and that it is forwarded to _get_attachment_upload_info() so the server
388+ can use it when generating the storage path.
389+ """
390+ self .sg .server_info ["s3_direct_uploads_enabled" ] = True
391+ self .sg .server_info ["s3_enabled_upload_types" ] = {"Version" : "*" }
392+ mock_get_info .return_value = {
393+ "upload_url" : "https://example.com/upload" ,
394+ "upload_info" : {"upload_type" : "s3" },
395+ }
396+ mock_send_form .return_value = "1\n :456\n asd"
397+ this_dir , _ = os .path .split (__file__ )
398+ u_path = os .path .abspath (
399+ os .path .expanduser (glob .glob (os .path .join (this_dir , "Noëlご.jpg" ))[0 ])
400+ )
401+ custom_time = datetime .datetime (2026 , 2 , 15 , 10 , 30 , 0 )
402+ self .sg .upload (
403+ "Version" ,
404+ self .version ["id" ],
405+ u_path ,
406+ "attachments" ,
407+ created_at = custom_time ,
408+ )
409+ mock_get_info .assert_called_once ()
410+ _ , get_info_kwargs = mock_get_info .call_args
411+ self .assertEqual (get_info_kwargs .get ("created_at" ), custom_time )
412+ mock_send_form .assert_called_once ()
413+ mock_send_form_args , _ = mock_send_form .call_args
414+ params = mock_send_form_args [1 ]
415+ self .assertIn ("created_at" , params )
416+ self .assertEqual (params ["created_at" ], custom_time )
417+
418+ @unittest .mock .patch ("shotgun_api3.Shotgun._send_form" )
419+ @unittest .mock .patch ("shotgun_api3.Shotgun._upload_file_to_storage" )
420+ @unittest .mock .patch ("shotgun_api3.Shotgun._get_attachment_upload_info" )
421+ def test_upload_to_storage_without_created_at (
422+ self , mock_get_info , mock_upload_file , mock_send_form
423+ ):
424+ """
425+ Verify that created_at is NOT included in form parameters when omitted
426+ via _upload_to_storage() (S3/cloud path), and that None is forwarded to
427+ _get_attachment_upload_info().
428+ """
429+ self .sg .server_info ["s3_direct_uploads_enabled" ] = True
430+ self .sg .server_info ["s3_enabled_upload_types" ] = {"Version" : "*" }
431+ mock_get_info .return_value = {
432+ "upload_url" : "https://example.com/upload" ,
433+ "upload_info" : {"upload_type" : "s3" },
434+ }
435+ mock_send_form .return_value = "1\n :456\n asd"
436+ this_dir , _ = os .path .split (__file__ )
437+ u_path = os .path .abspath (
438+ os .path .expanduser (glob .glob (os .path .join (this_dir , "Noëlご.jpg" ))[0 ])
439+ )
440+ self .sg .upload (
441+ "Version" ,
442+ self .version ["id" ],
443+ u_path ,
444+ "attachments" ,
445+ )
446+ mock_get_info .assert_called_once ()
447+ _ , get_info_kwargs = mock_get_info .call_args
448+ self .assertIsNone (get_info_kwargs .get ("created_at" ))
449+ mock_send_form .assert_called_once ()
450+ mock_send_form_args , _ = mock_send_form .call_args
451+ params = mock_send_form_args [1 ]
452+ self .assertNotIn ("created_at" , params )
453+
454+ @unittest .mock .patch ("shotgun_api3.Shotgun._send_form" )
455+ def test_get_attachment_upload_info_with_created_at (self , mock_send_form ):
456+ """
457+ Verify that _get_attachment_upload_info() includes created_at in the
458+ form params (as ISO 8601 string) when sending the request to
459+ /upload/api_get_upload_link_info.
460+ """
461+ mock_send_form .return_value = (
462+ "1\n https://example.com/upload\n 2026-02-15T10:30:00\n Attachment\n upload-123"
463+ )
464+ custom_time = datetime .datetime (2026 , 2 , 15 , 10 , 30 , 0 )
465+ self .sg ._get_attachment_upload_info (
466+ is_thumbnail = False ,
467+ filename = "example.jpg" ,
468+ is_multipart_upload = False ,
469+ created_at = custom_time ,
470+ )
471+ mock_send_form .assert_called_once ()
472+ mock_send_form_args , _ = mock_send_form .call_args
473+ params = mock_send_form_args [1 ]
474+ self .assertIn ("created_at" , params )
475+ self .assertEqual (params ["created_at" ], custom_time .isoformat ())
476+ self .assertEqual (params ["upload_type" ], "Attachment" )
477+ self .assertEqual (params ["filename" ], "example.jpg" )
478+
479+ @unittest .mock .patch ("shotgun_api3.Shotgun._send_form" )
480+ def test_get_attachment_upload_info_without_created_at (self , mock_send_form ):
481+ """
482+ Verify that _get_attachment_upload_info() does NOT include created_at
483+ in the form params when it is omitted (None).
484+ """
485+ mock_send_form .return_value = (
486+ "1\n https://example.com/upload\n 2026-02-15T10:30:00\n Attachment\n upload-123"
487+ )
488+ self .sg ._get_attachment_upload_info (
489+ is_thumbnail = False ,
490+ filename = "example.jpg" ,
491+ is_multipart_upload = False ,
492+ )
493+ mock_send_form .assert_called_once ()
494+ mock_send_form_args , _ = mock_send_form .call_args
495+ params = mock_send_form_args [1 ]
496+ self .assertNotIn ("created_at" , params )
497+
498+ @unittest .mock .patch ("shotgun_api3.Shotgun._send_form" )
499+ def test_get_attachment_upload_info_thumbnail_with_created_at (self , mock_send_form ):
500+ """
501+ Verify that _get_attachment_upload_info() forwards created_at even when
502+ is_thumbnail=True. The implementation does not gate created_at on the
503+ thumbnail flag at this stage; the server is responsible for handling
504+ thumbnail-specific behavior.
505+ """
506+ mock_send_form .return_value = (
507+ "1\n https://example.com/upload\n 2026-02-15T10:30:00\n Thumbnail\n upload-123"
508+ )
509+ custom_time = datetime .datetime (2026 , 2 , 15 , 10 , 30 , 0 )
510+ self .sg ._get_attachment_upload_info (
511+ is_thumbnail = True ,
512+ filename = "thumb.jpg" ,
513+ is_multipart_upload = False ,
514+ created_at = custom_time ,
515+ )
516+ mock_send_form .assert_called_once ()
517+ mock_send_form_args , _ = mock_send_form .call_args
518+ params = mock_send_form_args [1 ]
519+ self .assertEqual (params ["upload_type" ], "Thumbnail" )
520+ self .assertIn ("created_at" , params )
521+ self .assertEqual (params ["created_at" ], custom_time .isoformat ())
522+
523+ def test_upload_created_at_invalid_type (self ):
524+ """
525+ Verify that passing a non-datetime value for created_at raises ShotgunError.
526+ """
527+ this_dir , _ = os .path .split (__file__ )
528+ u_path = os .path .abspath (
529+ os .path .expanduser (glob .glob (os .path .join (this_dir , "Noëlご.jpg" ))[0 ])
530+ )
531+ with self .assertRaisesRegex (
532+ shotgun_api3 .ShotgunError ,
533+ "created_at must be a datetime.datetime instance" ,
534+ ):
535+ self .sg .upload (
536+ "Version" ,
537+ self .version ["id" ],
538+ u_path ,
539+ "attachments" ,
540+ created_at = "2026-02-15T10:30:00Z" ,
541+ )
542+ with self .assertRaisesRegex (
543+ shotgun_api3 .ShotgunError ,
544+ "created_at must be a datetime.datetime instance" ,
545+ ):
546+ self .sg .upload (
547+ "Version" ,
548+ self .version ["id" ],
549+ u_path ,
550+ "attachments" ,
551+ created_at = 1234567890 ,
552+ )
553+
328554 def test_upload_thumbnail_in_create (self ):
329555 """Upload a thumbnail via the create method"""
330556 this_dir , _ = os .path .split (__file__ )
0 commit comments