Skip to content

Commit dc667ae

Browse files
committed
fix(util): improve the robustness of timestamp conversion funcion
1 parent c5cb62a commit dc667ae

File tree

2 files changed

+108
-4
lines changed

2 files changed

+108
-4
lines changed

src/firebase_functions/private/util.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,17 +402,33 @@ def get_precision_timestamp(time: str) -> PrecisionTimestamp:
402402
return PrecisionTimestamp.MICROSECONDS
403403

404404

405-
def timestamp_conversion(time: str) -> _dt.datetime:
406-
"""Converts a timestamp and returns a datetime object of the current time in UTC"""
407-
precision_timestamp = get_precision_timestamp(time)
405+
def timestamp_conversion(time) -> _dt.datetime:
406+
"""
407+
Converts a timestamp and returns a datetime object of the current time in UTC.
408+
Accepts RFC 3339/ISO 8601 strings or Firebase Timestamp objects (with 'seconds', 'nanoseconds' attributes).
409+
"""
410+
# Handle Firebase Timestamp object case
411+
# Accept dict-like objects, or python objects with 'seconds' and 'nanoseconds' attributes
412+
if hasattr(time, 'seconds') and hasattr(time, 'nanoseconds'):
413+
# Use UTC time
414+
return _dt.datetime.fromtimestamp(
415+
time.seconds + time.nanoseconds / 1_000_000_000, tz=_dt.timezone.utc
416+
)
417+
elif isinstance(time, dict) and "seconds" in time and "nanoseconds" in time:
418+
return _dt.datetime.fromtimestamp(
419+
time["seconds"] + time["nanoseconds"] / 1_000_000_000, tz=_dt.timezone.utc
420+
)
408421

422+
# Assume string input
423+
if not isinstance(time, str):
424+
raise ValueError("timestamp_conversion expects a string or a Timestamp-like object")
425+
precision_timestamp = get_precision_timestamp(time)
409426
if precision_timestamp == PrecisionTimestamp.NANOSECONDS:
410427
return nanoseconds_timestamp_conversion(time)
411428
elif precision_timestamp == PrecisionTimestamp.MICROSECONDS:
412429
return microsecond_timestamp_conversion(time)
413430
elif precision_timestamp == PrecisionTimestamp.SECONDS:
414431
return second_timestamp_conversion(time)
415-
416432
raise ValueError("Invalid timestamp")
417433

418434

tests/test_util.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import datetime as _dt
1919
from os import environ, path
2020

21+
import pytest
22+
2123
from firebase_functions.private.util import (
2224
PrecisionTimestamp,
2325
_unsafe_decode_id_token,
@@ -28,6 +30,7 @@
2830
nanoseconds_timestamp_conversion,
2931
normalize_path,
3032
second_timestamp_conversion,
33+
timestamp_conversion,
3134
)
3235

3336
test_bucket = "python-functions-testing.appspot.com"
@@ -187,3 +190,88 @@ def test_unsafe_decode_token():
187190
result = _unsafe_decode_id_token(test_token)
188191
assert result["sub"] == "firebase"
189192
assert result["name"] == "John Doe"
193+
194+
195+
def test_timestamp_conversion_with_object():
196+
"""
197+
Testing timestamp_conversion works with objects that have seconds and nanoseconds attributes.
198+
"""
199+
class Timestamp:
200+
def __init__(self, seconds, nanoseconds):
201+
self.seconds = seconds
202+
self.nanoseconds = nanoseconds
203+
204+
test_cases = [
205+
(1672578896, 123456789),
206+
(1672578896, 0),
207+
(1672578896, 1_500_000_000),
208+
]
209+
210+
for seconds, nanoseconds in test_cases:
211+
timestamp_obj = Timestamp(seconds=seconds, nanoseconds=nanoseconds)
212+
result = timestamp_conversion(timestamp_obj)
213+
expected = _dt.datetime.fromtimestamp(
214+
seconds + nanoseconds / 1_000_000_000, tz=_dt.timezone.utc
215+
)
216+
assert result == expected
217+
assert result.tzinfo == _dt.timezone.utc
218+
219+
220+
def test_timestamp_conversion_with_dict():
221+
"""
222+
Testing timestamp_conversion works with dict objects containing seconds and nanoseconds keys.
223+
"""
224+
test_cases = [
225+
(1687256122, 396358000),
226+
(1687256122, 0),
227+
]
228+
229+
for seconds, nanoseconds in test_cases:
230+
timestamp_dict = {"seconds": seconds, "nanoseconds": nanoseconds}
231+
result = timestamp_conversion(timestamp_dict)
232+
expected = _dt.datetime.fromtimestamp(
233+
seconds + nanoseconds / 1_000_000_000, tz=_dt.timezone.utc
234+
)
235+
assert result == expected
236+
assert result.tzinfo == _dt.timezone.utc
237+
238+
239+
def test_timestamp_conversion_with_string():
240+
"""
241+
Testing timestamp_conversion works with string inputs.
242+
"""
243+
test_cases = [
244+
("2023-01-01T12:34:56.123456789Z", nanoseconds_timestamp_conversion),
245+
("2023-06-20T10:15:22.396358Z", microsecond_timestamp_conversion),
246+
("2023-01-01T12:34:56Z", second_timestamp_conversion),
247+
]
248+
249+
for timestamp_str, conversion_func in test_cases:
250+
result = timestamp_conversion(timestamp_str)
251+
expected = conversion_func(timestamp_str)
252+
assert result == expected
253+
254+
255+
def test_timestamp_conversion_errors():
256+
"""
257+
Testing timestamp_conversion raises appropriate errors for invalid inputs.
258+
"""
259+
class IncompleteTimestamp:
260+
def __init__(self, nanoseconds):
261+
self.nanoseconds = nanoseconds
262+
263+
with pytest.raises(ValueError):
264+
timestamp_conversion(IncompleteTimestamp(nanoseconds=123456789))
265+
266+
with pytest.raises(ValueError) as context:
267+
timestamp_conversion(12345)
268+
assert "timestamp_conversion expects a string or a Timestamp-like object" in str(context.value)
269+
with pytest.raises(ValueError):
270+
timestamp_conversion({"nanoseconds": 123456789})
271+
272+
with pytest.raises(ValueError):
273+
timestamp_conversion("invalid_timestamp")
274+
275+
with pytest.raises(ValueError):
276+
timestamp_conversion(None)
277+

0 commit comments

Comments
 (0)