# #region Test.AppTimezone [C:3] [TYPE Module] [SEMANTICS test,timezone] # @BRIEF Unit tests for AppTimezone module — timezone utilities, caching, and localization. # @RELATION BINDS_TO -> [AppTimezone] # @TEST_EDGE: invalid_timezone -> validate_timezone returns False for unknown names # @TEST_EDGE: none_datetime -> localize returns None for None input # @TEST_EDGE: cache_invalidation -> invalidate clears cache, next call re-reads env import pytest from datetime import datetime from zoneinfo import ZoneInfo from src.core.timezone import ( _get_default_tz_name, get_app_timezone, invalidate_timezone_cache, validate_timezone, localize, now, format_timezone_offset, ) @pytest.fixture(autouse=True) def _clean_cache(): """Ensure cache is clean before and after each test.""" invalidate_timezone_cache() yield invalidate_timezone_cache() # #region test_get_default_tz_name_default [C:2] [TYPE Function] # @BRIEF Without APP_TIMEZONE env var, returns "Europe/Moscow". def test_get_default_tz_name_default(monkeypatch): monkeypatch.delenv("APP_TIMEZONE", raising=False) assert _get_default_tz_name() == "Europe/Moscow" # #endregion test_get_default_tz_name_default # #region test_get_default_tz_name_custom [C:2] [TYPE Function] # @BRIEF With APP_TIMEZONE set, returns the custom value. def test_get_default_tz_name_custom(monkeypatch): monkeypatch.setenv("APP_TIMEZONE", "US/Eastern") assert _get_default_tz_name() == "US/Eastern" # #endregion test_get_default_tz_name_custom # #region test_get_app_timezone_returns_zoneinfo [C:2] [TYPE Function] # @BRIEF Returns a ZoneInfo instance for the configured timezone. def test_get_app_timezone_returns_zoneinfo(monkeypatch): monkeypatch.setenv("APP_TIMEZONE", "Europe/Moscow") tz = get_app_timezone() assert isinstance(tz, ZoneInfo) assert tz.key == "Europe/Moscow" # #endregion test_get_app_timezone_returns_zoneinfo # #region test_get_app_timezone_cached [C:2] [TYPE Function] # @BRIEF Two consecutive calls return the exact same object (identity). def test_get_app_timezone_cached(monkeypatch): monkeypatch.setenv("APP_TIMEZONE", "Europe/Berlin") first = get_app_timezone() second = get_app_timezone() assert first is second # #endregion test_get_app_timezone_cached # #region test_invalidate_and_recreate_cycle [C:2] [TYPE Function] # @BRIEF After invalidate, next call re-reads env and returns a new object. def test_invalidate_and_recreate_cycle(monkeypatch): monkeypatch.setenv("APP_TIMEZONE", "Europe/Moscow") before = get_app_timezone() assert before.key == "Europe/Moscow" invalidate_timezone_cache() monkeypatch.setenv("APP_TIMEZONE", "US/Eastern") after = get_app_timezone() assert after.key == "US/Eastern" assert before is not after # #endregion test_invalidate_and_recreate_cycle # #region test_validate_timezone_valid [C:2] [TYPE Function] # @BRIEF Known IANA timezone names return True. @pytest.mark.parametrize("tz_name", ["Europe/Moscow", "UTC", "US/Eastern", "Asia/Tokyo"]) def test_validate_timezone_valid(tz_name): assert validate_timezone(tz_name) is True # #endregion test_validate_timezone_valid # #region test_validate_timezone_invalid [C:2] [TYPE Function] # @BRIEF Unknown or garbage timezone names return False. @pytest.mark.parametrize("tz_name", ["Invalid/Timezone", "NotATZ", "Foo/Bar/Baz"]) def test_validate_timezone_invalid(tz_name): assert validate_timezone(tz_name) is False # #endregion test_validate_timezone_invalid # #region test_validate_timezone_none [C:2] [TYPE Function] # @BRIEF None input returns False (TypeError caught internally). def test_validate_timezone_none(): assert validate_timezone(None) is False # #endregion test_validate_timezone_none # #region test_validate_timezone_empty_string_raises [C:2] [TYPE Function] # @BRIEF BUG: empty string raises ValueError — production code only catches KeyError/TypeError. def test_validate_timezone_empty_string_raises(): with pytest.raises(ValueError, match="normalized relative paths"): validate_timezone("") # #endregion test_validate_timezone_empty_string_raises # #region test_localize_naive_datetime [C:2] [TYPE Function] # @BRIEF Naive UTC datetime is treated as UTC and converted to app timezone. def test_localize_naive_datetime(monkeypatch): monkeypatch.setenv("APP_TIMEZONE", "Europe/Moscow") invalidate_timezone_cache() naive_utc = datetime(2024, 6, 15, 12, 0, 0) result = localize(naive_utc) # Hardcoded fixture: Moscow is UTC+3 in June (no DST) assert result.hour == 15 assert result.tzinfo is not None assert result.tzinfo.key == "Europe/Moscow" # #endregion test_localize_naive_datetime # #region test_localize_aware_datetime [C:2] [TYPE Function] # @BRIEF Aware datetime in a different timezone is converted to app timezone. def test_localize_aware_datetime(monkeypatch): monkeypatch.setenv("APP_TIMEZONE", "Europe/Moscow") invalidate_timezone_cache() aware_utc = datetime(2024, 1, 15, 10, 0, 0, tzinfo=ZoneInfo("UTC")) result = localize(aware_utc) # Hardcoded fixture: Moscow is UTC+3 in January (no DST) assert result.hour == 13 assert result.tzinfo.key == "Europe/Moscow" # #endregion test_localize_aware_datetime # #region test_localize_none [C:2] [TYPE Function] # @BRIEF None input returns None without error. def test_localize_none(): assert localize(None) is None # #endregion test_localize_none # #region test_now_returns_aware [C:2] [TYPE Function] # @BRIEF now() returns a timezone-aware datetime in the app timezone. def test_now_returns_aware(monkeypatch): monkeypatch.setenv("APP_TIMEZONE", "UTC") invalidate_timezone_cache() result = now() assert result.tzinfo is not None assert result.tzinfo.key == "UTC" # #endregion test_now_returns_aware # #region test_format_timezone_offset [C:2] [TYPE Function] # @BRIEF Returns formatted UTC offset string like "+03:00". def test_format_timezone_offset(monkeypatch): monkeypatch.setenv("APP_TIMEZONE", "Europe/Moscow") invalidate_timezone_cache() offset = format_timezone_offset() # Hardcoded fixture: Moscow is always UTC+3 assert offset == "+03:00" # #endregion test_format_timezone_offset # #region test_format_timezone_offset_utc [C:2] [TYPE Function] # @BRIEF UTC timezone returns "+00:00". def test_format_timezone_offset_utc(monkeypatch): monkeypatch.setenv("APP_TIMEZONE", "UTC") invalidate_timezone_cache() offset = format_timezone_offset() assert offset == "+00:00" # #endregion test_format_timezone_offset_utc # #endregion Test.AppTimezone