PyGithub MCP Server
by AstroMined
- tests
- unit
- converters
- common
"""Tests for datetime conversion functions.
This module tests the datetime conversion functions used to convert between
datetime objects and ISO format strings.
"""
import pytest
import functools
from datetime import datetime, timezone, timedelta
from pygithub_mcp_server.converters.common.datetime import (
convert_datetime,
convert_iso_string_to_datetime,
ensure_utc_datetime,
with_utc_datetimes
)
class TestConvertDatetime:
"""Tests for convert_datetime function."""
def test_convert_datetime_with_datetime(self):
"""Test converting a datetime to ISO string."""
dt = datetime(2023, 1, 15, 12, 30, 45, tzinfo=timezone.utc)
result = convert_datetime(dt)
assert result == "2023-01-15T12:30:45+00:00"
def test_convert_datetime_with_none(self):
"""Test converting None to ISO string."""
result = convert_datetime(None)
assert result is None
class TestConvertIsoStringToDatetime:
"""Tests for convert_iso_string_to_datetime function."""
def test_with_datetime_object(self):
"""Test with datetime object (should return as-is)."""
dt = datetime(2023, 1, 15, 12, 30, 45, tzinfo=timezone.utc)
result = convert_iso_string_to_datetime(dt)
assert result is dt
def test_with_z_timezone(self):
"""Test with Z timezone format."""
result = convert_iso_string_to_datetime("2023-01-15T12:30:45Z")
expected = datetime(2023, 1, 15, 12, 30, 45, tzinfo=timezone.utc)
assert result == expected
def test_with_offset_timezone_with_colon(self):
"""Test with standard offset timezone with colon."""
result = convert_iso_string_to_datetime("2023-01-15T12:30:45+02:00")
expected = datetime(2023, 1, 15, 12, 30, 45,
tzinfo=timezone(timedelta(hours=2)))
assert result.year == expected.year
assert result.month == expected.month
assert result.day == expected.day
assert result.hour == expected.hour
assert result.minute == expected.minute
assert result.second == expected.second
assert result.tzinfo is not None
assert result.utcoffset() == expected.utcoffset()
def test_with_offset_timezone_without_colon(self):
"""Test with offset timezone without colon."""
result = convert_iso_string_to_datetime("2023-01-15T12:30:45-0500")
expected = datetime(2023, 1, 15, 12, 30, 45,
tzinfo=timezone(timedelta(hours=-5)))
assert result.utcoffset() == expected.utcoffset()
def test_with_short_timezone(self):
"""Test with short timezone format (hours only)."""
result = convert_iso_string_to_datetime("2023-01-15T12:30:45+05")
expected = datetime(2023, 1, 15, 12, 30, 45,
tzinfo=timezone(timedelta(hours=5)))
assert result.utcoffset() == expected.utcoffset()
def test_with_single_digit_timezone(self):
"""Test with single digit timezone format."""
result = convert_iso_string_to_datetime("2023-01-15T12:30:45+5")
expected = datetime(2023, 1, 15, 12, 30, 45,
tzinfo=timezone(timedelta(hours=5)))
assert result.utcoffset() == expected.utcoffset()
def test_invalid_type(self):
"""Test with invalid type (not string or datetime)."""
with pytest.raises(ValueError) as exc_info:
convert_iso_string_to_datetime(123)
assert "Expected string or datetime" in str(exc_info.value)
def test_short_string(self):
"""Test with too short string that can't be a date."""
with pytest.raises(ValueError) as exc_info:
convert_iso_string_to_datetime("abc")
assert "Invalid date format" in str(exc_info.value)
def test_datetime_without_timezone_handling(self):
"""Test that datetime without timezone remains naive."""
result = convert_iso_string_to_datetime("2023-01-15T12:30:45")
# Verify it keeps the time components
assert result.year == 2023
assert result.month == 1
assert result.day == 15
assert result.hour == 12
assert result.minute == 30
assert result.second == 45
# For convert_iso_string_to_datetime, the result should stay naive
assert result.tzinfo is None
def test_date_only_format_handling(self):
"""Test that date-only format is handled as naive datetime."""
result = convert_iso_string_to_datetime("2023-01-15")
# Verify it creates a valid datetime with default time values
assert result.year == 2023
assert result.month == 1
assert result.day == 15
# Default time should be midnight
assert result.hour == 0
assert result.minute == 0
assert result.second == 0
# Should be a naive datetime
assert result.tzinfo is None
def test_malformed_datetime(self):
"""Test with malformed datetime string."""
with pytest.raises(ValueError) as exc_info:
convert_iso_string_to_datetime("2023-13-40T99:99:99Z")
assert "Invalid isoformat string" in str(exc_info.value)
class TestEnsureUtcDatetime:
"""Tests for ensure_utc_datetime function."""
def test_with_string(self):
"""Test with ISO format string."""
result = ensure_utc_datetime("2023-01-15T12:30:45Z")
expected = datetime(2023, 1, 15, 12, 30, 45, tzinfo=timezone.utc)
assert result == expected
assert result.tzinfo == timezone.utc
def test_ensure_utc_with_naive_datetime_string(self):
"""Test that ensure_utc_datetime adds UTC timezone to naive datetime string."""
# Pass a naive datetime string directly to ensure_utc_datetime
result = ensure_utc_datetime("2023-01-15T12:30:45")
# Should have UTC timezone
assert result.tzinfo is not None
assert result.tzinfo == timezone.utc
assert result.year == 2023
assert result.month == 1
assert result.day == 15
assert result.hour == 12
assert result.minute == 30
assert result.second == 45
def test_ensure_utc_with_naive_datetime(self):
"""Test that ensure_utc_datetime adds UTC timezone to naive datetime object."""
# Create a naive datetime using convert_iso_string_to_datetime
naive_dt = convert_iso_string_to_datetime("2023-01-15T12:30:45")
assert naive_dt.tzinfo is None # Verify it's naive
# Now pass to ensure_utc_datetime
result = ensure_utc_datetime(naive_dt)
# Should now have UTC timezone
assert result.tzinfo is not None
assert result.tzinfo == timezone.utc
assert result.year == 2023
assert result.month == 1
assert result.day == 15
assert result.hour == 12
assert result.minute == 30
assert result.second == 45
def test_ensure_utc_with_date_only_string(self):
"""Test that ensure_utc_datetime handles date-only strings correctly."""
# Pass a date-only string to ensure_utc_datetime
result = ensure_utc_datetime("2023-01-15")
# Should have UTC timezone and midnight time
assert result.tzinfo is not None
assert result.tzinfo == timezone.utc
assert result.year == 2023
assert result.month == 1
assert result.day == 15
assert result.hour == 0
assert result.minute == 0
assert result.second == 0
def test_with_naive_datetime(self):
"""Test with naive datetime object (no timezone)."""
dt = datetime(2023, 1, 15, 12, 30, 45)
result = ensure_utc_datetime(dt)
assert result.tzinfo == timezone.utc
assert result.year == 2023
assert result.month == 1
assert result.day == 15
assert result.hour == 12
assert result.minute == 30
assert result.second == 45
def test_with_non_utc_timezone(self):
"""Test with non-UTC timezone."""
# Create a datetime with non-UTC timezone
non_utc_tz = timezone(timedelta(hours=5))
dt = datetime(2023, 1, 15, 12, 30, 45, tzinfo=non_utc_tz)
# Ensure UTC conversion
result = ensure_utc_datetime(dt)
# Should be converted to UTC (5 hours earlier)
assert result.tzinfo == timezone.utc
assert result.hour == 7 # 12 - 5 = 7
def test_microseconds_truncation(self):
"""Test that microseconds are truncated."""
dt = datetime(2023, 1, 15, 12, 30, 45, 123456, tzinfo=timezone.utc)
result = ensure_utc_datetime(dt)
assert result.microsecond == 0
class TestWithUtcDatetimes:
"""Tests for with_utc_datetimes decorator."""
def test_all_datetime_params(self):
"""Test decorator with all params conversion."""
@with_utc_datetimes()
def test_func(dt1, dt2=None, regular_param="value"):
return {
"dt1": dt1,
"dt2": dt2,
"regular_param": regular_param
}
# Call with datetime string and datetime object
result = test_func(
dt1="2023-01-15T12:30:45Z",
dt2=datetime(2023, 1, 15, 10, 0, 0)
)
# Both should be converted to UTC timezone-aware datetimes
assert isinstance(result["dt1"], datetime)
assert result["dt1"].tzinfo == timezone.utc
assert isinstance(result["dt2"], datetime)
assert result["dt2"].tzinfo == timezone.utc
# Regular param should be untouched
assert result["regular_param"] == "value"
def test_specific_param_names(self):
"""Test decorator with specific param names."""
@with_utc_datetimes(param_names=["only_this_param"])
def test_func(only_this_param, not_this_param=None):
return {
"only_this_param": only_this_param,
"not_this_param": not_this_param
}
# Call with both params as datetime strings
result = test_func(
only_this_param="2023-01-15T12:30:45Z",
not_this_param="2023-01-15T12:30:45Z"
)
# Only the specified param should be converted
assert isinstance(result["only_this_param"], datetime)
assert result["only_this_param"].tzinfo == timezone.utc
# The other param should remain a string
assert isinstance(result["not_this_param"], str)
def test_original_function_name_and_docs(self):
"""Test that decorator preserves function name and docstring."""
def original_func(dt_param):
"""Test docstring."""
return dt_param
decorated_func = with_utc_datetimes()(original_func)
assert decorated_func.__name__ == original_func.__name__
assert decorated_func.__doc__ == original_func.__doc__