按 10.13「开通精英宝典/购买幸运币」抓包(m-shop.huya.com/points.html)对齐商城链路: - WSS host 改为 launch.queryHttpDns 动态解析(失败回退抓包实测 host);握手 baseinfo 只带 uid/guid/页面 UA/exp,Origin 与页面同源 - wsLaunch 改为带真实 lUid/sGuid;初始化补上 AUTH(0x0A) → getConfig → UpdateUserExps(0x17),并把 getConfig 下发的版本 map 原样回传(不回传则业务 RPC 无响应) - 补齐新 RPC:getSupplierInfoV5 / acceptHyProtocolV5 / calcOrderPromotion / PlayMallUI.getMyPromotion|queryAllocation / wupui.* / huyauserui.getUserProfile - 下单参数对齐:shopAppInfo.scene=7 与订单 src/orderScene=4 分离、orderType=6、 bizType=5、gameCategoryId=507、extraParam.bizParams 标记 jifencenter、env 仅空点击 序列、支付回调指向 m-shop.huya.com/points.html - 开通链路抽到 services/huya_shop_flow.py,充值任务改为调用它 验证:新增 tests/test_huya_points_shop_protocol.py 与抓包逐字节比对(心跳、版本回传、 wsLaunch/getConfig/下单/支付帧与新 RPC 参数,共 10 条);按 AGENTS.md 做了反向验证 (把 jifencenter 改成 0 后该用例变红,还原后恢复)。ruff format --check 与全量 pytest 通过。
392 lines
13 KiB
Python
392 lines
13 KiB
Python
"""积分中心商城链路回归测试(2026-10-13「开通精英宝典 / 购买幸运币」抓包)。
|
||
|
||
抓包文件不入库,这里把关键帧的字节与字段直接固化在测试里;每条断言的来源
|
||
都写在注释里,改动协议时先核对抓包再改测试。
|
||
"""
|
||
|
||
import asyncio
|
||
import base64
|
||
import struct
|
||
|
||
from core.huya.frame_decoder import _decode_wup_body, _extract_wup
|
||
from core.huya.http_client import (
|
||
WSConnectParaInfo,
|
||
_extract_ws_hosts,
|
||
generate_ws_baseinfo,
|
||
)
|
||
from core.huya.shop_structs import shop_spu_number
|
||
from core.huya.taf_protocol import TafInputStream, TafOutputStream, TafType
|
||
from core.huya.wss_client import (
|
||
HuyaWssClient,
|
||
WssCommand,
|
||
WssMessage,
|
||
)
|
||
from core.huya.wup_protocol import WupResponse
|
||
|
||
UID = 1199668595232
|
||
GUID = "0a8995af9218ce6a340261946294a9e7"
|
||
COOKIE = f"udb_uid={UID}; yyuid={UID}; guid={GUID}"
|
||
PID = 1199650619883
|
||
# 抓包字面值(不要引用实现常量,否则断言会与实现一起漂移)
|
||
POINTS_UA_LITERAL = "webh5&0.0.1&websocket&&h5_/points.html"
|
||
TAIL_LITERAL = bytes.fromhex("2c36004c5c6600")
|
||
BIZ_PARAMS_LITERAL = '{ "jifencenter": 1 }'
|
||
|
||
# 抓包实测:RegisterGroup(live:0) 与 UpdateUserExps(版本 map) 的 body 逐字节一致
|
||
HEARTBEAT_BODY = bytes.fromhex("09000106066c6976653a3016002c36004c5c6600")
|
||
UPDATE_EXPS_BODY = bytes.fromhex("0800010605353836363416063130373236332c36004c5c6600")
|
||
CONFIG_VERSIONS = {"58664": "107263"}
|
||
|
||
|
||
class _FakeWs:
|
||
"""记录发出的帧,并立刻用预设 body 兑现 RPC future(离线跑完整链路)。"""
|
||
|
||
def __init__(self, client: HuyaWssClient, response: bytes = b""):
|
||
self.client = client
|
||
self.response = response
|
||
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(self.response)
|
||
|
||
def last_body(self) -> bytes:
|
||
return WssMessage.decode(self.sent[-1]).body
|
||
|
||
def last_request(self) -> dict:
|
||
return _decode_wup_body(self.last_body())
|
||
|
||
|
||
def _points_client(response: bytes = b"") -> HuyaWssClient:
|
||
client = HuyaWssClient(logger=lambda _message: None)
|
||
client._points_mode = True
|
||
client.ws = _FakeWs(client, response)
|
||
return client
|
||
|
||
|
||
def _wup_response(t_rsp: bytes) -> bytes:
|
||
"""构造一个 tRsp 为给定结构的 WUP 响应(含 4 字节长度前缀)。"""
|
||
buffer = TafOutputStream()
|
||
buffer.write_head(0, TafType.MAP)
|
||
buffer.write_int32(0, 1)
|
||
buffer.write_string(0, "tRsp")
|
||
buffer.write_bytes(1, t_rsp)
|
||
|
||
output = TafOutputStream()
|
||
output.write_int16(1, 3)
|
||
output.write_int8(2, 0)
|
||
output.write_int32(3, 0)
|
||
output.write_int32(4, 1)
|
||
output.write_string(5, "mobileui")
|
||
output.write_string(6, "getConfig")
|
||
output.write_bytes(7, buffer.get_bytes())
|
||
output.write_int32(8, 0)
|
||
output.write_map(9, {})
|
||
output.write_map(10, {})
|
||
body = output.get_bytes()
|
||
return struct.pack(">I", 4 + len(body)) + body
|
||
|
||
|
||
def _config_rsp(versions: dict[str, str]) -> bytes:
|
||
"""mobileui.getConfig 响应:tag0 开关 map、tag1 版本 map(抓包实测)。"""
|
||
output = TafOutputStream()
|
||
output.write_struct_begin(0)
|
||
output.write_map(0, {})
|
||
output.write_map(1, versions)
|
||
output.write_struct_end()
|
||
return _wup_response(output.get_bytes())
|
||
|
||
|
||
def test_register_group_frame_matches_capture():
|
||
async def run():
|
||
client = _points_client()
|
||
await client.send_heartbeat()
|
||
message = WssMessage.decode(client.ws.sent[-1])
|
||
assert message.command == WssCommand.HEARTBEAT_SEND
|
||
assert message.sequence == 0x1D00000D
|
||
assert message.body == HEARTBEAT_BODY
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_shop_ws_launch_carries_real_identity_and_points_ua():
|
||
async def run():
|
||
client = _points_client(_wup_response(b""))
|
||
await client.call_ws_launch_shop(UID, GUID)
|
||
|
||
message = WssMessage.decode(client.ws.sent[-1])
|
||
assert message.command == WssCommand.RPC_REQUEST
|
||
# 抓包:wsLaunch 帧 seq=0x1d000100,iRequestId=-1
|
||
assert message.sequence == 0x1D000100
|
||
wup = WupResponse()
|
||
wup.decode(_extract_wup(message.body))
|
||
assert wup.iRequestId == -1
|
||
|
||
decoded = _decode_wup_body(message.body)
|
||
assert (decoded["servant"], decoded["func"]) == ("launch", "wsLaunch")
|
||
t_req = decoded["tReq"]
|
||
# 抓包:lUid=真实 uid、sGuid=真实 guid、UA=h5_/points.html(不再是匿名)
|
||
assert t_req["tag0"] == UID
|
||
assert t_req["tag1"] == GUID
|
||
assert t_req["tag2"] == POINTS_UA_LITERAL
|
||
assert t_req["tag3"] == "HUYA&ZH&2052"
|
||
assert t_req["tag4"] == {
|
||
"tag0": "",
|
||
"tag1": "",
|
||
"tag2": "",
|
||
"tag3": "",
|
||
"tag4": "",
|
||
}
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_get_config_uses_real_user_and_confirms_version_map():
|
||
async def run():
|
||
client = _points_client(_config_rsp(CONFIG_VERSIONS))
|
||
body = await client.call_get_config_shop(uid=UID, guid=GUID, cookie=COOKIE)
|
||
assert body is not None
|
||
assert client._config_versions == CONFIG_VERSIONS
|
||
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == ("mobileui", "getConfig")
|
||
t_req = decoded["tReq"]
|
||
# 抓包:getConfig 的 UserId 带真实 uid/guid/页面 UA/完整 Cookie
|
||
assert t_req["tag0"]["tag0"] == UID
|
||
assert t_req["tag0"]["tag1"] == GUID
|
||
assert t_req["tag0"]["tag3"] == POINTS_UA_LITERAL
|
||
assert GUID in t_req["tag0"]["tag4"]
|
||
assert t_req["tag2"] == "huya"
|
||
|
||
# UpdateUserExps 必须原样回传版本 map(抓包逐字节一致)
|
||
await client.send_confirm(client._config_versions)
|
||
assert client.ws.last_body() == UPDATE_EXPS_BODY
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_create_order_v5_matches_points_capture():
|
||
async def run():
|
||
client = _points_client()
|
||
await client.create_order(
|
||
uid=UID,
|
||
guid=GUID,
|
||
cookie=COOKIE,
|
||
pid=PID,
|
||
spu_id="hy-8328015",
|
||
sku_id=7756811,
|
||
item_count=1,
|
||
)
|
||
|
||
message = WssMessage.decode(client.ws.sent[-1])
|
||
# 抓包:下单帧用 0x1d000108(其余业务帧 0x1d000107)
|
||
assert message.sequence == 0x1D000108
|
||
|
||
decoded = _decode_wup_body(message.body)
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"shopMiddleUI",
|
||
"createOrderV5",
|
||
)
|
||
t_req = decoded["tReq"]
|
||
assert t_req["tag1"]["tag4"] == 7 # shopAppInfo.scene=7(appInfo 用 7)
|
||
assert t_req["tag3"] == PID
|
||
assert t_req["tag4"] == 7756811
|
||
assert t_req["tag5"] == 1
|
||
assert t_req["tag7"] == "hy-8328015"
|
||
assert t_req["tag8"] == "0"
|
||
assert t_req["tag10"] == 4 # src
|
||
assert t_req["tag12"] == 6 # orderType
|
||
assert t_req["tag13"]["tag4"] == BIZ_PARAMS_LITERAL
|
||
assert t_req["tag16"] == "yellowcarlist"
|
||
assert t_req["tag17"] == {"user_click_seq": "[]"}
|
||
assert t_req["tag18"] == 4 # orderScene
|
||
assert t_req["tag21"]["tag2"] == 1
|
||
assert t_req["tag30"] == 5
|
||
assert t_req["tag31"] == 507
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_pay_order_submit_v5_callback_points_page():
|
||
async def run():
|
||
client = _points_client()
|
||
await client.pay_order_submit(
|
||
uid=UID,
|
||
guid=GUID,
|
||
cookie=COOKIE,
|
||
order_id=11339649,
|
||
pay_type=1,
|
||
pid=PID,
|
||
)
|
||
|
||
message = WssMessage.decode(client.ws.sent[-1])
|
||
assert message.sequence == 0x1D000108
|
||
|
||
decoded = _decode_wup_body(message.body)
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"shopMiddleUI",
|
||
"payOrderSubmitV5",
|
||
)
|
||
t_req = decoded["tReq"]
|
||
assert t_req["tag2"] == 11339649
|
||
assert t_req["tag3"] == "Zfb"
|
||
assert t_req["tag4"] == "QrCode"
|
||
# 抓包:回调指向 m-shop.huya.com/points.html,不是 yaoguo 域名
|
||
assert t_req["tag5"].startswith(
|
||
"https://m-shop.huya.com/points.html#/consumer/paycallback?orderId=11339649"
|
||
)
|
||
assert t_req["tag6"] == {"user_click_seq": "[]"}
|
||
assert t_req["tag7"] == "null"
|
||
assert t_req["tag8"] == ""
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_new_points_rpcs_match_capture():
|
||
async def run():
|
||
client = _points_client()
|
||
|
||
await client.get_supplier_info(uid=UID, guid=GUID, cookie=COOKIE)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"shopMiddleUI",
|
||
"getSupplierInfoV5",
|
||
)
|
||
assert decoded["tReq"]["tag2"] == 87401
|
||
|
||
await client.accept_hy_protocol(uid=UID, guid=GUID, cookie=COOKIE)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"shopMiddleUI",
|
||
"acceptHyProtocolV5",
|
||
)
|
||
assert decoded["tReq"]["tag2"] == 1
|
||
|
||
await client.calc_order_promotion(
|
||
uid=UID,
|
||
guid=GUID,
|
||
cookie=COOKIE,
|
||
pid=PID,
|
||
spu_id="hy-8328015",
|
||
sku_id=7756811,
|
||
)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"shopMiddleUI",
|
||
"calcOrderPromotion",
|
||
)
|
||
t_req = decoded["tReq"]
|
||
assert t_req["tag2"] == 8328015 # spuId 去前缀后的数字
|
||
assert t_req["tag3"] == 7756811
|
||
assert t_req["tag4"] == 1
|
||
assert t_req["tag5"] == PID
|
||
assert (t_req["tag7"], t_req["tag8"], t_req["tag9"]) == (1, 1, 1)
|
||
assert t_req["tag14"] == 8328015
|
||
assert t_req["tag15"] == 9815
|
||
|
||
await client.play_mall_query_allocation(uid=UID, guid=GUID, cookie=COOKIE)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"PlayMallUI",
|
||
"queryAllocation",
|
||
)
|
||
t_req = decoded["tReq"]
|
||
assert t_req["tag1"]["tag1"] == 1001
|
||
assert t_req["tag1"]["tag5"] == 7
|
||
assert t_req["tag1"]["tag6"] == "yellowcarlist"
|
||
assert t_req["tag2"] == "heping_baodian_credit_center_page"
|
||
|
||
await client.play_mall_get_my_promotion(
|
||
uid=UID,
|
||
guid=GUID,
|
||
cookie=COOKIE,
|
||
spu_id="hy-8328015",
|
||
sku_id=7756811,
|
||
price=3000,
|
||
)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"PlayMallUI",
|
||
"getMyPromotion",
|
||
)
|
||
goods = decoded["tReq"]["tag6"]
|
||
assert goods["tag0"] == 87401
|
||
assert goods["tag1"] == 8328015
|
||
assert goods["tag2"] == 7756811
|
||
assert goods["tag3"] == 3000
|
||
assert goods["tag6"] == 9815
|
||
|
||
await client.get_user_profile(uid=UID, guid=GUID, cookie=COOKIE)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"huyauserui",
|
||
"getUserProfile",
|
||
)
|
||
assert decoded["tReq"]["tag1"] == UID
|
||
|
||
await client.get_follow_wechat_status(uid=UID, guid=GUID, cookie=COOKIE)
|
||
decoded = client.ws.last_request()
|
||
assert (decoded["servant"], decoded["func"]) == (
|
||
"wupui",
|
||
"getUserFollowWeChatOfficialStatus",
|
||
)
|
||
assert decoded["tReq"]["tag1"] == 1
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_confirm_frame_ends_with_capture_tail():
|
||
async def run():
|
||
client = _points_client()
|
||
await client.send_confirm({})
|
||
assert client.ws.last_body().endswith(TAIL_LITERAL)
|
||
|
||
asyncio.run(run())
|
||
|
||
|
||
def test_ws_baseinfo_matches_capture_shape():
|
||
raw = base64.b64decode(generate_ws_baseinfo(UID, GUID, POINTS_UA_LITERAL))
|
||
info = WSConnectParaInfo()
|
||
info.read_from(TafInputStream(raw))
|
||
# 抓包:握手 baseinfo 只有 uid/guid/UA/appSrc/exp 有值,cookie 与 traceId 为空
|
||
assert info.lUid == UID
|
||
assert info.sGuid == GUID
|
||
assert info.sUA == POINTS_UA_LITERAL
|
||
assert info.sAppSrc == "HUYA&ZH&2052"
|
||
assert info.sCookie == ""
|
||
assert info.sTraceId == ""
|
||
assert info.sExp == ""
|
||
|
||
with_exp = base64.b64decode(
|
||
generate_ws_baseinfo(UID, GUID, POINTS_UA_LITERAL, exp="58664.107263")
|
||
)
|
||
parsed = WSConnectParaInfo()
|
||
parsed.read_from(TafInputStream(with_exp))
|
||
assert parsed.sExp == "58664.107263"
|
||
|
||
|
||
def test_query_http_dns_hosts_extraction():
|
||
payload = (
|
||
b"\x1a\x09\x00\x04"
|
||
b"\x06\x17d35bf201-ws.va.huya.com"
|
||
b"\x06\x17d35bf373-ws.va.huya.com"
|
||
b"\x06\x17d35bf35c-ws.va.huya.com"
|
||
b"\x06\x17dcca159d-ws.va.huya.com"
|
||
)
|
||
assert _extract_ws_hosts(payload) == [
|
||
"d35bf201-ws.va.huya.com",
|
||
"d35bf373-ws.va.huya.com",
|
||
"d35bf35c-ws.va.huya.com",
|
||
"dcca159d-ws.va.huya.com",
|
||
]
|
||
assert _extract_ws_hosts(b"") == []
|
||
|
||
|
||
def test_shop_spu_number_strips_prefix():
|
||
assert shop_spu_number("hy-8328015") == 8328015
|
||
assert shop_spu_number("8327356") == 8327356
|
||
assert shop_spu_number("") == 0
|