Files
ss-tools/backend/tests/test_core/test_timezone.py
busya ce0369ae5c test(backend): add 55+ test files to push coverage to 98%
Subagents delivered tests across all uncovered backend modules:

Schemas (100%): agent, auth, health, profile, settings, validation
Services (98-100%): auth, profile, health, llm, mapping, resource,
  security, git, superset_lookup, sql_table_extractor, rbac
API routes (new): auth, admin, health, environments, plugins,
  dashboards (helpers, projection, actions, listing),
  git (config, deps, env, helpers)
Clean Release (100%): DTO, facade, policy_engine, stages,
  repos, preparation, source_isolation, compliance
Git services: base, remote_providers
Agent module: app, run, middleware, langgraph_setup
Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching
Reports: normalizer, report_service, type_profiles
Notifications: service, providers

Also:
- .gitignore: add .coverage, *.cover, coverage-* dirs
- src/schemas/auth.py: fix AD group DN regex (comma in CN=...)
- Remove co-located src/services/__tests__/ (caused pytest module collision)
2026-06-15 13:55:57 +03:00

167 lines
6.5 KiB
Python

# #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