- Dockerfile.agent: fix CMD (python -m src.agent.run), use backend/requirements.txt, minimal COPY (only src.agent + src.core.cot_logger), add GRACE contract - docker-compose.yml: SERVICE_TOKEN_SECRET -> SERVICE_JWT (match code) - docker-compose.enterprise-clean.yml: same env var fix - docker/.env.agent.example: same env var fix - build.sh: same env var fix - chore: semantics-testing SKILL.md, backend tests, pyproject.toml
177 lines
6.5 KiB
Python
177 lines
6.5 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 -> [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
|