33from django .contrib .contenttypes .models import ContentType
44from django .urls import reverse
55from django .utils .timezone import make_aware , now
6+ from rest_framework import status
67
78from core .choices import ManagedFileRootPathChoices
89from core .events import *
@@ -858,16 +859,16 @@ def setUpTestData(cls):
858859class ScriptTest (APITestCase ):
859860
860861 class TestScriptClass (PythonClass ):
861-
862862 class Meta :
863- name = "Test script"
863+ name = 'Test script'
864+ commit = True
865+ scheduling_enabled = True
864866
865867 var1 = StringVar ()
866868 var2 = IntegerVar ()
867869 var3 = BooleanVar ()
868870
869871 def run (self , data , commit = True ):
870-
871872 self .log_info (data ['var1' ])
872873 self .log_success (data ['var2' ])
873874 self .log_failure (data ['var3' ])
@@ -878,14 +879,16 @@ def run(self, data, commit=True):
878879 def setUpTestData (cls ):
879880 module = ScriptModule .objects .create (
880881 file_root = ManagedFileRootPathChoices .SCRIPTS ,
881- file_path = '/var/tmp/ script.py'
882+ file_path = 'script.py' ,
882883 )
883- Script .objects .create (
884+ script = Script .objects .create (
884885 module = module ,
885- name = " Test script" ,
886+ name = ' Test script' ,
886887 is_executable = True ,
887888 )
889+ cls .url = reverse ('extras-api:script-detail' , kwargs = {'pk' : script .pk })
888890
891+ @property
889892 def python_class (self ):
890893 return self .TestScriptClass
891894
@@ -898,7 +901,7 @@ def setUp(self):
898901 def test_get_script (self ):
899902 module = ScriptModule .objects .get (
900903 file_root = ManagedFileRootPathChoices .SCRIPTS ,
901- file_path = '/var/tmp/ script.py'
904+ file_path = 'script.py' ,
902905 )
903906 script = module .scripts .all ().first ()
904907 url = reverse ('extras-api:script-detail' , kwargs = {'pk' : script .pk })
@@ -909,6 +912,76 @@ def test_get_script(self):
909912 self .assertEqual (response .data ['vars' ]['var2' ], 'IntegerVar' )
910913 self .assertEqual (response .data ['vars' ]['var3' ], 'BooleanVar' )
911914
915+ def test_schedule_script_past_time_rejected (self ):
916+ """
917+ Scheduling with past schedule_at should fail.
918+ """
919+ self .add_permissions ('extras.run_script' )
920+
921+ payload = {
922+ 'data' : {'var1' : 'hello' , 'var2' : 1 , 'var3' : False },
923+ 'commit' : True ,
924+ 'schedule_at' : now () - datetime .timedelta (hours = 1 ),
925+ }
926+ response = self .client .post (self .url , payload , format = 'json' , ** self .header )
927+
928+ self .assertHttpStatus (response , status .HTTP_400_BAD_REQUEST )
929+ self .assertIn ('schedule_at' , response .data )
930+ # Be tolerant of exact wording but ensure we failed on schedule_at being in the past
931+ self .assertIn ('future' , str (response .data ['schedule_at' ]).lower ())
932+
933+ def test_schedule_script_interval_only (self ):
934+ """
935+ Interval without schedule_at should auto-set schedule_at now.
936+ """
937+ self .add_permissions ('extras.run_script' )
938+
939+ payload = {
940+ 'data' : {'var1' : 'hello' , 'var2' : 1 , 'var3' : False },
941+ 'commit' : True ,
942+ 'interval' : 60 ,
943+ }
944+ response = self .client .post (self .url , payload , format = 'json' , ** self .header )
945+
946+ self .assertHttpStatus (response , status .HTTP_200_OK )
947+ # The latest job is returned in the script detail serializer under "result"
948+ self .assertIn ('result' , response .data )
949+ self .assertEqual (response .data ['result' ]['interval' ], 60 )
950+ # Ensure a start time was autopopulated
951+ self .assertIsNotNone (response .data ['result' ]['scheduled' ])
952+
953+ def test_schedule_script_when_disabled (self ):
954+ """
955+ Scheduling should fail when script.scheduling_enabled=False.
956+ """
957+ self .add_permissions ('extras.run_script' )
958+
959+ # Temporarily disable scheduling on the in-test Python class
960+ original = getattr (self .TestScriptClass .Meta , 'scheduling_enabled' , True )
961+ self .TestScriptClass .Meta .scheduling_enabled = False
962+ base = {
963+ 'data' : {'var1' : 'hello' , 'var2' : 1 , 'var3' : False },
964+ 'commit' : True ,
965+ }
966+ # Check both schedule_at and interval paths
967+ cases = [
968+ {** base , 'schedule_at' : now () + datetime .timedelta (minutes = 5 )},
969+ {** base , 'interval' : 60 },
970+ ]
971+ try :
972+ for case in cases :
973+ with self .subTest (case = list (case .keys ())):
974+ response = self .client .post (self .url , case , format = 'json' , ** self .header )
975+
976+ self .assertHttpStatus (response , status .HTTP_400_BAD_REQUEST )
977+ # Error should be attached to whichever field we used
978+ key = 'schedule_at' if 'schedule_at' in case else 'interval'
979+ self .assertIn (key , response .data )
980+ self .assertIn ('scheduling is not enabled' , str (response .data [key ]).lower ())
981+ finally :
982+ # Restore the original setting for other tests
983+ self .TestScriptClass .Meta .scheduling_enabled = original
984+
912985
913986class CreatedUpdatedFilterTest (APITestCase ):
914987
0 commit comments