@@ -550,7 +550,6 @@ def test_updates_simple_properties(self, mock_api_constructor: MagicMock, *args,
550550 "value" : 150 ,
551551 "state" : "hidden" ,
552552 "connection_info" : "https://example.com" ,
553- "scheduled_at" : None ,
554553 "max_attempts" : 0 ,
555554 }
556555
@@ -597,7 +596,6 @@ def test_updates_attempts(self, mock_api_constructor: MagicMock, *args, **kwargs
597596 "state" : "hidden" ,
598597 "max_attempts" : 5 ,
599598 "connection_info" : None ,
600- "scheduled_at" : None ,
601599 }
602600
603601 mock_api : MagicMock = mock_api_constructor .return_value
@@ -646,7 +644,6 @@ def test_updates_extra_properties(self, mock_api_constructor: MagicMock, *args,
646644 "application_name" : "application-name" ,
647645 "max_attempts" : 0 ,
648646 "connection_info" : None ,
649- "scheduled_at" : None ,
650647 }
651648
652649 mock_api : MagicMock = mock_api_constructor .return_value
@@ -704,7 +701,6 @@ def test_updates_flags(self, mock_api_constructor: MagicMock, *args, **kwargs):
704701 "state" : "hidden" ,
705702 "max_attempts" : 0 ,
706703 "connection_info" : None ,
707- "scheduled_at" : None ,
708704 }
709705
710706 mock_api : MagicMock = mock_api_constructor .return_value
@@ -794,7 +790,6 @@ def test_updates_topics(self, mock_api_constructor: MagicMock, *args, **kwargs):
794790 "state" : "hidden" ,
795791 "max_attempts" : 0 ,
796792 "connection_info" : None ,
797- "scheduled_at" : None ,
798793 }
799794
800795 mock_api : MagicMock = mock_api_constructor .return_value
@@ -856,7 +851,6 @@ def test_updates_tags(self, mock_api_constructor: MagicMock, *args, **kwargs):
856851 "state" : "hidden" ,
857852 "max_attempts" : 0 ,
858853 "connection_info" : None ,
859- "scheduled_at" : None ,
860854 }
861855
862856 mock_api : MagicMock = mock_api_constructor .return_value
@@ -921,7 +915,6 @@ def test_updates_files(self, mock_api_constructor: MagicMock, *args, **kwargs):
921915 "state" : "hidden" ,
922916 "max_attempts" : 0 ,
923917 "connection_info" : None ,
924- "scheduled_at" : None ,
925918 }
926919
927920 def mock_get (* args , ** kwargs ):
@@ -1077,7 +1070,6 @@ def test_updates_hints_with_requirements(self, mock_api_constructor: MagicMock,
10771070 "state" : "hidden" ,
10781071 "max_attempts" : 0 ,
10791072 "connection_info" : None ,
1080- "scheduled_at" : None ,
10811073 }
10821074
10831075 mock_api : MagicMock = mock_api_constructor .return_value
@@ -1185,7 +1177,6 @@ def test_updates_requirements(self, mock_api_constructor: MagicMock, *args, **kw
11851177 "state" : "hidden" ,
11861178 "max_attempts" : 0 ,
11871179 "connection_info" : None ,
1188- "scheduled_at" : None ,
11891180 }
11901181
11911182 mock_api : MagicMock = mock_api_constructor .return_value
@@ -1387,7 +1378,6 @@ def test_challenge_cannot_require_itself(
13871378 "state" : "hidden" ,
13881379 "max_attempts" : 0 ,
13891380 "connection_info" : None ,
1390- "scheduled_at" : None ,
13911381 }
13921382
13931383 def mock_get (* args , ** kwargs ):
@@ -1449,7 +1439,6 @@ def test_defaults_to_standard_challenge_type(self, mock_api_constructor: MagicMo
14491439 "state" : "hidden" ,
14501440 "max_attempts" : 0 ,
14511441 "connection_info" : None ,
1452- "scheduled_at" : None ,
14531442 }
14541443
14551444 mock_api : MagicMock = mock_api_constructor .return_value
@@ -1488,7 +1477,6 @@ def test_defaults_to_visible_state(self, mock_api_constructor: MagicMock, *args,
14881477 "value" : 150 ,
14891478 "max_attempts" : 0 ,
14901479 "connection_info" : None ,
1491- "scheduled_at" : None ,
14921480 # initial patch should set the state to hidden for the duration of the update
14931481 "state" : "hidden" ,
14941482 }
@@ -1542,7 +1530,6 @@ def test_does_not_update_dynamic_value(self, mock_api_constructor: MagicMock, *a
15421530 "state" : "hidden" ,
15431531 "max_attempts" : 0 ,
15441532 "connection_info" : None ,
1545- "scheduled_at" : None ,
15461533 }
15471534
15481535 mock_api : MagicMock = mock_api_constructor .return_value
@@ -1602,7 +1589,6 @@ def test_updates_multiple_attributes_at_once(self, mock_api_constructor: MagicMo
16021589 "state" : "hidden" ,
16031590 "max_attempts" : 5 ,
16041591 "connection_info" : "https://example.com" ,
1605- "scheduled_at" : None ,
16061592 }
16071593
16081594 mock_api : MagicMock = mock_api_constructor .return_value
@@ -1720,6 +1706,7 @@ def test_does_not_update_ignored_attributes(self):
17201706 "state" : "hidden" ,
17211707 "max_attempts" : 0 ,
17221708 "connection_info" : None ,
1709+ # the remote reports scheduled_at, so an unset local value clears it
17231710 "scheduled_at" : None ,
17241711 }
17251712
@@ -1830,7 +1817,6 @@ def test_creates_standard_challenge(self, mock_api_constructor: MagicMock, *args
18301817 "max_attempts" : 5 ,
18311818 "type" : "standard" ,
18321819 "connection_info" : "https://example.com" ,
1833- "scheduled_at" : None ,
18341820 "extra_property" : "extra_property_value" ,
18351821 "state" : "hidden" ,
18361822 }
@@ -2035,7 +2021,6 @@ def test_does_not_set_ignored_attributes(self):
20352021 "state" : "hidden" ,
20362022 "max_attempts" : 0 ,
20372023 "connection_info" : None ,
2038- "scheduled_at" : None ,
20392024 }
20402025
20412026 # add a property that should be defined but ignored
@@ -2067,7 +2052,6 @@ def test_does_not_set_ignored_attributes(self):
20672052
20682053 if p == "scheduled_at" :
20692054 challenge ["scheduled_at" ] = "2026-06-15T12:00:00+00:00"
2070- del expected_challenge_payload ["scheduled_at" ]
20712055
20722056 if p == "state" :
20732057 challenge [p ] = "new-value"
@@ -2731,7 +2715,6 @@ def test_sync_substitutes_media_in_description(self, mock_api_constructor: Magic
27312715 "state" : "hidden" ,
27322716 "max_attempts" : 0 ,
27332717 "connection_info" : None ,
2734- "scheduled_at" : None ,
27352718 },
27362719 )
27372720
@@ -2873,6 +2856,9 @@ def _prop(self):
28732856 def _payload (self , challenge , ignore = ()):
28742857 return self ._prop ().create_payload (PropertyContext (challenge , ignore = ignore ))
28752858
2859+ def _sync_payload (self , challenge , remote_challenge = None , ignore = ()):
2860+ return self ._prop ().sync_payload (PropertyContext (challenge , ignore = ignore , remote_challenge = remote_challenge ))
2861+
28762862 def test_parse_accepts_timezone_aware_string (self ):
28772863 parsed = self ._prop ().parse ("2026-06-15T12:00:00+00:00" )
28782864 self .assertEqual (parsed , datetime (2026 , 6 , 15 , 12 , 0 , 0 , tzinfo = timezone .utc ))
@@ -2908,9 +2894,33 @@ def test_payload_includes_scheduled_at_iso(self):
29082894 # The explicit offset is preserved when sent to CTFd (CTFd normalizes server-side)
29092895 self .assertEqual (self ._payload (challenge )["scheduled_at" ], "2026-06-15T14:00:00+02:00" )
29102896
2911- def test_payload_scheduled_at_none_when_absent (self ):
2897+ def test_payload_omits_scheduled_at_when_absent (self ):
2898+ # CTFd passes the payload straight to the challenge model, so instances
2899+ # (or challenge type plugins) which don't know scheduled_at fail on the key
29122900 challenge = Challenge (self .minimal_challenge )
2913- self .assertIsNone (self ._payload (challenge )["scheduled_at" ])
2901+ self .assertNotIn ("scheduled_at" , self ._payload (challenge ))
2902+
2903+ def test_sync_payload_includes_scheduled_at_iso (self ):
2904+ challenge = Challenge (self .minimal_challenge , {"scheduled_at" : "2026-06-15T14:00:00+02:00" })
2905+ remote = {"scheduled_at" : None }
2906+ self .assertEqual (self ._sync_payload (challenge , remote )["scheduled_at" ], "2026-06-15T14:00:00+02:00" )
2907+
2908+ def test_sync_payload_clears_scheduled_at_set_on_the_remote (self ):
2909+ challenge = Challenge (self .minimal_challenge )
2910+ remote = {"scheduled_at" : "2026-06-15T12:00:00" }
2911+ self .assertEqual (self ._sync_payload (challenge , remote ), {"scheduled_at" : None })
2912+ self .assertEqual (self ._sync_payload (challenge , {"scheduled_at" : None }), {"scheduled_at" : None })
2913+
2914+ def test_sync_payload_omits_scheduled_at_for_remotes_without_the_field (self ):
2915+ # remotes which don't support scheduled_at don't return the key at all,
2916+ # and reject a payload carrying it
2917+ challenge = Challenge (self .minimal_challenge )
2918+ self .assertEqual (self ._sync_payload (challenge , {}), {})
2919+
2920+ def test_sync_payload_omits_scheduled_at_when_ignored (self ):
2921+ challenge = Challenge (self .minimal_challenge , {"scheduled_at" : "2026-06-15T12:00:00+00:00" })
2922+ remote = {"scheduled_at" : "2026-06-15T12:00:00" }
2923+ self .assertEqual (self ._sync_payload (challenge , remote , ignore = ("scheduled_at" ,)), {})
29142924
29152925 def test_payload_omits_scheduled_at_when_ignored (self ):
29162926 challenge = Challenge (self .minimal_challenge , {"scheduled_at" : "2026-06-15T12:00:00+00:00" })
0 commit comments