Files
noteflow/tests/grpc/test_timestamp_converters.py
Travis Vasceannie d8090a98e8
Some checks failed
CI / test-typescript (push) Has been cancelled
CI / test-rust (push) Has been cancelled
CI / test-python (push) Has been cancelled
ci/cd fixes
2026-01-26 00:28:15 +00:00

194 lines
7.8 KiB
Python

"""Tests for timestamp conversion helpers in grpc/_mixins/converters.py."""
from __future__ import annotations
from datetime import UTC, datetime, timedelta, timezone
import pytest
from google.protobuf.timestamp_pb2 import Timestamp
from noteflow.grpc.mixins.converters import (
datetime_to_epoch_seconds,
datetime_to_iso_string,
datetime_to_proto_timestamp,
epoch_seconds_to_datetime,
iso_string_to_datetime,
proto_timestamp_to_datetime,
)
# Test constants for timestamp conversion tests
TEST_YEAR = 2024
TEST_MONTH = 6
TEST_DAY = 15
TEST_HOUR = 14
TEST_MINUTE = 30
TEST_SECOND = 45
class TestDatetimeProtoTimestampConversion:
"""Test datetime <-> protobuf Timestamp conversions."""
def test_datetime_to_proto_timestamp_computes_correct_epoch(self) -> None:
"""Convert datetime to proto Timestamp with correct epoch seconds."""
dt = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
expected_epoch = 1718461845
ts = datetime_to_proto_timestamp(dt)
assert ts.seconds == expected_epoch, (
f"Expected epoch {expected_epoch}, got {ts.seconds}"
)
def test_proto_timestamp_to_datetime_returns_utc(self) -> None:
"""Convert proto Timestamp to datetime with UTC timezone."""
ts = Timestamp()
ts.FromDatetime(datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC))
dt = proto_timestamp_to_datetime(ts)
assert dt.tzinfo == UTC, "Returned datetime should have UTC timezone"
def test_datetime_proto_timestamp_roundtrip(self) -> None:
"""Datetime -> proto Timestamp -> datetime roundtrip preserves value."""
original = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
ts = datetime_to_proto_timestamp(original)
result = proto_timestamp_to_datetime(ts)
# Protobuf timestamps have nanosecond precision, datetime has microsecond
assert result.year == original.year, "Year should be preserved"
assert result.month == original.month, "Month should be preserved"
assert result.day == original.day, "Day should be preserved"
assert result.hour == original.hour, "Hour should be preserved"
assert result.minute == original.minute, "Minute should be preserved"
assert result.second == original.second, "Second should be preserved"
class TestEpochSecondsConversion:
"""Test epoch seconds <-> datetime conversions."""
def test_epoch_seconds_to_datetime_returns_utc(self) -> None:
"""Convert epoch seconds to datetime with UTC timezone."""
# 2024-06-15 14:30:45 UTC
epoch_seconds = 1718458245.0
dt = epoch_seconds_to_datetime(epoch_seconds)
assert dt.tzinfo == UTC, "Returned datetime should have UTC timezone"
assert dt.year == TEST_YEAR, f"Expected year {TEST_YEAR}, got {dt.year}"
def test_datetime_to_epoch_seconds_computes_correct_value(self) -> None:
"""Convert datetime to epoch seconds with correct value."""
dt = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
expected_seconds = 1718461845.0
seconds = datetime_to_epoch_seconds(dt)
assert seconds == expected_seconds, (
f"Expected {expected_seconds}, got {seconds}"
)
def test_epoch_seconds_roundtrip(self) -> None:
"""Epoch seconds -> datetime -> epoch seconds roundtrip preserves value."""
original_seconds = 1718458245.5 # Include fractional seconds
dt = epoch_seconds_to_datetime(original_seconds)
result_seconds = datetime_to_epoch_seconds(dt)
# Allow small floating point tolerance
tolerance = 0.001
assert abs(result_seconds - original_seconds) < tolerance, (
f"Expected {original_seconds}, got {result_seconds}"
)
@pytest.mark.parametrize(
("seconds", "expected_year"),
[
pytest.param(0.0, 1970, id="unix_epoch_start"),
pytest.param(1000000000.0, 2001, id="billion_seconds"),
pytest.param(1718458245.0, TEST_YEAR, id="recent_date"),
],
)
def test_epoch_seconds_to_datetime_various_values(
self, seconds: float, expected_year: int
) -> None:
"""Convert various epoch seconds values to datetime."""
dt = epoch_seconds_to_datetime(seconds)
assert dt.year == expected_year, f"Expected year {expected_year}, got {dt.year}"
class TestIsoStringConversion:
"""Test ISO 8601 string <-> datetime conversions."""
def test_iso_string_with_z_suffix_parsed_as_utc(self) -> None:
"""Parse ISO string with Z suffix as UTC datetime."""
iso_str = "2024-06-15T14:30:45Z"
dt = iso_string_to_datetime(iso_str)
assert dt.tzinfo == UTC, "Z suffix should be parsed as UTC"
assert dt.year == TEST_YEAR, f"Expected year {TEST_YEAR}, got {dt.year}"
assert dt.month == TEST_MONTH, f"Expected month {TEST_MONTH}, got {dt.month}"
assert dt.day == TEST_DAY, f"Expected day {TEST_DAY}, got {dt.day}"
assert dt.hour == TEST_HOUR, f"Expected hour {TEST_HOUR}, got {dt.hour}"
assert dt.minute == TEST_MINUTE, f"Expected minute {TEST_MINUTE}, got {dt.minute}"
assert dt.second == TEST_SECOND, f"Expected second {TEST_SECOND}, got {dt.second}"
def test_iso_string_with_offset_preserved(self) -> None:
"""Parse ISO string with timezone offset."""
iso_str = "2024-06-15T14:30:45+05:30"
dt = iso_string_to_datetime(iso_str)
assert dt.tzinfo is not None, "Timezone should be preserved"
# +05:30 offset
expected_offset = timezone(offset=timedelta(hours=5, minutes=30))
assert dt.utcoffset() == expected_offset.utcoffset(None), (
"Timezone offset should be preserved"
)
def test_iso_string_without_timezone_defaults_to_utc(self) -> None:
"""Parse ISO string without timezone, defaulting to UTC."""
iso_str = "2024-06-15T14:30:45"
dt = iso_string_to_datetime(iso_str)
assert dt.tzinfo == UTC, "Missing timezone should default to UTC"
def test_datetime_to_iso_string_includes_timezone(self) -> None:
"""Format datetime as ISO string with timezone."""
dt = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
iso_str = datetime_to_iso_string(dt)
assert f"{TEST_YEAR}-0{TEST_MONTH}-{TEST_DAY}" in iso_str, f"Date should be in output: {iso_str}"
assert f"{TEST_HOUR}:{TEST_MINUTE}:{TEST_SECOND}" in iso_str, f"Time should be in output: {iso_str}"
# UTC represented as +00:00 in isoformat
assert "+00:00" in iso_str or "Z" in iso_str, (
f"Timezone should be in output: {iso_str}"
)
def test_iso_string_roundtrip(self) -> None:
"""Datetime -> ISO string -> datetime roundtrip preserves value."""
original = datetime(TEST_YEAR, TEST_MONTH, TEST_DAY, TEST_HOUR, TEST_MINUTE, TEST_SECOND, tzinfo=UTC)
iso_str = datetime_to_iso_string(original)
result = iso_string_to_datetime(iso_str)
assert result == original, f"Roundtrip should preserve datetime: {original} -> {iso_str} -> {result}"
@pytest.mark.parametrize(
("iso_str", "expected_hour"),
[
pytest.param("2024-06-15T00:00:00Z", 0, id="midnight"),
pytest.param("2024-06-15T12:00:00Z", 12, id="noon"),
pytest.param("2024-06-15T23:59:59Z", 23, id="end_of_day"),
],
)
def test_iso_string_various_times(self, iso_str: str, expected_hour: int) -> None:
"""Parse various ISO string time values."""
dt = iso_string_to_datetime(iso_str)
assert dt.hour == expected_hour, f"Expected hour {expected_hour}, got {dt.hour}"