"""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