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
177 lines
6.9 KiB
Python
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
|