Files
ss-tools/backend/tests/core/test_timezone.py
root 632b730fff chore: migrate GRACE-Poly anchors to hierarchical dotted naming
Systematic rename of all semantic anchors (#region, [DEF], @RELATION)
across 1400+ files — backend Python, frontend Svelte/TS, specs, docs:
- Flat anchors become Namespace.Module.Entity
- @RELATION references updated to match new anchor paths
- Zero business logic changes
2026-07-22 11:48:15 +03:00

177 lines
6.9 KiB
Python

# #region Test.AppTimezone [C:3] [TYPE Module] [SEMANTICS test,timezone]
# @BRIEF Unit tests for AppTimezone module — timezone utilities, caching, and localization.
# @RELATION BINDS_TO -> [Core.Timezone.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.AppTimezone.TestGetDefaultTzNameDefault [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.AppTimezone.TestGetDefaultTzNameDefault
# #region Test.AppTimezone.TestGetDefaultTzNameCustom [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.AppTimezone.TestGetDefaultTzNameCustom
# #region Test.AppTimezone.TestGetAppTimezoneReturnsZoneinfo [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.AppTimezone.TestGetAppTimezoneReturnsZoneinfo
# #region Test.AppTimezone.TestGetAppTimezoneCached [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.AppTimezone.TestGetAppTimezoneCached
# #region Test.AppTimezone.TestInvalidateAndRecreateCycle [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.AppTimezone.TestInvalidateAndRecreateCycle
# #region Test.AppTimezone.TestValidateTimezoneValid [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.AppTimezone.TestValidateTimezoneValid
# #region Test.AppTimezone.TestValidateTimezoneInvalid [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.AppTimezone.TestValidateTimezoneInvalid
# #region Test.AppTimezone.TestValidateTimezoneNone [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.AppTimezone.TestValidateTimezoneNone
# #region Test.AppTimezone.TestValidateTimezoneEmptyStringRaises [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.AppTimezone.TestValidateTimezoneEmptyStringRaises
# #region Test.AppTimezone.TestLocalizeNaiveDatetime [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.AppTimezone.TestLocalizeNaiveDatetime
# #region Test.AppTimezone.TestLocalizeAwareDatetime [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.AppTimezone.TestLocalizeAwareDatetime
# #region Test.AppTimezone.TestLocalizeNone [C:2] [TYPE Function]
# @BRIEF None input returns None without error.
def test_localize_none():
assert localize(None) is None
# #endregion Test.AppTimezone.TestLocalizeNone
# #region Test.AppTimezone.TestNowReturnsAware [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.AppTimezone.TestNowReturnsAware
# #region Test.AppTimezone.TestFormatTimezoneOffset [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.AppTimezone.TestFormatTimezoneOffset
# #region Test.AppTimezone.TestFormatTimezoneOffsetUtc [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.AppTimezone.TestFormatTimezoneOffsetUtc
# #endregion Test.AppTimezone