"""WHMCS 面板适配器测试:页面解析与状态映射
使用真实客户区结构的 HTML 片段,不发起网络请求。
"""
from datetime import date
import pytest
from app.adapters.whmcs import WHMCSAdapter, WHMCSError
SERVICES_HTML = """
產品 / 服務 價格 下次到期日 狀態
TOT K11 remote-dev
226,950VND 季繳
2027-01-07 星期四, 一月 7第, 2027
可用
TOT K11 vpn.host
$3.03 月繳
2026-11-25 星期三, 十一月 25第, 2026
可用
TOT D12 123.31.29.104
275,000VND 月繳
2026-06-11 星期四, 六月 11第, 2026
已刪除
TOT K11 paused.host
$3.03 月繳
2026-12-01 星期二, 十二月 1第, 2026
已暫停
"""
def _adapter():
return WHMCSAdapter({
"base_url": "https://billing.example.com/clientarea.php",
"username": "user@example.com",
"password": "secret",
})
def test_parse_services_extracts_all_fields():
services = WHMCSAdapter.parse_services(SERVICES_HTML)
assert [s["id"] for s in services] == ["36979", "28911", "36980", "36981"]
first = services[0]
assert first["product"] == "TOT K11"
assert first["domain"] == "remote-dev"
assert first["label"] == "TOT K11 remote-dev"
assert first["expiry_date"] == date(2027, 1, 7)
assert first["status"] == "active"
assert first["status_text"] == "可用"
assert first["amount"] == 226950.0
assert first["cycle"] == "季繳"
def test_parse_services_maps_whmcs_status_classes():
services = {s["id"]: s for s in WHMCSAdapter.parse_services(SERVICES_HTML)}
assert services["36979"]["status"] == "active"
assert services["36980"]["status"] == "cancelled"
assert services["36981"]["status"] == "stopped"
def test_parse_services_requires_service_table():
with pytest.raises(WHMCSError):
WHMCSAdapter.parse_services("no table")
def test_list_vps_filters_terminated_and_keeps_ip(monkeypatch):
adapter = _adapter()
services = WHMCSAdapter.parse_services(SERVICES_HTML)
monkeypatch.setattr(adapter, "fetch_services", lambda include_terminated=True: [
s for s in services if include_terminated or s["status"] != "cancelled"
])
vps = adapter.list_vps()
assert [v.external_id for v in vps] == ["36979", "28911", "36981"]
by_id = {v.external_id: v for v in vps}
assert by_id["36979"].name == "TOT K11 remote-dev"
assert by_id["36979"].ip_address == "" # 主机名不是 IP
assert by_id["36979"].expiry_date == date(2027, 1, 7)
assert "36980" not in by_id # 已删除项不出现在 list_vps
def test_extract_token_supports_attribute_orders():
assert WHMCSAdapter._extract_token(' ') == "abc"
assert WHMCSAdapter._extract_token(' ') == "xyz"
def test_extract_token_prefers_login_form():
"""页面上有多个带 token 的表单时,必须取登录表单里的那个"""
page = """
"""
assert WHMCSAdapter._extract_token(page) == "right"
@pytest.mark.parametrize("body", [
'Logout ',
'退出登录 ',
'登出 ',
'ログアウト ',
])
def test_logged_in_markers_cover_localized_themes(body):
"""主题本地化后不再输出英文 Logout,误判会让核对永远失败"""
from app.adapters.whmcs import _LOGGED_IN_RE
assert _LOGGED_IN_RE.search(body)
def test_logged_in_marker_ignores_plain_text():
from app.adapters.whmcs import _LOGGED_IN_RE
assert not _LOGGED_IN_RE.search("请勿退出页面
")
def test_adapter_requires_base_url():
with pytest.raises(WHMCSError):
WHMCSAdapter({"username": "u", "password": "p"})