feat(renewal): 提醒前核对平台真实状态,消除手工数据导致的误报
容错策略:核对失败/无可用通道时标记「未核对」并继续按本地数据提醒, 避免因核对链路故障漏掉真正到期的资产;已停止资产仅在 auto_renew=true 时提醒(停止实例不等于停止扣费)。新增 WHMCS 面板只读适配器, 用于 TotHost 这类没有开放 API 的平台。
This commit is contained in:
@@ -24,6 +24,7 @@ import app.models.provider # noqa: F401
|
||||
import app.models.ssl # noqa: F401
|
||||
from app.models.asset import Asset, AssetStatus, AssetType
|
||||
from app.services import renewal_service
|
||||
from app.services.verification_service import VerificationResult
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -36,11 +37,11 @@ def engine(tmp_path):
|
||||
eng.dispose()
|
||||
|
||||
|
||||
def _seed(engine, name, *, expiry, status=AssetStatus.ACTIVE, provider="vultr"):
|
||||
def _seed(engine, name, *, expiry, status=AssetStatus.ACTIVE, provider="vultr", auto_renew=False):
|
||||
with Session(engine) as s:
|
||||
s.add(Asset(
|
||||
name=name, asset_type=AssetType.VPS, provider=provider,
|
||||
expiry_date=expiry, status=status,
|
||||
expiry_date=expiry, status=status, auto_renew=auto_renew,
|
||||
))
|
||||
s.commit()
|
||||
|
||||
@@ -91,6 +92,27 @@ def test_get_expiring_assets_excludes_cancelled(engine):
|
||||
assert names == ["active"]
|
||||
|
||||
|
||||
def test_get_expiring_assets_stopped_without_auto_renew_excluded(engine):
|
||||
"""已停止且未开自动续费的资产不再提醒"""
|
||||
today = date.today()
|
||||
_seed(engine, "stopped", expiry=today + timedelta(days=5), status=AssetStatus.STOPPED)
|
||||
with Session(engine) as s:
|
||||
result = renewal_service.get_expiring_assets(s, threshold_days=30)
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_get_expiring_assets_stopped_with_auto_renew_included(engine):
|
||||
"""已停止但自动续费仍开着,可能继续扣费,必须提醒"""
|
||||
today = date.today()
|
||||
_seed(
|
||||
engine, "stopped-autorenew", expiry=today + timedelta(days=5),
|
||||
status=AssetStatus.STOPPED, auto_renew=True,
|
||||
)
|
||||
with Session(engine) as s:
|
||||
result = renewal_service.get_expiring_assets(s, threshold_days=30)
|
||||
assert [a.name for a, _ in result] == ["stopped-autorenew"]
|
||||
|
||||
|
||||
def test_get_expiring_assets_no_expiry_date_skipped(engine):
|
||||
"""未填到期日的资产不出现在到期列表"""
|
||||
_seed(engine, "no-expiry", expiry=None)
|
||||
@@ -132,6 +154,26 @@ def test_build_message_no_overdue():
|
||||
assert "共 1 项" in msg
|
||||
|
||||
|
||||
def test_build_message_labels_verification_source():
|
||||
"""消息中区分「已核对」与「未核对」,并给出核对汇总"""
|
||||
today = date.today()
|
||||
a1 = Asset(id=1, name="vps-1", provider="tothost", expiry_date=today - timedelta(days=1))
|
||||
a2 = Asset(id=2, name="vps-2", provider="agnes", expiry_date=today + timedelta(days=5))
|
||||
verification = {
|
||||
1: VerificationResult(
|
||||
asset_id=1, verified=True, source="TotHost 面板",
|
||||
detail="服务 TOT K11 vpn.host:可用,下次到期 2026-11-25",
|
||||
),
|
||||
2: VerificationResult(asset_id=2, verified=False, detail="平台「Agnes」未接入自动核对"),
|
||||
}
|
||||
msg = renewal_service.build_renewal_message(
|
||||
[(a1, -1), (a2, 5)], threshold_days=30, verification=verification
|
||||
)
|
||||
assert "已核对:服务 TOT K11 vpn.host" in msg
|
||||
assert "未核对:平台「Agnes」未接入自动核对" in msg
|
||||
assert "核对:已核对 1 项,未核对 1 项" in msg
|
||||
|
||||
|
||||
# ----------------------------- check_renewals ----------------------------- #
|
||||
def test_check_renewals_no_send(engine):
|
||||
"""send=False 时不触发通知,仅返回预览"""
|
||||
@@ -172,3 +214,48 @@ def test_check_renewals_default_threshold_from_config(engine):
|
||||
with Session(engine) as s:
|
||||
result = renewal_service.check_renewals(s, send=False)
|
||||
assert result["threshold_days"] == renewal_service.settings.RENEWAL_THRESHOLD_DAYS
|
||||
|
||||
|
||||
def test_check_renewals_verification_suppresses_false_alarm(engine):
|
||||
"""核对发现已续费到 90 天后时,本轮不再提醒"""
|
||||
today = date.today()
|
||||
_seed(engine, "renewed", expiry=today - timedelta(days=1))
|
||||
|
||||
def fake_verify(session, assets):
|
||||
results = {}
|
||||
for asset in assets:
|
||||
asset.expiry_date = today + timedelta(days=90)
|
||||
session.add(asset)
|
||||
results[asset.id] = VerificationResult(
|
||||
asset_id=asset.id, verified=True, source="TotHost 面板",
|
||||
expiry_date=asset.expiry_date, detail="已续费",
|
||||
)
|
||||
session.commit()
|
||||
return results
|
||||
|
||||
with Session(engine) as s:
|
||||
with patch.object(
|
||||
renewal_service.verification_service, "verify_assets", side_effect=fake_verify
|
||||
) as mock_verify:
|
||||
result = renewal_service.check_renewals(s, threshold_days=30, send=False)
|
||||
mock_verify.assert_called_once()
|
||||
assert result["expiring_count"] == 0
|
||||
assert result["message"] == ""
|
||||
assert result["notifications"] == []
|
||||
|
||||
|
||||
def test_check_renewals_can_disable_verification(engine):
|
||||
"""verify=False 时不调用核对服务"""
|
||||
today = date.today()
|
||||
_seed(engine, "soon", expiry=today + timedelta(days=5))
|
||||
with Session(engine) as s:
|
||||
with patch.object(
|
||||
renewal_service.verification_service, "verify_assets",
|
||||
side_effect=AssertionError("不应调用核对"),
|
||||
) as mock_verify:
|
||||
result = renewal_service.check_renewals(
|
||||
s, threshold_days=30, send=False, verify=False
|
||||
)
|
||||
mock_verify.assert_not_called()
|
||||
assert result["expiring_count"] == 1
|
||||
assert result["verification"] is None
|
||||
|
||||
Reference in New Issue
Block a user