feat: 增加风控定时扫描 Worker 与环境配置
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from app.service import risk_scan_schedule_config as config_module
|
||||
from app.service.risk_scan_schedule_config import (
|
||||
RiskScanScheduleConfig,
|
||||
load_risk_scan_schedule_config,
|
||||
)
|
||||
|
||||
|
||||
def test_risk_scan_schedule_config_defaults_to_disabled() -> None:
|
||||
config = RiskScanScheduleConfig()
|
||||
|
||||
assert config.enabled is False
|
||||
assert config.interval_minutes == 5
|
||||
assert config.run_immediately is False
|
||||
assert config.retry_limit == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_risk_scan_schedule_config_reads_environment_settings(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
config_module,
|
||||
"get_settings",
|
||||
lambda: SimpleNamespace(
|
||||
risk_scan_schedule_enabled=True,
|
||||
risk_scan_interval_minutes=15,
|
||||
risk_scan_run_immediately=True,
|
||||
risk_scan_retry_limit=1,
|
||||
),
|
||||
)
|
||||
|
||||
config = await load_risk_scan_schedule_config()
|
||||
|
||||
assert config.enabled is True
|
||||
assert config.interval_minutes == 15
|
||||
assert config.run_immediately is True
|
||||
assert config.retry_limit == 1
|
||||
@@ -0,0 +1,127 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import UTC, datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from app.service.risk_scan_schedule_config import RiskScanScheduleConfig
|
||||
from app.worker.risk_scan_scheduler import RiskScanSchedulerWorker
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def acquired_lock():
|
||||
yield True
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def busy_lock():
|
||||
yield False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disabled_schedule_does_not_scan() -> None:
|
||||
calls = 0
|
||||
|
||||
async def scan():
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
return {"created_count": 1}
|
||||
|
||||
async def config_loader():
|
||||
return RiskScanScheduleConfig(enabled=False)
|
||||
|
||||
worker = RiskScanSchedulerWorker(
|
||||
config_loader=config_loader,
|
||||
scan_executor=scan,
|
||||
audit_writer=lambda *_args: None, # type: ignore[arg-type]
|
||||
lock_factory=acquired_lock,
|
||||
)
|
||||
|
||||
assert await worker.run_once(force=True) is False
|
||||
assert calls == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_force_scan_runs_once_and_writes_audit() -> None:
|
||||
calls = 0
|
||||
audits: list[tuple[str, dict[str, object]]] = []
|
||||
|
||||
async def scan():
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
return {"created_count": 2, "high_risk_count": 1}
|
||||
|
||||
async def config_loader():
|
||||
return RiskScanScheduleConfig(enabled=True, interval_minutes=5)
|
||||
|
||||
async def audit(status, detail):
|
||||
audits.append((status, detail))
|
||||
|
||||
worker = RiskScanSchedulerWorker(
|
||||
config_loader=config_loader,
|
||||
scan_executor=scan,
|
||||
audit_writer=audit,
|
||||
lock_factory=acquired_lock,
|
||||
now=lambda: datetime(2026, 9, 11, 0, 0, tzinfo=UTC),
|
||||
)
|
||||
|
||||
assert await worker.run_once(force=True) is True
|
||||
assert calls == 1
|
||||
assert audits[0][0] == "succeeded"
|
||||
assert audits[0][1]["created_count"] == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_busy_lock_skips_scan() -> None:
|
||||
calls = 0
|
||||
|
||||
async def scan():
|
||||
nonlocal calls
|
||||
calls += 1
|
||||
return {"created_count": 1}
|
||||
|
||||
async def config_loader():
|
||||
return RiskScanScheduleConfig(enabled=True, run_immediately=True)
|
||||
|
||||
worker = RiskScanSchedulerWorker(
|
||||
config_loader=config_loader,
|
||||
scan_executor=scan,
|
||||
audit_writer=lambda *_args: None, # type: ignore[arg-type]
|
||||
lock_factory=busy_lock,
|
||||
)
|
||||
|
||||
assert await worker.run_once() is False
|
||||
assert calls == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scan_retries_before_success() -> None:
|
||||
attempts = 0
|
||||
audits: list[str] = []
|
||||
|
||||
async def scan():
|
||||
nonlocal attempts
|
||||
attempts += 1
|
||||
if attempts == 1:
|
||||
raise RuntimeError("transient")
|
||||
return {"created_count": 1}
|
||||
|
||||
async def config_loader():
|
||||
return RiskScanScheduleConfig(
|
||||
enabled=True,
|
||||
run_immediately=True,
|
||||
retry_limit=1,
|
||||
)
|
||||
|
||||
async def audit(status, _detail):
|
||||
audits.append(status)
|
||||
|
||||
worker = RiskScanSchedulerWorker(
|
||||
config_loader=config_loader,
|
||||
scan_executor=scan,
|
||||
audit_writer=audit,
|
||||
lock_factory=acquired_lock,
|
||||
)
|
||||
|
||||
assert await worker.run_once() is True
|
||||
assert attempts == 2
|
||||
assert audits == ["succeeded"]
|
||||
Reference in New Issue
Block a user