容错策略:核对失败/无可用通道时标记「未核对」并继续按本地数据提醒, 避免因核对链路故障漏掉真正到期的资产;已停止资产仅在 auto_renew=true 时提醒(停止实例不等于停止扣费)。新增 WHMCS 面板只读适配器, 用于 TotHost 这类没有开放 API 的平台。
208 lines
7.0 KiB
Python
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
|