feat: 增加风控定时扫描 Worker 与环境配置

This commit is contained in:
zhangshy
2026-09-11 09:49:21 +08:00
parent a94d5c754d
commit c2178a985d
10 changed files with 622 additions and 2 deletions
@@ -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"]