1414"""Scheduler function tests."""
1515
1616import unittest
17- from datetime import datetime
18- from unittest .mock import Mock
17+ from datetime import datetime , timezone
18+ from unittest .mock import Mock , patch
1919
2020from flask import Flask , Request
2121from werkzeug .test import EnvironBuilder
@@ -81,6 +81,30 @@ def test_on_schedule_call(self):
8181 )
8282 )
8383
84+ def test_on_schedule_call_with_z_suffix (self ):
85+ """
86+ Tests to ensure that timestamps with 'Z' suffix are parsed correctly as UTC.
87+ """
88+ with Flask (__name__ ).test_request_context ("/" ):
89+ environ = EnvironBuilder (
90+ headers = {
91+ "X-CloudScheduler-JobName" : "example-job" ,
92+ "X-CloudScheduler-ScheduleTime" : "2023-04-13T19:00:00Z" ,
93+ }
94+ ).get_environ ()
95+ mock_request = Request (environ )
96+ example_func = Mock (__name__ = "example_func" )
97+ decorated_func = scheduler_fn .on_schedule (schedule = "* * * * *" )(example_func )
98+ response = decorated_func (mock_request )
99+
100+ self .assertEqual (response .status_code , 200 )
101+ example_func .assert_called_once_with (
102+ scheduler_fn .ScheduledEvent (
103+ job_name = "example-job" ,
104+ schedule_time = datetime (2023 , 4 , 13 , 19 , 0 , 0 , tzinfo = timezone .utc ),
105+ )
106+ )
107+
84108 def test_on_schedule_call_with_no_headers (self ):
85109 """
86110 Tests to ensure that if the function is called manually
@@ -99,6 +123,7 @@ def test_on_schedule_call_with_no_headers(self):
99123 self .assertEqual (example_func .call_count , 1 )
100124 self .assertIsNone (example_func .call_args [0 ][0 ].job_name )
101125 self .assertIsNotNone (example_func .call_args [0 ][0 ].schedule_time )
126+ self .assertIsNotNone (example_func .call_args [0 ][0 ].schedule_time .tzinfo )
102127
103128 def test_on_schedule_call_with_exception (self ):
104129 """
@@ -121,6 +146,90 @@ def test_on_schedule_call_with_exception(self):
121146 self .assertEqual (response .status_code , 500 )
122147 self .assertEqual (response .data , b"Test exception" )
123148
149+ def test_on_schedule_call_with_fractional_seconds (self ):
150+ """
151+ Tests to ensure that timestamps with fractional seconds are parsed correctly.
152+ """
153+ with Flask (__name__ ).test_request_context ("/" ):
154+ environ = EnvironBuilder (
155+ headers = {
156+ "X-CloudScheduler-JobName" : "example-job" ,
157+ "X-CloudScheduler-ScheduleTime" : "2023-04-13T19:00:00.123456Z" ,
158+ }
159+ ).get_environ ()
160+ mock_request = Request (environ )
161+ example_func = Mock (__name__ = "example_func" )
162+ decorated_func = scheduler_fn .on_schedule (schedule = "* * * * *" )(example_func )
163+ response = decorated_func (mock_request )
164+
165+ self .assertEqual (response .status_code , 200 )
166+ example_func .assert_called_once_with (
167+ scheduler_fn .ScheduledEvent (
168+ job_name = "example-job" ,
169+ schedule_time = datetime (2023 , 4 , 13 , 19 , 0 , 0 , 123456 , tzinfo = timezone .utc ),
170+ )
171+ )
172+
173+ def test_on_schedule_call_fallback_parsing (self ):
174+ """
175+ Tests fallback parsing for formats that might fail fromisoformat
176+ but pass strptime (e.g., offset without colon).
177+ """
178+ with Flask (__name__ ).test_request_context ("/" ):
179+ environ = EnvironBuilder (
180+ headers = {
181+ "X-CloudScheduler-JobName" : "example-job" ,
182+ # Offset without colon might fail fromisoformat in some versions
183+ # but should pass strptime("%Y-%m-%dT%H:%M:%S%z")
184+ "X-CloudScheduler-ScheduleTime" : "2023-04-13T12:00:00-0700" ,
185+ }
186+ ).get_environ ()
187+ mock_request = Request (environ )
188+ example_func = Mock (__name__ = "example_func" )
189+ decorated_func = scheduler_fn .on_schedule (schedule = "* * * * *" )(example_func )
190+ response = decorated_func (mock_request )
191+
192+ self .assertEqual (response .status_code , 200 )
193+
194+ # Create expected datetime with fixed offset -07:00
195+ tz = datetime .strptime ("-0700" , "%z" ).tzinfo
196+ expected_dt = datetime (2023 , 4 , 13 , 12 , 0 , 0 , tzinfo = tz )
197+
198+ example_func .assert_called_once_with (
199+ scheduler_fn .ScheduledEvent (
200+ job_name = "example-job" ,
201+ schedule_time = expected_dt ,
202+ )
203+ )
204+
205+ def test_on_schedule_call_invalid_timestamp (self ):
206+ """
207+ Tests that invalid timestamps log an error and fallback to current time.
208+ """
209+ with Flask (__name__ ).test_request_context ("/" ):
210+ environ = EnvironBuilder (
211+ headers = {
212+ "X-CloudScheduler-JobName" : "example-job" ,
213+ "X-CloudScheduler-ScheduleTime" : "invalid-timestamp" ,
214+ }
215+ ).get_environ ()
216+ mock_request = Request (environ )
217+ example_func = Mock (__name__ = "example_func" )
218+
219+ with patch ("firebase_functions.scheduler_fn._logging" ) as mock_logging :
220+ decorated_func = scheduler_fn .on_schedule (schedule = "* * * * *" )(example_func )
221+ response = decorated_func (mock_request )
222+
223+ self .assertEqual (response .status_code , 200 )
224+ mock_logging .exception .assert_called_once ()
225+
226+ # Should have called with *some* time (current time), so we just check it's not None
227+ self .assertEqual (example_func .call_count , 1 )
228+ called_event = example_func .call_args [0 ][0 ]
229+ self .assertEqual (called_event .job_name , "example-job" )
230+ self .assertIsNotNone (called_event .schedule_time )
231+ self .assertIsNotNone (called_event .schedule_time .tzinfo )
232+
124233 def test_calls_init (self ):
125234 hello = None
126235
0 commit comments