499 lines
16 KiB
Python
499 lines
16 KiB
Python
|
|
"""external_systems scheduler handler 单元测试。
|
|||
|
|
|
|||
|
|
infrastructure 包 ``__init__.py`` 在本地非容器环境导入 ``container.py`` 时会失败,
|
|||
|
|
因此本测试通过 ``importlib`` 直接加载 ``scheduler.py`` 模块,避免触发包级装配代码。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import importlib.util
|
|||
|
|
import sys
|
|||
|
|
import types
|
|||
|
|
from datetime import timedelta
|
|||
|
|
from pathlib import Path
|
|||
|
|
from typing import Any
|
|||
|
|
from unittest.mock import AsyncMock, MagicMock
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
|
|||
|
|
from yuxi.scheduler.core.contracts import TaskContext, TaskResult
|
|||
|
|
from yuxi.scheduler.framework.runtime import HandlerRegistry
|
|||
|
|
from yuxi.utils.datetime_utils import utc_now_naive
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _load_scheduler_module() -> Any:
|
|||
|
|
"""直接加载 scheduler.py 模块,绕过 infrastructure 包 __init__.py。"""
|
|||
|
|
module_path = Path(__file__).parents[5] / "backend/package/yuxi/external_systems/infrastructure/scheduler.py"
|
|||
|
|
spec = importlib.util.spec_from_file_location(
|
|||
|
|
"external_systems_scheduler",
|
|||
|
|
module_path.resolve(),
|
|||
|
|
)
|
|||
|
|
assert spec is not None and spec.loader is not None
|
|||
|
|
module = importlib.util.module_from_spec(spec)
|
|||
|
|
spec.loader.exec_module(module)
|
|||
|
|
return module
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _make_task_context(payload: dict[str, Any] | None = None) -> TaskContext:
|
|||
|
|
return TaskContext(
|
|||
|
|
task_id="task-1",
|
|||
|
|
run_id="run-1",
|
|||
|
|
handler_name="external_systems.webhook_event_cleanup",
|
|||
|
|
payload=payload or {},
|
|||
|
|
triggered_by="scheduler",
|
|||
|
|
scheduled_at=utc_now_naive(),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _make_repos(
|
|||
|
|
deleted_count: int = 0,
|
|||
|
|
near_critical: list[Any] | None = None,
|
|||
|
|
near_warning: list[Any] | None = None,
|
|||
|
|
expiring_quotas: list[Any] | None = None,
|
|||
|
|
) -> MagicMock:
|
|||
|
|
repos = MagicMock()
|
|||
|
|
repos.webhook_event = AsyncMock()
|
|||
|
|
repos.webhook_event.delete_old_events = AsyncMock(return_value=deleted_count)
|
|||
|
|
repos.quota_usage = AsyncMock()
|
|||
|
|
repos.quota_usage.list_near_critical = AsyncMock(return_value=near_critical or [])
|
|||
|
|
repos.quota_usage.list_near_warning = AsyncMock(return_value=near_warning or [])
|
|||
|
|
repos.quota_usage.list = AsyncMock(return_value=expiring_quotas or [])
|
|||
|
|
repos.quota_usage.reset_window = AsyncMock()
|
|||
|
|
repos.alert = AsyncMock()
|
|||
|
|
return repos
|
|||
|
|
|
|||
|
|
|
|||
|
|
class _QuotaStub:
|
|||
|
|
"""配额记录存根,用于 quota handler 测试。"""
|
|||
|
|
|
|||
|
|
def __init__(
|
|||
|
|
self,
|
|||
|
|
quota_id: int | None = None,
|
|||
|
|
system_id: int = 1,
|
|||
|
|
env_key: str = "default",
|
|||
|
|
quota_key: str = "api_calls",
|
|||
|
|
quota_name: str = "API Calls",
|
|||
|
|
quota_window: str = "daily",
|
|||
|
|
limit_value: int = 1000,
|
|||
|
|
used_value: int = 0,
|
|||
|
|
usage_ratio: float = 0.0,
|
|||
|
|
warning_threshold: float = 80.0,
|
|||
|
|
critical_threshold: float = 95.0,
|
|||
|
|
window_start: Any | None = None,
|
|||
|
|
window_end: Any | None = None,
|
|||
|
|
) -> None:
|
|||
|
|
self.id = quota_id
|
|||
|
|
self.system_id = system_id
|
|||
|
|
self.env_key = env_key
|
|||
|
|
self.quota_key = quota_key
|
|||
|
|
self.quota_name = quota_name
|
|||
|
|
self.quota_window = quota_window
|
|||
|
|
self.limit_value = limit_value
|
|||
|
|
self.used_value = used_value
|
|||
|
|
self.usage_ratio = usage_ratio
|
|||
|
|
self.warning_threshold = warning_threshold
|
|||
|
|
self.critical_threshold = critical_threshold
|
|||
|
|
self.window_start = window_start
|
|||
|
|
self.window_end = window_end
|
|||
|
|
|
|||
|
|
|
|||
|
|
class _FakeSession:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
class _FakeSessionFactory:
|
|||
|
|
async def __aenter__(self) -> _FakeSession:
|
|||
|
|
return _FakeSession()
|
|||
|
|
|
|||
|
|
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def scheduler_module(monkeypatch: pytest.MonkeyPatch) -> Any:
|
|||
|
|
"""加载 scheduler 模块,并将其内部 create_repositories 替换为返回 mock repos 的工厂。"""
|
|||
|
|
module = _load_scheduler_module()
|
|||
|
|
repos = _make_repos(deleted_count=5)
|
|||
|
|
monkeypatch.setattr(
|
|||
|
|
module,
|
|||
|
|
"create_repositories",
|
|||
|
|
lambda _db: repos,
|
|||
|
|
)
|
|||
|
|
return module
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_cleanupHandler_uses_default_retention_days(scheduler_module: Any) -> None:
|
|||
|
|
handler = scheduler_module.WebhookEventCleanupHandler(
|
|||
|
|
session_factory=_FakeSessionFactory,
|
|||
|
|
default_retention_days=30,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert isinstance(result, TaskResult)
|
|||
|
|
assert result.success is True
|
|||
|
|
assert result.output["deleted_count"] == 5
|
|||
|
|
assert result.output["retention_days"] == 30
|
|||
|
|
repos = scheduler_module.create_repositories(None)
|
|||
|
|
repos.webhook_event.delete_old_events.assert_called_once()
|
|||
|
|
before = repos.webhook_event.delete_old_events.call_args.args[0]
|
|||
|
|
assert before < utc_now_naive()
|
|||
|
|
assert before > utc_now_naive() - timedelta(days=31)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_cleanupHandler_uses_payload_retention_days(scheduler_module: Any) -> None:
|
|||
|
|
repos = _make_repos(deleted_count=0)
|
|||
|
|
scheduler_module.create_repositories = lambda _db: repos
|
|||
|
|
handler = scheduler_module.WebhookEventCleanupHandler(
|
|||
|
|
session_factory=_FakeSessionFactory,
|
|||
|
|
default_retention_days=30,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
result = await handler.execute(_make_task_context({"retention_days": 7}))
|
|||
|
|
|
|||
|
|
assert result.success is True
|
|||
|
|
assert result.output["retention_days"] == 7
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_cleanupHandler_returns_failure_on_exception(scheduler_module: Any) -> None:
|
|||
|
|
repos = _make_repos()
|
|||
|
|
repos.webhook_event.delete_old_events = AsyncMock(side_effect=RuntimeError("db down"))
|
|||
|
|
scheduler_module.create_repositories = lambda _db: repos
|
|||
|
|
handler = scheduler_module.WebhookEventCleanupHandler(session_factory=_FakeSessionFactory)
|
|||
|
|
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert result.success is False
|
|||
|
|
assert "db down" in (result.error or "")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
def test_registerSchedulerHandlers_registers_cleanup_handler() -> None:
|
|||
|
|
module = _load_scheduler_module()
|
|||
|
|
registry = HandlerRegistry()
|
|||
|
|
module.register_scheduler_handlers(registry)
|
|||
|
|
|
|||
|
|
handler = registry.get("external_systems.webhook_event_cleanup")
|
|||
|
|
assert handler is not None
|
|||
|
|
assert handler.name == "external_systems.webhook_event_cleanup"
|
|||
|
|
assert handler.description == "清理超过保留期的 Webhook 事件"
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── SecretRotationHandler ─────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _patch_container_for_secret_rotation(
|
|||
|
|
monkeypatch: pytest.MonkeyPatch,
|
|||
|
|
scheduled_rotation_output: Any,
|
|||
|
|
) -> AsyncMock:
|
|||
|
|
"""向 sys.modules 注入 fake container,避免本地非容器环境导入 app_config 失败。"""
|
|||
|
|
fake_service = AsyncMock()
|
|||
|
|
fake_service.scheduled_rotation = AsyncMock(return_value=scheduled_rotation_output)
|
|||
|
|
fake_use_cases = MagicMock()
|
|||
|
|
fake_use_cases.secret_rotation_service = fake_service
|
|||
|
|
|
|||
|
|
fake_container = types.ModuleType("yuxi.external_systems.infrastructure.container")
|
|||
|
|
fake_container.create_use_cases_from_db = lambda _db: fake_use_cases
|
|||
|
|
monkeypatch.setitem(
|
|||
|
|
sys.modules,
|
|||
|
|
"yuxi.external_systems.infrastructure.container",
|
|||
|
|
fake_container,
|
|||
|
|
)
|
|||
|
|
return fake_service
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_secretRotationHandler_executes_scheduled_rotation(
|
|||
|
|
scheduler_module: Any,
|
|||
|
|
monkeypatch: pytest.MonkeyPatch,
|
|||
|
|
) -> None:
|
|||
|
|
from yuxi.external_systems.use_cases.dto.secret_rotation import ScheduledRotationOutput
|
|||
|
|
|
|||
|
|
fake_service = _patch_container_for_secret_rotation(
|
|||
|
|
monkeypatch,
|
|||
|
|
ScheduledRotationOutput(affected=2, succeeded=2, failed=0),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
handler = scheduler_module.SecretRotationHandler(session_factory=_FakeSessionFactory)
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert isinstance(result, TaskResult)
|
|||
|
|
assert result.success is True
|
|||
|
|
assert result.output["affected"] == 2
|
|||
|
|
assert result.output["succeeded"] == 2
|
|||
|
|
fake_service.scheduled_rotation.assert_awaited_once()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_secretRotationHandler_returns_failure_on_exception(
|
|||
|
|
scheduler_module: Any,
|
|||
|
|
monkeypatch: pytest.MonkeyPatch,
|
|||
|
|
) -> None:
|
|||
|
|
fake_service = AsyncMock()
|
|||
|
|
fake_service.scheduled_rotation = AsyncMock(side_effect=RuntimeError("rotation failed"))
|
|||
|
|
fake_use_cases = MagicMock()
|
|||
|
|
fake_use_cases.secret_rotation_service = fake_service
|
|||
|
|
|
|||
|
|
fake_container = types.ModuleType("yuxi.external_systems.infrastructure.container")
|
|||
|
|
fake_container.create_use_cases_from_db = lambda _db: fake_use_cases
|
|||
|
|
monkeypatch.setitem(
|
|||
|
|
sys.modules,
|
|||
|
|
"yuxi.external_systems.infrastructure.container",
|
|||
|
|
fake_container,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
handler = scheduler_module.SecretRotationHandler(session_factory=_FakeSessionFactory)
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert result.success is False
|
|||
|
|
assert "rotation failed" in (result.error or "")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── QuotaThresholdAlertHandler ────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_quotaThresholdAlertHandler_fires_critical_and_warning(
|
|||
|
|
scheduler_module: Any,
|
|||
|
|
monkeypatch: pytest.MonkeyPatch,
|
|||
|
|
) -> None:
|
|||
|
|
critical_quota = _QuotaStub(quota_id=1, quota_name="Q1", usage_ratio=0.96)
|
|||
|
|
warning_quota = _QuotaStub(quota_id=2, quota_name="Q2", usage_ratio=0.85)
|
|||
|
|
repos = _make_repos(
|
|||
|
|
near_critical=[critical_quota],
|
|||
|
|
near_warning=[warning_quota],
|
|||
|
|
)
|
|||
|
|
scheduler_module.create_repositories = lambda _db: repos
|
|||
|
|
|
|||
|
|
class _FakeAlertManager:
|
|||
|
|
fired: list[dict[str, Any]] = []
|
|||
|
|
|
|||
|
|
async def fire(
|
|||
|
|
self,
|
|||
|
|
system_id: Any,
|
|||
|
|
env_key: Any,
|
|||
|
|
alert_type: Any,
|
|||
|
|
**kwargs: Any,
|
|||
|
|
) -> None:
|
|||
|
|
self.fired.append(
|
|||
|
|
{
|
|||
|
|
"system_id": system_id,
|
|||
|
|
"env_key": env_key,
|
|||
|
|
"alert_type": alert_type,
|
|||
|
|
**kwargs,
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def build_dedup_key(self, *args: Any, **kwargs: Any) -> str:
|
|||
|
|
return "dedup"
|
|||
|
|
|
|||
|
|
fake_alert_manager = _FakeAlertManager()
|
|||
|
|
monkeypatch.setattr(
|
|||
|
|
scheduler_module,
|
|||
|
|
"AlertManagerImpl",
|
|||
|
|
lambda _alert_repo: fake_alert_manager,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
handler = scheduler_module.QuotaThresholdAlertHandler(
|
|||
|
|
session_factory=_FakeSessionFactory,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert result.success is True
|
|||
|
|
assert result.output["critical_count"] == 1
|
|||
|
|
assert result.output["warning_count"] == 1
|
|||
|
|
assert len(fake_alert_manager.fired) == 2
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_quotaThresholdAlertHandler_dedup_warning_when_critical(
|
|||
|
|
scheduler_module: Any,
|
|||
|
|
monkeypatch: pytest.MonkeyPatch,
|
|||
|
|
) -> None:
|
|||
|
|
quota = _QuotaStub(quota_id=1, quota_name="Q1", usage_ratio=0.97)
|
|||
|
|
repos = _make_repos(
|
|||
|
|
near_critical=[quota],
|
|||
|
|
near_warning=[quota],
|
|||
|
|
)
|
|||
|
|
scheduler_module.create_repositories = lambda _db: repos
|
|||
|
|
|
|||
|
|
class _FakeAlertManager:
|
|||
|
|
fired: list[dict[str, Any]] = []
|
|||
|
|
|
|||
|
|
async def fire(
|
|||
|
|
self,
|
|||
|
|
system_id: Any,
|
|||
|
|
env_key: Any,
|
|||
|
|
alert_type: Any,
|
|||
|
|
**kwargs: Any,
|
|||
|
|
) -> None:
|
|||
|
|
self.fired.append(
|
|||
|
|
{
|
|||
|
|
"system_id": system_id,
|
|||
|
|
"env_key": env_key,
|
|||
|
|
"alert_type": alert_type,
|
|||
|
|
**kwargs,
|
|||
|
|
}
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
fake_alert_manager = _FakeAlertManager()
|
|||
|
|
monkeypatch.setattr(
|
|||
|
|
scheduler_module,
|
|||
|
|
"AlertManagerImpl",
|
|||
|
|
lambda _alert_repo: fake_alert_manager,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
handler = scheduler_module.QuotaThresholdAlertHandler(
|
|||
|
|
session_factory=_FakeSessionFactory,
|
|||
|
|
)
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert result.success is True
|
|||
|
|
assert result.output["warning_count"] == 0
|
|||
|
|
assert len(fake_alert_manager.fired) == 1
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_quotaThresholdAlertHandler_returns_failure_on_exception(scheduler_module: Any) -> None:
|
|||
|
|
repos = _make_repos()
|
|||
|
|
repos.quota_usage.list_near_critical = AsyncMock(side_effect=RuntimeError("db down"))
|
|||
|
|
scheduler_module.create_repositories = lambda _db: repos
|
|||
|
|
|
|||
|
|
handler = scheduler_module.QuotaThresholdAlertHandler(
|
|||
|
|
session_factory=_FakeSessionFactory,
|
|||
|
|
)
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert result.success is False
|
|||
|
|
assert "db down" in (result.error or "")
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── QuotaWindowResetHandler ───────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_quotaWindowResetHandler_resets_expired_windows(scheduler_module: Any) -> None:
|
|||
|
|
now = utc_now_naive()
|
|||
|
|
quota = _QuotaStub(
|
|||
|
|
system_id=1,
|
|||
|
|
env_key="default",
|
|||
|
|
quota_key="api_calls",
|
|||
|
|
quota_window="daily",
|
|||
|
|
window_start=now - timedelta(days=2),
|
|||
|
|
window_end=now - timedelta(hours=1),
|
|||
|
|
)
|
|||
|
|
repos = _make_repos(expiring_quotas=[quota])
|
|||
|
|
scheduler_module.create_repositories = lambda _db: repos
|
|||
|
|
|
|||
|
|
handler = scheduler_module.QuotaWindowResetHandler(
|
|||
|
|
session_factory=_FakeSessionFactory,
|
|||
|
|
)
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert result.success is True
|
|||
|
|
assert result.output["reset_count"] == 1
|
|||
|
|
assert result.output["failed_count"] == 0
|
|||
|
|
repos.quota_usage.reset_window.assert_called_once()
|
|||
|
|
call = repos.quota_usage.reset_window.call_args
|
|||
|
|
assert call.kwargs["used_value"] == 0
|
|||
|
|
assert call.kwargs["updated_by"] == "scheduler"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_quotaWindowResetHandler_rolling_24h_window(scheduler_module: Any) -> None:
|
|||
|
|
now = utc_now_naive()
|
|||
|
|
quota = _QuotaStub(
|
|||
|
|
quota_window="rolling_24h",
|
|||
|
|
window_start=now - timedelta(days=2),
|
|||
|
|
window_end=now - timedelta(hours=1),
|
|||
|
|
)
|
|||
|
|
repos = _make_repos(expiring_quotas=[quota])
|
|||
|
|
scheduler_module.create_repositories = lambda _db: repos
|
|||
|
|
|
|||
|
|
handler = scheduler_module.QuotaWindowResetHandler(
|
|||
|
|
session_factory=_FakeSessionFactory,
|
|||
|
|
)
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert result.success is True
|
|||
|
|
assert result.output["reset_count"] == 1
|
|||
|
|
repos.quota_usage.reset_window.assert_called_once()
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_quotaWindowResetHandler_isolates_single_failure(scheduler_module: Any) -> None:
|
|||
|
|
now = utc_now_naive()
|
|||
|
|
quota = _QuotaStub(
|
|||
|
|
system_id=1,
|
|||
|
|
env_key="default",
|
|||
|
|
quota_key="api_calls",
|
|||
|
|
quota_window="daily",
|
|||
|
|
window_end=now - timedelta(hours=1),
|
|||
|
|
)
|
|||
|
|
repos = _make_repos(expiring_quotas=[quota])
|
|||
|
|
repos.quota_usage.reset_window = AsyncMock(side_effect=RuntimeError("locked"))
|
|||
|
|
scheduler_module.create_repositories = lambda _db: repos
|
|||
|
|
|
|||
|
|
handler = scheduler_module.QuotaWindowResetHandler(
|
|||
|
|
session_factory=_FakeSessionFactory,
|
|||
|
|
)
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert result.success is True
|
|||
|
|
assert result.output["reset_count"] == 0
|
|||
|
|
assert result.output["failed_count"] == 1
|
|||
|
|
assert len(result.output["errors"]) == 1
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
@pytest.mark.asyncio
|
|||
|
|
async def test_quotaWindowResetHandler_returns_failure_on_exception(scheduler_module: Any) -> None:
|
|||
|
|
repos = _make_repos()
|
|||
|
|
repos.quota_usage.list = AsyncMock(side_effect=RuntimeError("db down"))
|
|||
|
|
scheduler_module.create_repositories = lambda _db: repos
|
|||
|
|
|
|||
|
|
handler = scheduler_module.QuotaWindowResetHandler(
|
|||
|
|
session_factory=_FakeSessionFactory,
|
|||
|
|
)
|
|||
|
|
result = await handler.execute(_make_task_context())
|
|||
|
|
|
|||
|
|
assert result.success is False
|
|||
|
|
assert "db down" in (result.error or "")
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
def test_registerSchedulerHandlers_registers_quota_handlers() -> None:
|
|||
|
|
module = _load_scheduler_module()
|
|||
|
|
registry = HandlerRegistry()
|
|||
|
|
module.register_scheduler_handlers(registry)
|
|||
|
|
|
|||
|
|
assert registry.get("external_systems.quota_threshold_alert") is not None
|
|||
|
|
assert registry.get("external_systems.quota_window_reset") is not None
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.mark.unit
|
|||
|
|
def test_registerSchedulerHandlers_registers_secret_rotation_handler() -> None:
|
|||
|
|
module = _load_scheduler_module()
|
|||
|
|
registry = HandlerRegistry()
|
|||
|
|
module.register_scheduler_handlers(registry)
|
|||
|
|
|
|||
|
|
handler = registry.get("external_systems.secret_rotation")
|
|||
|
|
assert handler is not None
|
|||
|
|
assert handler.name == "external_systems.secret_rotation"
|
|||
|
|
assert handler.description == "检查到期密钥轮换策略并执行轮换/告警"
|