# #region Test.AppTimezone [C:3] [TYPE Module] [SEMANTICS test,timezone,datetime,utilities] # @BRIEF Tests for core/timezone.py — all timezone utility functions. # @RELATION BINDS_TO -> [AppTimezone] from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src")) from datetime import datetime, timezone, timedelta from unittest.mock import patch import pytest from zoneinfo import ZoneInfo # #region test_get_default_tz_name [C:2] [TYPE Function] # @BRIEF Test _get_default_tz_name reads env var or default. class TestGetDefaultTzName: def test_returns_env_value(self): from src.core.timezone import _get_default_tz_name with patch("src.core.timezone.os.getenv", return_value="America/New_York"): assert _get_default_tz_name() == "America/New_York" def test_returns_default_when_not_set(self): from src.core.timezone import _get_default_tz_name with patch("src.core.timezone.os.getenv", return_value=None): # os.getenv returns None if not set, but _get_default_tz_name uses getenv with default pass # The function uses os.getenv("APP_TIMEZONE", "Europe/Moscow"), so we can't test unset easily # Let's test directly with patch("src.core.timezone.os.getenv", return_value="Europe/Moscow"): assert _get_default_tz_name() == "Europe/Moscow" # #endregion test_get_default_tz_name # #region test_get_app_timezone [C:2] [TYPE Function] # @BRIEF Test get_app_timezone returns cached ZoneInfo. class TestGetAppTimezone: def test_returns_zoneinfo(self): from src.core.timezone import get_app_timezone, invalidate_timezone_cache invalidate_timezone_cache() tz = get_app_timezone() assert isinstance(tz, ZoneInfo) def test_caches_result(self): from src.core.timezone import get_app_timezone, invalidate_timezone_cache invalidate_timezone_cache() tz1 = get_app_timezone() tz2 = get_app_timezone() assert tz1 is tz2 # Same cached object # #endregion test_get_app_timezone # #region test_invalidate_timezone_cache [C:2] [TYPE Function] # @BRIEF Test invalidate_timezone_cache resets the cache. class TestInvalidateTimezoneCache: def test_invalidates_cache(self): import src.core.timezone as tz_mod tz_mod._APP_TZ_CACHE = None # Reset tz_mod.invalidate_timezone_cache() assert tz_mod._APP_TZ_CACHE is None # Cache cleared tz1 = tz_mod.get_app_timezone() assert tz_mod._APP_TZ_CACHE is not None # Cache populated tz_mod.invalidate_timezone_cache() assert tz_mod._APP_TZ_CACHE is None # Cache cleared again tz_mod._APP_TZ_CACHE = None # Reset for other tests # #endregion test_invalidate_timezone_cache # #region test_validate_timezone [C:2] [TYPE Function] # @BRIEF Test validate_timezone for valid/invalid IANA names. class TestValidateTimezone: def test_valid_timezone(self): from src.core.timezone import validate_timezone assert validate_timezone("Europe/Moscow") is True assert validate_timezone("UTC") is True assert validate_timezone("America/New_York") is True def test_invalid_timezone(self): from src.core.timezone import validate_timezone assert validate_timezone("Invalid/Zone") is False def test_none_raises_type_error(self): from src.core.timezone import validate_timezone # This should return False when TypeError is caught assert validate_timezone(None) is False # type: ignore # #endregion test_validate_timezone # #region test_localize [C:2] [TYPE Function] # @BRIEF Test localize converts UTC to app timezone. class TestLocalize: def test_none_returns_none(self): from src.core.timezone import localize assert localize(None) is None def test_converts_naive_utc_to_tz(self): from src.core.timezone import localize, invalidate_timezone_cache from src.core.timezone import _get_default_tz_name invalidate_timezone_cache() dt = datetime(2024, 1, 15, 12, 0, 0) # Naive UTC result = localize(dt) assert result is not None assert result.tzinfo is not None # Should be in Europe/Moscow (+03:00) assert result.hour == 15 # Moscow is UTC+3 def test_converts_aware_utc_to_tz(self): from src.core.timezone import localize, invalidate_timezone_cache invalidate_timezone_cache() dt = datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc) result = localize(dt) assert result is not None assert result.tzinfo is not None offset = result.utcoffset() assert offset is not None assert offset.total_seconds() == 3 * 3600 # Moscow is UTC+3 def test_preserves_non_utc_aware(self): from src.core.timezone import localize, invalidate_timezone_cache invalidate_timezone_cache() dt = datetime(2024, 1, 15, 12, 0, 0, tzinfo=ZoneInfo("America/New_York")) result = localize(dt) assert result is not None # Should convert to Moscow time assert result.tzinfo is not None assert "Europe/Moscow" in str(result.tzinfo) or "+03" in result.isoformat() # #endregion test_localize # #region test_now [C:2] [TYPE Function] # @BRIEF Test now returns timezone-aware datetime. class TestNow: def test_returns_aware_datetime(self): from src.core.timezone import now result = now() assert result.tzinfo is not None def test_in_app_timezone(self): from src.core.timezone import now, invalidate_timezone_cache invalidate_timezone_cache() result = now() offset = result.utcoffset() assert offset is not None assert offset.total_seconds() == 3 * 3600 # Europe/Moscow # #endregion test_now # #region test_format_timezone_offset [C:2] [TYPE Function] # @BRIEF Test format_timezone_offset returns formatted offset string. class TestFormatTimezoneOffset: def test_returns_formatted_offset(self): from src.core.timezone import format_timezone_offset offset = format_timezone_offset() assert offset.startswith("+") or offset.startswith("-") assert ":" in offset assert len(offset) == 6 # e.g. "+03:00" def test_offset_is_moscow(self): from src.core.timezone import format_timezone_offset, invalidate_timezone_cache invalidate_timezone_cache() result = format_timezone_offset() assert result == "+03:00" # #endregion test_format_timezone_offset # #endregion Test.AppTimezone