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)
167 lines
6.5 KiB
Python
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
|