Files
live-hub-py/tests/test_huya_elite_query.py

865 lines
30 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""虎牙精英宝典开通状态与「一键查询」(2026-10-15 真机实测)。
判定来源与真机结论:
* 宝典开通状态 = 积分中心 ``shopMiddleUI.getRedeemNeedGoods`` 的 ``tag5``。
下面固化的两段响应字节分别来自已开通账号(hy_302016799)与未开通账号
(hy_260034554),与网页显示的「已开通精英宝典 / 获取积分需先开通精英宝典」
一致。
* 开通时间 = ``revenueWebUI.queryUserOrderList`` 里已支付的宝典订单。
"""
import json
from types import SimpleNamespace
from core.huya.frame_decoder import decode_struct_bytes
from core.huya.http_client import HuyaHttpClient
from core.huya.shop_structs import ShopRawRsp
from web.backend.models import HuyaAccount
from web.backend.services.huya_pay_link import PayTargets, pay_qr_path
from web.backend.services.huya_runner import HuyaBatchRunner
UID = 1199668595203
COOKIE = f"yyuid={UID}; udb_uid={UID}"
# 真机 getRedeemNeedGoods 响应:已开通(tag5=1、tag6=1、tag7 为空)
REDEEM_OPENED_HEX = "0a0100c8160ce69fa5e8afa2e68890e58a9f21011830144c50016001790c0b"
# 真机 getRedeemNeedGoods 响应:未开通(tag5=0、tag6=0、tag7 列出开通套餐)
REDEEM_NOT_OPENED_HEX = (
"0a0100c8160ce69fa5e8afa2e68890e58a9f2c301440145c6c7900040a02007f134f1200"
"765c0b260ce7b2bee88bb1e5ae9de585b830054101fb51012c610bb87c9001a90cb90cca010"
"bb8110bb82c310bb8490c5c610bb8790c8c9cacbcccdc0bf00f010b0a02007ede691200766"
"17f2616e5bc80e9809ae5ae9de585b82be7b2bee88bb1e5b88130054101fc51012d610bc2"
"70018900020a02007ede6912007f134f2200765c0b310bb841012c5001660ce7b2bee88bb1"
"e5ae9de585b87c0b0a02007ede6912007f10bc220076662f300a400150016609e7b2bee88b"
"b1e5b88170010b9ca90cb90cca010bc2110bc22c310bc2490c5c610bc2790c8c9cacbcccdc0b"
"fc0f0b"
)
# 真机订单列表里已支付的宝典订单(pay_time 与 spu 均来自真机响应)
PAID_ELITE_ORDER = {
"item_name": "精英宝典",
"order_status": 60,
"pay_time": 1791895396000,
"goods_detail": {"spu_id": "hy-8328015", "sku_id": 7756811},
}
UNPAID_ELITE_ORDER = {
"item_name": "精英宝典",
"order_status": 10,
"pay_time": 0,
"goods_detail": {"spu_id": "hy-8328015", "sku_id": 7756811},
}
OTHER_ORDER = {
"item_name": "电竞徽章",
"order_status": 60,
"pay_time": 1788000000000,
"goods_detail": {"spu_id": "hy-7000000", "sku_id": 1},
}
# 真机 queryAllocation 响应里的赛季节点(精英宝典福利季丨Q4:2026-10-09 ~ 2026-12-31)
ALLOCATION_SEASON_PAYLOAD = {
"tag0": {"tag0": 200, "tag1": "成功", "tag2": ""},
"tag1": {
"tag0": "和平精英宝典积分中心",
"tag1": "heping_baodian_credit_center_page",
},
"tag2": [
{"tag0": 343, "tag4": 6},
{
"tag0": 1,
"tag2_2": {
"tag0": {
"tag0": 15,
"tag1": "精英宝典福利季",
"tag2": "精英宝典福利季丨Q4",
"tag3": 3203,
"tag5": 17096,
"tag6": 9815,
"tag10": 1791475200000,
"tag11": 1798732799000,
},
"tag4": {"tag0": 300},
},
},
],
}
def _redeem(hex_text: str) -> ShopRawRsp:
return ShopRawRsp.from_payload(decode_struct_bytes(bytes.fromhex(hex_text)))
class _FakeOrder:
def __init__(self, payload: dict):
self._payload = payload
def to_dict(self) -> dict:
return dict(self._payload)
class _FakeOrderList:
def __init__(self, orders: list[dict], total_count: int | None = None):
self.orders = [_FakeOrder(item) for item in orders]
self.totalCount = len(orders) if total_count is None else total_count
class _FakeBindStatus:
"""只喂给 EliteMixin 用到的那几个字段(角色解析由 BindMixin 自己的用例覆盖)。"""
def __init__(self, status: int = 200, msg: str = ""):
self.status = status
self.msg = msg
def to_dict(self) -> dict:
return {"status": self.status, "msg": self.msg}
class _FakeGameAccount:
def __init__(self, nick: str):
self.nick = nick
def to_dict(self) -> dict:
return {"nick": self.nick}
class _FakeGameRole:
def __init__(self, role_name: str, plat_name: str = "", area_name: str = ""):
self.roleName = role_name
self.platName = plat_name
self.areaName = area_name
def to_dict(self) -> dict:
return {
"role_name": self.roleName,
"plat_name": self.platName,
"area_name": self.areaName,
}
class _FakeBindAccountData:
def __init__(self, nick: str, role_name: str):
self.gameAccount = _FakeGameAccount(nick)
self.gameRole = _FakeGameRole(role_name, "苹果(iOS)", "手Q")
self.isBindAcount = 1
self.isBindRole = 1
self.isNeedActCheck = 0
self.changBindTime = 0
class _FakeBindResponse:
def __init__(
self,
*,
status: int = 200,
msg: str = "",
nick: str = "微信用户",
role_name: str = "吃鸡199706",
):
self.status = status
self.msg = msg
self.gameName = "和平精英"
self.changeBindDay = 7
self.accountData = _FakeBindAccountData(nick, role_name)
def to_dict(self) -> dict:
return {
"status": self.status,
"msg": self.msg,
"game_name": self.gameName,
"account_data": self.accountData.gameAccount.to_dict(),
"game_role": self.accountData.gameRole.to_dict(),
}
class _CkProbeClient:
def __init__(self, response):
self.response = response
self.calls: list[dict] = []
def check_user_bind_game_account(self, **kwargs):
self.calls.append(kwargs)
return self.response
def _query_game_runner(monkeypatch, client):
runner = HuyaBatchRunner(
db=None, batch_id="test-batch", task_type="query_game_name"
)
marks: list[tuple[str, str, dict | None]] = []
monkeypatch.setattr(runner, "_http_client", lambda: client, raising=False)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
raising=False,
)
return runner, marks
class _StubClient:
"""积分中心通道的假客户端:可返回宝典状态/订单列表/赛季配置,并记录调用。"""
def __init__(self, redeem=None, order_pages=None, season: bool = False):
self.redeem = redeem
self.order_pages = order_pages or []
self.season_payload = ALLOCATION_SEASON_PAYLOAD if season else None
self.calls: list[tuple[str, dict]] = []
self.redeem_calls = 0
def get_redeem_need_goods(self, **kwargs):
self.calls.append(("get_redeem_need_goods", kwargs))
self.redeem_calls += 1
return self.redeem
def play_mall_query_allocation(self, **kwargs):
self.calls.append(("play_mall_query_allocation", kwargs))
if self.season_payload is None:
return None
return ShopRawRsp.from_payload(self.season_payload)
def query_user_order_list(self, **kwargs):
self.calls.append(("query_user_order_list", kwargs))
offset = int(kwargs.get("offset") or 0)
index = offset // 20
if index >= len(self.order_pages):
return None
page = self.order_pages[index]
return _FakeOrderList(page, total_count=sum(len(p) for p in self.order_pages))
def _runner(monkeypatch, shop_client, activity_client=None):
runner = HuyaBatchRunner(db=None, batch_id="test-batch", task_type="query_all")
marks: list[tuple[str, str, dict | None]] = []
logs: list[tuple[str, str]] = []
monkeypatch.setattr(runner, "_http_client", lambda: shop_client, raising=False)
if activity_client is not None:
monkeypatch.setattr(
runner,
"_activity_client",
lambda uid, cookie: activity_client,
raising=False,
)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
raising=False,
)
monkeypatch.setattr(
runner,
"_push_log",
lambda level, message: logs.append((level, message)),
raising=False,
)
return runner, marks, logs
def test_real_redeem_bytes_report_elite_opened():
runner = HuyaBatchRunner(db=None, batch_id="t", task_type="query_all")
assert runner._elite_opened_from_redeem(_redeem(REDEEM_OPENED_HEX)) is True
assert runner._elite_opened_from_redeem(_redeem(REDEEM_NOT_OPENED_HEX)) is False
# 不是 200 或没有 tag5 时不下结论(界面显示「未查询」而不是猜一个值)
assert runner._elite_opened_from_redeem(None) is None
assert (
runner._elite_opened_from_redeem(ShopRawRsp.from_payload({"tag0": 500})) is None
)
def test_elite_open_time_uses_latest_paid_handbook_order():
client = _StubClient(
order_pages=[[OTHER_ORDER, UNPAID_ELITE_ORDER, PAID_ELITE_ORDER]]
)
runner = HuyaBatchRunner(db=None, batch_id="t", task_type="query_all")
assert runner._resolve_elite_open_time(client, UID, COOKIE) == 1791895396000
def test_elite_open_time_ignores_unpaid_handbook_order():
"""未支付的宝典订单不算开通时间(真机:order_status=10、pay_time=0)。"""
client = _StubClient(order_pages=[[UNPAID_ELITE_ORDER]])
runner = HuyaBatchRunner(db=None, batch_id="t", task_type="query_all")
assert runner._resolve_elite_open_time(client, UID, COOKIE) is None
def test_elite_open_time_scans_following_pages():
later_order = {**PAID_ELITE_ORDER, "pay_time": 1792000000000}
client = _StubClient(order_pages=[[OTHER_ORDER] * 20, [later_order]])
runner = HuyaBatchRunner(db=None, batch_id="t", task_type="query_all")
assert runner._resolve_elite_open_time(client, UID, COOKIE) == 1792000000000
offsets = [
kwargs.get("offset")
for name, kwargs in client.calls
if name == "query_user_order_list"
]
assert offsets == [0, 20]
def test_extract_elite_season_from_allocation_payload():
runner = HuyaBatchRunner(db=None, batch_id="t", task_type="query_all")
season = runner._extract_elite_season(ALLOCATION_SEASON_PAYLOAD)
assert season == {
"name": "精英宝典福利季丨Q4",
"start_ms": 1791475200000,
"end_ms": 1798732799000,
"config_id": 15,
}
# 认不出赛季结构时不下结论(界面就不显示有效期)
assert runner._extract_elite_season({"tag0": {"tag1": "x"}}) is None
assert runner._extract_elite_season(None) is None
def test_query_all_fills_expire_time_from_season(monkeypatch):
shop_client = _StubClient(
redeem=_redeem(REDEEM_OPENED_HEX),
order_pages=[[PAID_ELITE_ORDER]],
season=True,
)
runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object())
monkeypatch.setattr(
runner,
"_probe_bind_status",
lambda **kwargs: (_FakeBindStatus(status=601, msg="活动已下线"), {}),
raising=False,
)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_all(
None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
)
status, message, result = marks[-1]
assert status == "success"
assert result["elite_expire_time"] == 1798732799
assert result["elite_season"]["name"] == "精英宝典福利季丨Q4"
assert account.elite_expire_time == 1798732799
assert "有效期至 2026-12-31" in message
def test_refresh_elite_open_after_payment_retries_until_opened():
"""刚支付完 tag5 可能还是 0(交付延迟)→ 重试到已开通,并写回账号。"""
class _FlakyClient(_StubClient):
def get_redeem_need_goods(self, **kwargs):
result = super().get_redeem_need_goods(**kwargs)
if self.redeem_calls > 1:
return _redeem(REDEEM_OPENED_HEX)
return result
client = _FlakyClient(season=True)
runner = HuyaBatchRunner(db=None, batch_id="t", task_type="create_recharge_order")
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
state = runner._refresh_elite_open_after_payment(
client,
account,
UID,
COOKIE,
goods_num=8315982,
pay_time_ms=1792053457000,
attempts=3,
interval=0,
)
assert client.redeem_calls == 2 # 第一次未开通 → 重试一次即成功
assert state["elite_opened"] is True
assert state["elite_open_time"] == 1792053457
assert state["elite_expire_time"] == 1798732799
assert state["redeem_points"] == 280
assert account.elite_opened is True
assert account.elite_open_time == 1792053457
assert account.elite_expire_time == 1798732799
def test_refresh_elite_open_after_payment_gives_up_after_attempts():
client = _StubClient(redeem=_redeem(REDEEM_NOT_OPENED_HEX))
runner = HuyaBatchRunner(db=None, batch_id="t", task_type="create_recharge_order")
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
state = runner._refresh_elite_open_after_payment(
client,
account,
UID,
COOKIE,
goods_num=8315982,
pay_time_ms=1792053457000,
attempts=2,
interval=0,
)
assert client.redeem_calls == 2 # 重试到上限
assert state["elite_opened"] is False
assert account.elite_opened is False
assert account.elite_open_time is None
def test_create_recharge_order_confirms_elite_open_after_payment(db, monkeypatch):
"""支付成功后自动确认开通状态:任务结果与账号字段都带上宝典状态。"""
outcome = SimpleNamespace(
ok=True,
message="",
detail={"spu_id": "hy-8328015", "sku_id": 7756811, "name": "精英宝典"},
sku_id=7756811,
product_name="精英宝典",
unit_price=3000,
order_raw={"order_id": 11399999},
pay_raw={"pay_order_id": "pay-1"},
amount=3000,
order_id=11399999,
app_order_id="app-1",
pay_order_id="pay-1",
pay_url="https://example.com/pay",
supplier_name="和平精英官方小店",
promotion={},
promotion_info="本单无返利",
reused_order={},
)
shop_client = _StubClient(redeem=_redeem(REDEEM_OPENED_HEX), season=True)
runner = HuyaBatchRunner(
db=None,
batch_id="t",
task_type="create_recharge_order",
payload={"spu_id": "hy-8328015", "sku_id": 7756811, "count": 1},
)
marks: list[tuple[str, str, dict | None]] = []
monkeypatch.setattr(
runner, "_shop_client", lambda uid, cookie: shop_client, raising=False
)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
raising=False,
)
monkeypatch.setattr(
runner, "_update_task_progress", lambda *args, **kwargs: None, raising=False
)
monkeypatch.setattr(
runner, "_push_log", lambda *args, **kwargs: None, raising=False
)
def fake_wait_payment(**kwargs):
# 真实现会边轮询边写 result,这里照抄「已支付」那几个字段
target = kwargs.get("result")
if isinstance(target, dict):
target.update(
{
"payment_status": "paid",
"payment_status_label": "已支付",
"payment_paid": True,
}
)
return "paid", {"pay_time": 1792053457000}
monkeypatch.setattr(
runner, "_wait_recharge_payment", fake_wait_payment, raising=False
)
monkeypatch.setattr(
"web.backend.services.huya_runner_recharge.run_open_flow",
lambda *args, **kwargs: outcome,
)
# 二维码内容优先用收银台页面里的扫码内容(真机:支付宝扫码支付码)
monkeypatch.setattr(
"web.backend.services.huya_runner_recharge.resolve_pay_targets",
lambda _pay_url, **_kwargs: PayTargets(
cashier_url="https://excashier.alipay.com/standard/auth.htm?payOrderId=x.25",
qr_code="https://qr.alipay.com/upx03110ghym9vm37sx7255c",
),
)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
db.add(account)
db.commit()
runner._execute_create_recharge_order(
db,
SimpleNamespace(id=587),
account,
{"uid": UID, "cookie": COOKIE},
{"room_pid": "1199650619883", "pay_channel": "Zfb", "sid": "2203"},
)
status, _message, result = marks[-1]
assert status == "success"
assert result["payment_paid"] is True
assert result["elite_opened"] is True
assert result["elite_open_time"] == 1792053457
assert result["elite_expire_time"] == 1798732799
assert result["post_payment_points"] == 280
# 二维码内容给前端两条短地址(原始支付地址 1626 字符,扫码扫不动)
assert result["pay_qr_path"] == pay_qr_path(587)
# 优先用收银台页面里的扫码内容(支付宝 App 扫码即付款)
assert result["pay_qr_code"] == "https://qr.alipay.com/upx03110ghym9vm37sx7255c"
# 收银台直达地址作为兜底保留(PC 打开就是这个页面)
assert result["pay_cashier_url"].startswith("https://excashier.alipay.com/")
assert account.elite_opened is True
assert account.elite_expire_time == 1798732799
assert account.points == 280
assert account.status == "recharge_paid"
def test_create_recharge_order_uses_requested_count(db, monkeypatch):
"""开通数量:payload.count 按份数下单(前端「数量」输入框),结果与文案都带 xN。"""
captured: dict = {}
outcome = SimpleNamespace(
ok=True,
message="",
detail={"spu_id": "hy-5872824", "sku_id": 7756812, "name": "精英徽章"},
sku_id=7756812,
product_name="精英徽章",
unit_price=100,
order_raw={"order_id": 11399998},
pay_raw={"pay_order_id": "pay-2"},
amount=300,
order_id=11399998,
app_order_id="app-2",
pay_order_id="pay-2",
pay_url="https://example.com/pay",
supplier_name="和平精英官方小店",
promotion={},
promotion_info="",
reused_order={},
)
def fake_open_flow(_client, **kwargs):
captured.update(kwargs)
return outcome
shop_client = _StubClient(redeem=_redeem(REDEEM_OPENED_HEX))
runner = HuyaBatchRunner(
db=None,
batch_id="t",
task_type="create_recharge_order",
payload={
"spu_id": "hy-5872824",
"sku_id": 7756812,
"product_name": "精英徽章",
"count": 3,
},
)
marks: list[tuple[str, str, dict | None]] = []
monkeypatch.setattr(
runner, "_shop_client", lambda uid, cookie: shop_client, raising=False
)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
raising=False,
)
monkeypatch.setattr(
runner, "_update_task_progress", lambda *args, **kwargs: None, raising=False
)
monkeypatch.setattr(
runner, "_push_log", lambda *args, **kwargs: None, raising=False
)
# 不真等支付:这一步只验证数量有没有进下单请求
monkeypatch.setattr(
runner,
"_wait_recharge_payment",
lambda **kwargs: ("timeout", None),
raising=False,
)
monkeypatch.setattr(
"web.backend.services.huya_runner_recharge.run_open_flow", fake_open_flow
)
monkeypatch.setattr(
"web.backend.services.huya_runner_recharge.resolve_pay_targets",
lambda _pay_url, **_kwargs: PayTargets(
cashier_url="", qr_code="https://qr.alipay.com/upx03110ghym9vm37sx7255c"
),
)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
db.add(account)
db.commit()
runner._execute_create_recharge_order(
db,
SimpleNamespace(id=588),
account,
{"uid": UID, "cookie": COOKIE},
{"room_pid": "1199650619883", "pay_channel": "Zfb", "sid": "2203"},
)
assert captured["count"] == 3
_status, message, result = marks[-1]
assert result["count"] == 3
assert result["amount"] == 300
assert "x3" in message
assert account.status == "recharge_order_created"
def test_query_all_combines_points_elite_and_role(monkeypatch):
shop_client = _StubClient(
redeem=_redeem(REDEEM_OPENED_HEX), order_pages=[[PAID_ELITE_ORDER]]
)
runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object())
monkeypatch.setattr(
runner,
"_probe_bind_status",
lambda **kwargs: (_FakeBindStatus(), {"bind_status_source": "outer"}),
raising=False,
)
monkeypatch.setattr(
runner,
"_bind_role_result",
staticmethod(
lambda _status: {"role_name": "软踏日潺湲", "game_title": "和平精英"}
),
raising=False,
)
monkeypatch.setattr(
runner,
"_bind_change_state",
staticmethod(lambda _status: {"is_bound": True}),
raising=False,
)
monkeypatch.setattr(
runner,
"_role_channel",
staticmethod(lambda _status: "苹果(iOS) / 手Q"),
raising=False,
)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_all(
None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
)
status, message, result = marks[-1]
assert status == "success"
assert result["points"] == 280
assert result["elite_opened"] is True
assert result["elite_open_time"] == 1791895396
assert result["role_name"] == "软踏日潺湲"
assert result["failures"] == []
assert account.points == 280
assert account.elite_opened is True
assert account.elite_open_time == 1791895396
assert account.game_name == "软踏日潺湲"
assert account.game_channel == "苹果(iOS) / 手Q"
assert account.status == "all_queried"
assert (
"积分 280" in message
and "已开通宝典" in message
and "角色 软踏日潺湲" in message
)
def test_query_all_reports_not_opened_without_order_lookup(monkeypatch):
shop_client = _StubClient(redeem=_redeem(REDEEM_NOT_OPENED_HEX))
runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object())
monkeypatch.setattr(
runner,
"_probe_bind_status",
lambda **kwargs: (_FakeBindStatus(status=601, msg="活动已下线"), {}),
raising=False,
)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_all(
None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
)
status, message, result = marks[-1]
# 角色接口业务失败不影响宝典/积分结论
assert status == "success"
assert result["elite_opened"] is False
assert account.elite_opened is False
assert account.elite_open_time is None
assert "未开通宝典" in message
assert "活动已下线" in message
# 未开通不查订单(省一次请求)
assert [name for name, _kwargs in shop_client.calls] == ["get_redeem_need_goods"]
def test_query_all_fails_when_points_center_unreachable(monkeypatch):
shop_client = _StubClient(redeem=None)
runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object())
monkeypatch.setattr(
runner,
"_probe_bind_status",
lambda **kwargs: (_FakeBindStatus(status=601, msg="活动已下线"), {}),
raising=False,
)
account = HuyaAccount(uid=str(UID), cookie=COOKIE, status="imported")
runner._execute_query_all(
None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
)
status, message, result = marks[-1]
assert status == "failed"
assert "积分中心接口无响应" in message
assert result.get("ck_expired") is None
# 没有查到任何东西时不能把账号标成 all_queried
assert account.status == "imported"
def test_query_all_flags_ck_expired_when_role_lookup_has_no_response(monkeypatch):
"""角色接口整包没有回包 = 这个 CK 已经不认了(真机:外部导入的过期 CK 就这表现)。
真机日志(logs/app-2026-10-15.log,hy_297838517)里
``webActUI.checkUserBindGameAccount`` 的响应只有 ``newdata keys: ['']``(没有 tRsp),
``_probe_bind_status`` 因此返回 None;积分中心走另一条通道却还能返回 200,
所以不能凭「积分查到了」就当成功,必须打 ck_expired 让页面标红。
2026-10-17 起一键查询也改成先验 CK:判定失效后立即停止,
不再白跑积分中心和宝典接口。
"""
shop_client = _StubClient(redeem=_redeem(REDEEM_NOT_OPENED_HEX))
runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object())
monkeypatch.setattr(
runner, "_probe_bind_status", lambda **kwargs: (None, {}), raising=False
)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_all(
None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
)
status, message, result = marks[-1]
assert status == "error"
assert result["ck_expired"] is True
assert result["ck_expired_reason"] == "角色信息接口无响应"
assert account.status == "ck_expired"
assert "CK 已失效" in message
assert shop_client.calls == []
def test_query_all_keeps_business_error_out_of_ck_expired(monkeypatch):
"""接口有回包、只是业务码非 200(例如活动已下线)不算 CK 失效,避免整批误标红。"""
shop_client = _StubClient(redeem=_redeem(REDEEM_NOT_OPENED_HEX))
runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object())
monkeypatch.setattr(
runner,
"_probe_bind_status",
lambda **kwargs: (_FakeBindStatus(status=601, msg="活动已下线"), {}),
raising=False,
)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_all(
None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
)
_status, message, result = marks[-1]
assert result.get("ck_expired") is None
assert account.status == "all_queried"
assert "活动已下线" in message
def test_query_game_name_uses_new_probe_params_and_saves_role(monkeypatch):
"""查询角色必须复用外部单协作的新探针:HTTP + outer_act_id + 15 秒超时。"""
client = _CkProbeClient(_FakeBindResponse())
runner, marks = _query_game_runner(monkeypatch, client)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_game_name(
None,
None,
account,
{"uid": UID, "cookie": COOKIE},
{"bind_act_id": "9271", "outer_act_id": "17096"},
)
status, message, result = marks[-1]
assert status == "success"
assert message == "角色: 吃鸡199706"
assert result["role_name"] == "吃鸡199706"
assert result["game_title"] == "和平精英"
assert account.status == "game_queried"
assert account.game_name == "吃鸡199706"
assert account.game_channel == "苹果(iOS) / 手Q"
assert client.calls == [
{
"uid": UID,
"cookie": COOKIE,
"b_act_id": 9271,
"is_use_outer_act_id": 1,
"outer_act_id": "17096",
"timeout": 15.0,
}
]
def test_query_game_name_marks_ck_expired_when_probe_has_no_response(monkeypatch):
client = _CkProbeClient(None)
runner, marks = _query_game_runner(monkeypatch, client)
account = HuyaAccount(uid=str(UID), cookie=COOKIE, status="imported")
runner._execute_query_game_name(
None,
None,
account,
{"uid": UID, "cookie": COOKIE},
{"bind_act_id": "9271", "outer_act_id": "17096"},
)
status, message, result = marks[-1]
assert status == "error"
assert "CK 已失效" in message
assert result["ck_expired"] is True
assert result["ck_expired_reason"] == "角色信息接口无响应"
assert account.status == "ck_expired"
def test_query_game_name_does_not_mark_business_error_as_ck_expired(monkeypatch):
client = _CkProbeClient(_FakeBindResponse(status=601, msg="活动已下线"))
runner, marks = _query_game_runner(monkeypatch, client)
account = HuyaAccount(uid=str(UID), cookie=COOKIE, status="imported")
runner._execute_query_game_name(
None,
None,
account,
{"uid": UID, "cookie": COOKIE},
{"bind_act_id": "9271", "outer_act_id": "17096"},
)
status, message, result = marks[-1]
assert status == "failed"
assert message == "活动已下线"
assert result.get("ck_expired") is None
assert account.status == "imported"
def test_shop_client_falls_back_to_http_when_wss_init_fails(monkeypatch):
"""设备态齐全但积分中心端点不回包时(wsLaunch 超时)回退 HTTP,而不是判整批失败。"""
cookie = (
"udb_cred=cred; udb_biztoken=biz; udb_uid=1199659425268; yyuid=1199659425268; "
"sdid=sdid; udb_deviceid=w_1234567890123456789; udb_guiddata=guiddata; "
"udb_anobiztoken=anobiz"
)
logs: list[tuple[str, str]] = []
runner = HuyaBatchRunner(db=None, batch_id="t", task_type="query_all")
def boom(uid: int, cookie_text: str, kind: str):
raise RuntimeError(f"虎牙 {kind} WSS 初始化失败")
monkeypatch.setattr(runner, "_open_wss_session", boom, raising=False)
monkeypatch.setattr(
runner,
"_push_log",
lambda level, message: logs.append((level, message)),
raising=False,
)
client = runner._shop_client(1199659425268, cookie)
assert isinstance(client, HuyaHttpClient)
assert logs and logs[-1][0] == "warning"
assert "WSS 会话不可用" in logs[-1][1]
assert "sdid" not in json.dumps(logs[-1][1]) # 日志不带 Cookie 值