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

349 lines
12 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 真机实测)。
背景:旧活动接口 ``webActUI.getUserScore`` 对任何 sid 都返回
603「活动已结束」(该接口已下线,与工作台里的 SID/活动配置无关);
积分中心用 ``shopMiddleUI.getRedeemNeedGoods`` 查积分:
``tag2``=当前积分、``tag3``=兑换门槛。下面固化的响应字节来自真机 HTTP 通道。
抓包/真机字面值直接写在测试里(不引用实现常量),避免断言跟着实现漂移。
"""
import asyncio
from types import SimpleNamespace
from core.huya.frame_decoder import _decode_wup_body, decode_struct_bytes
from core.huya.http_client import HuyaHttpClient
from core.huya.shop_structs import ShopRawRsp
from core.huya.wss_client import HuyaWssClient, WssMessage
from web.backend.models import HuyaAccount
from web.backend.services.huya_runner import HuyaBatchRunner
UID = 1199665170519
GUID = "0a8995af9218ce6a340261946294a9e7"
COOKIE = f"udb_uid={UID}; yyuid={UID}; guid={GUID}"
# 真机(2026-10-15,账号 hy_297832664)getRedeemNeedGoods 响应:200 / 积分 280 / 门槛 20
REDEEM_RSP_HEX = "0a0100c8160ce69fa5e8afa2e68890e58a9f21011830144c50016001790c0b"
# 真机 getGrowLevelUserInfo 响应:成长值 980 / 成长等级 3 / 昵称 / 头像
GROW_RSP_HEX = (
"0a0a0c1606e68890e58a9f0b1a0103d41003230000019f124082043c4c50"
"0360ff7101f48103e80b260fe8bdafe8b88fe697a5e6bdbae6b9b236386874"
"7470733a2f2f6469792d6173736574732e6d737374617469632e636f6d2f6371"
"68666f6c6465722f4e657744656661756c742e706e670b"
)
class _FakeWs:
"""记录发出的帧,并立刻兑现 RPC future(离线跑完整链路)。"""
def __init__(self, client: HuyaWssClient):
self.client = client
self.sent: list[bytes] = []
async def send(self, data: bytes):
self.sent.append(data)
while self.client.rpc_queue:
future = self.client.rpc_queue.popleft()
if not future.done():
future.set_result(b"")
def last_request(self) -> dict:
return _decode_wup_body(WssMessage.decode(self.sent[-1]).body)
def _points_client() -> HuyaWssClient:
client = HuyaWssClient(logger=lambda _message: None)
client._points_mode = True
client.ws = _FakeWs(client)
return client
def _raw_rsp(payload_hex: str) -> ShopRawRsp:
return ShopRawRsp.from_payload(
decode_struct_bytes(bytes.fromhex("".join(payload_hex.split())))
)
class _StubClient:
"""按需返回真机响应字节的积分中心客户端。"""
def __init__(self, redeem=True, grow=True):
self.redeem = _raw_rsp(REDEEM_RSP_HEX) if redeem else None
self.grow = _raw_rsp(GROW_RSP_HEX) if grow else None
self.calls: list[tuple[str, dict]] = []
def get_redeem_need_goods(self, **kwargs):
self.calls.append(("get_redeem_need_goods", kwargs))
return self.redeem
def get_grow_level_user_info(self, **kwargs):
self.calls.append(("get_grow_level_user_info", kwargs))
return self.grow
def _runner(
monkeypatch, client, *, ck_valid: bool = True
) -> tuple[HuyaBatchRunner, list]:
runner = HuyaBatchRunner(db=None, batch_id="test-batch", task_type="query_points")
marks: list[tuple[str, str, dict | None]] = []
monkeypatch.setattr(runner, "_http_client", lambda: client, raising=False)
probe_status = SimpleNamespace(status=200, msg="") if ck_valid else None
monkeypatch.setattr(
runner,
"_probe_bind_status",
lambda **_kwargs: (probe_status, {}),
raising=False,
)
monkeypatch.setattr(
runner,
"_mark_task",
lambda _db, _task, status, message, result=None: marks.append(
(status, message, result)
),
raising=False,
)
return runner, marks
def test_wss_redeem_need_goods_request_matches_real_call():
async def run():
client = _points_client()
await client.get_redeem_need_goods(uid=UID, guid=GUID, cookie=COOKIE)
message = WssMessage.decode(client.ws.sent[-1])
# 积分中心业务帧统一 0x1d000107(只有下单/支付用 0x1d000108)
assert message.sequence == 0x1D000107
decoded = client.ws.last_request()
assert (decoded["servant"], decoded["func"]) == (
"shopMiddleUI",
"getRedeemNeedGoods",
)
t_req = decoded["tReq"]
assert t_req["tag0"]["tag0"] == UID
assert t_req["tag1"]["tag0"] == "huya"
assert t_req["tag1"]["tag4"] == 7
assert t_req["tag1"]["tag5"] == "yellowcarlist"
# 默认兑换商品号必须是真机可用的 hy-8315982 的数字形式
assert t_req["tag2"] == 8315982
asyncio.run(run())
def test_wss_grow_level_request_carries_only_user_id():
async def run():
client = _points_client()
await client.get_grow_level_user_info(uid=UID, guid=GUID, cookie=COOKIE)
decoded = client.ws.last_request()
assert (decoded["servant"], decoded["func"]) == (
"GameBizUI",
"getGrowLevelUserInfo",
)
t_req = decoded["tReq"]
assert t_req["tag0"]["tag0"] == UID
# 抓包 tReq 只有 tag0,没有 shopAppInfo
assert set(t_req) == {"tag0"}
asyncio.run(run())
def test_http_redeem_need_goods_request_matches_real_call():
client = HuyaHttpClient(logger=lambda _message: None)
recorded: list[tuple] = []
def fake_call_rpc(
service, method, req_struct, rsp_class=None, uid=0, guid="", cookie="", **kwargs
):
recorded.append((service, method, req_struct))
client.call_rpc = fake_call_rpc # type: ignore[method-assign]
client.get_redeem_need_goods(uid=UID, guid="", cookie=COOKIE)
service, method, req = recorded[-1]
assert (service, method) == ("shopMiddleUI", "getRedeemNeedGoods")
assert req.userId.lUid == UID
assert req.shopAppInfo.scene == 7
assert req.shopAppInfo.sourceId == "yellowcarlist"
assert req.goodsNum == 8315982
def test_real_redeem_response_bytes_are_points_and_threshold():
rsp = _raw_rsp(REDEEM_RSP_HEX)
assert rsp.code == 200
assert rsp.message == "查询成功"
# tag2 = 当前积分,tag3 = 兑换门槛积分(真机 280 / 20)
assert rsp.value("tag2") == 280
assert rsp.value("tag3") == 20
def test_real_grow_level_response_bytes_are_level_and_nickname():
rsp = _raw_rsp(GROW_RSP_HEX)
assert rsp.code == 0
assert rsp.message == "成功"
assert rsp.value("tag1", "tag0") == 980 # 成长值
assert rsp.value("tag1", "tag1") == 3 # 成长等级
assert rsp.value("tag2") == "软踏日潺湲"
assert str(rsp.value("tag3")).startswith("https://diy-assets.msstatic.com/")
def test_query_points_reads_points_from_points_center(monkeypatch):
client = _StubClient()
runner, marks = _runner(monkeypatch, client)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_points(
None, None, account, {"uid": UID, "cookie": COOKIE}, {}
)
assert [name for name, _kwargs in client.calls] == [
"get_redeem_need_goods",
"get_grow_level_user_info",
]
assert client.calls[0][1]["goods_num"] == 8315982
status, message, result = marks[-1]
assert status == "success"
assert message == "积分: 280"
assert account.points == 280
assert result["points"] == 280
assert result["redeem_threshold"] == 20
assert result["grow_level"] == 3
assert result["grow_value"] == 980
assert result["nickname"] == "软踏日潺湲"
def test_query_points_uses_http_and_prechecks_ck_with_new_probe(monkeypatch):
client = _StubClient()
runner, marks = _runner(monkeypatch, client)
probe_calls: list[dict] = []
monkeypatch.setattr(
runner,
"_probe_bind_status",
lambda **kwargs: (
probe_calls.append(kwargs) or SimpleNamespace(status=200, msg=""),
{},
),
raising=False,
)
monkeypatch.setattr(
runner,
"_shop_client",
lambda *_args, **_kwargs: (_ for _ in ()).throw(
AssertionError("查询积分不应再建 WSS 商城会话")
),
raising=False,
)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_points(
None,
None,
account,
{"uid": UID, "cookie": COOKIE},
{"bind_act_id": "9271", "outer_act_id": "17096"},
)
assert probe_calls == [
{
"uid": UID,
"cookie": COOKIE,
"b_act_id_int": 9271,
"outer_act_id": "17096",
}
]
assert marks[-1][0] == "success"
assert [name for name, _kwargs in client.calls] == [
"get_redeem_need_goods",
"get_grow_level_user_info",
]
def test_query_points_keeps_working_when_grow_level_fails(monkeypatch):
client = _StubClient(grow=False)
runner, marks = _runner(monkeypatch, client)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_points(
None, None, account, {"uid": UID, "cookie": COOKIE}, {}
)
status, message, result = marks[-1]
assert status == "success"
assert message == "积分: 280"
assert "grow_level" not in result
def test_query_points_reports_server_error_of_points_center(monkeypatch):
class _FailingClient(_StubClient):
def get_redeem_need_goods(self, **kwargs):
self.calls.append(("get_redeem_need_goods", kwargs))
return ShopRawRsp.from_payload(
decode_struct_bytes(
bytes.fromhex(
"0a0101f4161ee7b3bbe7bb9fe5bc82e5b8b8efbc8ce8afb7e7a88de5"
"908ee9878de8af952c3c4c5c6c0b"
)
)
)
client = _FailingClient()
runner, marks = _runner(monkeypatch, client)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_points(
None, None, account, {"uid": UID, "cookie": COOKIE}, {}
)
status, message, result = marks[-1]
assert status == "failed"
assert message == "系统异常,请稍后重试"
assert result["code"] == 500
assert result["goods_num"] == 8315982
def test_query_points_reports_points_center_offline_without_legacy_fallback(
monkeypatch,
):
client = _StubClient(redeem=False)
runner, marks = _runner(monkeypatch, client)
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
runner._execute_query_points(
None,
None,
account,
{"uid": UID, "cookie": COOKIE},
{"bind_act_id": "9271", "outer_act_id": "17096"},
)
status, message, result = marks[-1]
assert status == "error"
assert "积分中心接口无响应" in message
assert result["ck_valid"] is True
# 旧接口已下线,不能再让它多等一个 15 秒超时。
assert [name for name, _kwargs in client.calls] == ["get_redeem_need_goods"]
def test_query_points_marks_ck_expired_before_calling_points_center(monkeypatch):
client = _StubClient()
runner, marks = _runner(monkeypatch, client, ck_valid=False)
account = HuyaAccount(uid=str(UID), cookie=COOKIE, status="imported")
runner._execute_query_points(
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"
assert client.calls == []