Files
vps-manager/tests/test_verification_service.py
T
gouki abd2fa8206 feat(renewal): 提醒前核对平台真实状态,消除手工数据导致的误报
容错策略:核对失败/无可用通道时标记「未核对」并继续按本地数据提醒,
避免因核对链路故障漏掉真正到期的资产;已停止资产仅在 auto_renew=true
时提醒(停止实例不等于停止扣费)。新增 WHMCS 面板只读适配器,
用于 TotHost 这类没有开放 API 的平台。
2026-10-25 23:05:39 +00:00

208 lines
7.0 KiB
Python

"""verification_service 测试:发送前平台状态核对
覆盖:
- WHMCS 面板:按 external_id 更新到期日/状态
- WHMCS 面板:服务已从面板消失 -> 标记已注销
- WHMCS 面板:账号下唯一未关联资产 + 唯一在用服务 -> 自动补 external_id
- 云厂商 API:按 external_id 更新到期日/状态
- 平台无适配器 -> 未核对
网络适配器全部用 fake 替换,不发起真实请求。
"""
from datetime import date
import pytest
from sqlmodel import Session, SQLModel, create_engine
import app.models.asset # noqa: F401
import app.models.credential # noqa: F401
import app.models.monitor # noqa: F401
import app.models.provider # noqa: F401
import app.models.ssl # noqa: F401
from app.adapters.base import NormalizedVPS
from app.models.asset import Account, Asset, AssetStatus, AssetType, VPSDetail
from app.models.provider import Provider, ProviderCategory
from app.services import sync_service, verification_service
@pytest.fixture()
def engine(tmp_path):
eng = create_engine(
f"sqlite:///{tmp_path / 'assets.db'}", connect_args={"check_same_thread": False}
)
SQLModel.metadata.create_all(eng)
yield eng
eng.dispose()
def _seed_provider(session, sdk_type="whmcs-panel"):
provider = Provider(
slug="tothost", name="TotHost", category=ProviderCategory.VPS,
services="vps", sdk_type=sdk_type,
console_url="https://billing.example.com/clientarea.php",
)
session.add(provider)
session.commit()
session.refresh(provider)
return provider
def _seed_account(session, provider):
account = Account(name="user@example.com", platform=provider.slug)
session.add(account)
session.commit()
session.refresh(account)
return account
def _seed_asset(session, provider, account, *, name="vn-200", ip="103.70.115.57",
expiry=date(2026, 10, 6), external_id=None):
asset = Asset(
name=name, asset_type=AssetType.VPS, provider=provider.slug,
provider_id=provider.id, account_id=account.id if account else None,
expiry_date=expiry, status=AssetStatus.ACTIVE, external_id=external_id,
)
session.add(asset)
session.commit()
session.refresh(asset)
session.add(VPSDetail(asset_id=asset.id, ip_address=ip))
session.commit()
return asset
class FakeWHMCS:
def __init__(self, services):
self.services = services
def fetch_services(self, include_terminated=True):
if include_terminated:
return self.services
return [s for s in self.services if s["status"] != "cancelled"]
def _service(sid="36979", domain="remote-dev", expiry=date(2027, 1, 7), status="active"):
return {
"id": sid, "product": "TOT K11", "domain": domain,
"label": f"TOT K11 {domain}", "expiry_date": expiry, "status": status,
"status_class": status, "status_text": "可用", "amount": None, "cycle": "季繳",
}
def test_whmcs_updates_expiry_and_links_external_id(engine, monkeypatch):
with Session(engine) as s:
provider = _seed_provider(s)
account = _seed_account(s, provider)
asset = _seed_asset(s, provider, account)
fake = FakeWHMCS([_service()])
monkeypatch.setattr(
sync_service, "_build_adapter_for_account", lambda session, acct: (provider, fake)
)
results = verification_service.verify_assets(s, [asset])
s.refresh(asset)
snapshot = (asset.expiry_date, asset.status, asset.external_id)
result = results[asset.id]
assert result.verified is True
assert result.source == "TotHost 面板"
assert snapshot == (date(2027, 1, 7), AssetStatus.ACTIVE, "36979")
def test_whmcs_missing_service_marks_cancelled(engine, monkeypatch):
with Session(engine) as s:
provider = _seed_provider(s)
account = _seed_account(s, provider)
asset = _seed_asset(s, provider, account, external_id="99999")
monkeypatch.setattr(
sync_service, "_build_adapter_for_account",
lambda session, acct: (provider, FakeWHMCS([_service()])),
)
results = verification_service.verify_assets(s, [asset])
s.refresh(asset)
assert results[asset.id].verified is True
assert results[asset.id].status == AssetStatus.CANCELLED
assert asset.status == AssetStatus.CANCELLED
assert "已无该服务" in results[asset.id].detail
def test_whmcs_without_account_is_unverified(engine, monkeypatch):
with Session(engine) as s:
provider = _seed_provider(s)
asset = _seed_asset(s, provider, None)
results = verification_service.verify_assets(s, [asset])
assert results[asset.id].verified is False
assert "未关联账号" in results[asset.id].detail
def test_whmcs_ambiguous_unlinked_asset_is_unverified(engine, monkeypatch):
"""同账号有两个未关联资产、面板只有一个在用服务时不做猜测"""
with Session(engine) as s:
provider = _seed_provider(s)
account = _seed_account(s, provider)
a1 = _seed_asset(s, provider, account, name="vps-a", ip="1.1.1.1")
a2 = _seed_asset(s, provider, account, name="vps-b", ip="2.2.2.2")
monkeypatch.setattr(
sync_service, "_build_adapter_for_account",
lambda session, acct: (provider, FakeWHMCS([_service()])),
)
results = verification_service.verify_assets(s, [a1, a2])
assert results[a1.id].verified is False
assert results[a2.id].verified is False
assert "无法与面板服务对应" in results[a1.id].detail
class FakeAPI:
def __init__(self, vps):
self._vps = vps
def capabilities(self):
return {"list_vps": True, "list_domains": False}
def list_vps(self):
return self._vps
def test_api_updates_expiry_and_status(engine, monkeypatch):
with Session(engine) as s:
provider = _seed_provider(s, sdk_type="vultr-api")
account = _seed_account(s, provider)
asset = _seed_asset(s, provider, account, external_id="abc")
fake = FakeAPI([NormalizedVPS(
external_id="abc", name="vps", ip_address="103.70.115.57",
status="active", expiry_date=date(2027, 5, 1),
)])
monkeypatch.setattr(
sync_service, "_build_adapter_for_account", lambda session, acct: (provider, fake)
)
results = verification_service.verify_assets(s, [asset])
s.refresh(asset)
snapshot = (asset.expiry_date, asset.status)
assert results[asset.id].verified is True
assert results[asset.id].source == "TotHost API"
assert snapshot == (date(2027, 5, 1), AssetStatus.ACTIVE)
def test_unsupported_provider_is_unverified(engine):
with Session(engine) as s:
provider = _seed_provider(s, sdk_type=None)
account = _seed_account(s, provider)
asset = _seed_asset(s, provider, account)
results = verification_service.verify_assets(s, [asset])
assert results[asset.id].verified is False
assert "未接入自动核对" in results[asset.id].detail