Files
live-hub-py/tests/test_huya_app_login.py
T
2026-08-31 10:55:44 +08:00

233 lines
8.5 KiB
Python

"""虎牙 App 密码登录及相关组件测试。"""
import os
import struct
from unittest.mock import MagicMock, patch
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from core.huya import (
HuyaAppLoginError,
)
from core.huya.app_login import (
DEFAULT_GOLDEN_DEV,
login_cred_with_flow,
wup_password_login_raw,
)
from core.huya.cert_forge import build_p1, decrypt_cert, forge_cert, parse_p1
from core.huya.device_profile import generate_profile, get_profile
from core.huya.dfp_register import DfpRegistrationError
from core.huya.envelope_forge import Envelope
from core.huya.login import HuyaLoginResult
from core.huya.nonce_forge import K1_DEFAULT, gen_nonce
from core.huya.udb_aes import udb_decrypt, udb_encrypt
from core.huya.wup_encoder import build_password_login_wup
from web.backend.database import Base
from web.backend.models import HuyaAccount, User
from web.backend.routers.huya import (
app_password_login_account,
app_password_login_selected_accounts,
)
from web.backend.schemas import (
HuyaAppPasswordLoginRequest,
HuyaPasswordLoginSelectedRequest,
)
class TestHuyaAppLogin:
def setup_method(self):
self.engine = create_engine("sqlite://")
Base.metadata.create_all(self.engine)
self.session = sessionmaker(bind=self.engine)()
self.admin = User(username="admin", password_hash="hash", role="super_admin")
self.session.add(self.admin)
self.session.commit()
def teardown_method(self):
self.session.close()
Base.metadata.drop_all(self.engine)
self.engine.dispose()
def test_udb_aes_roundtrip(self):
key = b"4VYcPdvKKqjBHZtC"
plain = b"Hello, Huya App Login Protocol!"
enc = udb_encrypt(key, plain)
dec = udb_decrypt(key, enc)
assert dec == plain
def test_cert_and_nonce_forge(self):
uid = 1199666914671
rnd = gen_nonce(uid, K1_DEFAULT)
assert len(rnd) == 20
fp = b"02df398797432eadefcc12767119ad5e80999389"
cred = b"\x0a" + os.urandom(113)
p1 = build_p1(b"5008", fp, cred, rnd=rnd)
assert len(p1) == 187
cert = forge_cert(p1)
dec = decrypt_cert(cert)
parsed = parse_p1(dec)
assert parsed["rnd"] == rnd
assert parsed["fingerprint"] == fp
assert parsed["cred"] == cred
def test_envelope_patching(self):
env = Envelope.load()
assert env.uid > 0
assert len(env.cert_b64) == 260
new_uid = 1199666911746
env.patch_uid(new_uid)
assert env.uid == new_uid
fake_cert = bytes([0x0C, 0x20]) + os.urandom(192)
env.patch_cert(fake_cert)
assert len(env.cert_b64) == 260
wup_b64 = env.wup_b64()
assert len(wup_b64) > 0
def test_device_profile_generation(self):
p1 = generate_profile()
assert p1["os"] == "android"
assert len(p1["fingerprint"]) == 40
assert len(p1["device_id"]) == 40
assert p1["hdid"] == "ed0db8334cadd236c00cadf7e11ab5a5"
# 画像不再承载 safedeviceid:该令牌由 dfp_register 注册链每次登录前签发
assert "safedeviceid" not in p1
assert "safedeviceid" not in DEFAULT_GOLDEN_DEV
p2 = get_profile("test_user_account_123")
p3 = get_profile("test_user_account_123")
assert p2["fingerprint"] == p3["fingerprint"]
def test_wup_encoder_output(self):
dev = generate_profile()
pkt = build_password_login_wup(
uid_str="300023887",
sha1_password="772ed992b0e161276f44ec63671e60155c506294",
# 注册链签发格式: 180B base64 action 令牌
safedeviceid="A" * 180,
hdid=dev["hdid"],
session=3251699,
trace_id="test-trace-id",
user_action_json="{}",
device_info=dev,
)
assert len(pkt) > 500
total_len = struct.unpack(">I", pkt[:4])[0]
assert total_len == len(pkt)
# ---- 新设备注册链 (core/huya/dfp_register) 生产接入测试 ----
def test_wup_login_skips_registration_when_safedeviceid_given(self):
"""显式传入 safedeviceid 时不再触发注册链(风控重试复用同一注册结果)。"""
with (
patch("core.huya.app_login.register_device") as m_reg,
patch("core.huya.app_login.build_password_login_wup", return_value=b"pkt"),
patch(
"core.huya.app_login.requests.post",
return_value=MagicMock(status_code=200, content=b""),
),
):
wup_password_login_raw("300023887", "pw", safedeviceid="A" * 180)
m_reg.assert_not_called()
def test_wup_login_registers_fresh_device_when_safedeviceid_missing(self):
"""未传 safedeviceid 时先注册,注册签发的 action/device_id 进 WUP 帧。"""
new_action = "B" * 180
new_device_id = "c" * 40
captured = {}
def fake_build(*args, **kwargs):
captured["args"] = args
return b"pkt"
with (
patch(
"core.huya.app_login.register_device",
return_value=("a" * 32, new_action, new_device_id),
) as m_reg,
patch(
"core.huya.app_login.build_password_login_wup", side_effect=fake_build
),
patch(
"core.huya.app_login.requests.post",
return_value=MagicMock(status_code=200, content=b""),
),
):
wup_password_login_raw("300023887", "pw")
m_reg.assert_called_once()
# args: uid_str, sha1, safedeviceid, hdid, session, traceId, ua, dev
assert captured["args"][2] == new_action
assert captured["args"][7]["device_id"] == new_device_id
# 画像默认值里的旧 device_id 被注册结果覆盖,而非沿用
assert captured["args"][7]["device_id"] != DEFAULT_GOLDEN_DEV["device_id"]
def test_wup_login_registration_failure_is_explicit(self):
"""注册失败必须抛错终止,禁止静默回退旧链(不发任何登录请求)。"""
with (
patch(
"core.huya.app_login.register_device",
side_effect=DfpRegistrationError("注册链超时"),
) as m_reg,
patch("core.huya.app_login.requests.post") as m_post,
pytest.raises(HuyaAppLoginError),
):
wup_password_login_raw("300023887", "pw")
m_reg.assert_called_once()
m_post.assert_not_called()
def test_login_cred_flow_registration_failure_is_explicit(self):
"""login_cred_with_flow 注册失败同样包装为 HuyaAppLoginError 显式失败。"""
with (
patch(
"core.huya.app_login.register_device",
side_effect=DfpRegistrationError("注册链 HTTP 500"),
),
pytest.raises(HuyaAppLoginError, match="注册失败"),
):
login_cred_with_flow("300023887", "pw")
def test_router_functions(self):
mock_res = HuyaLoginResult(
success=True,
cookie="udb_cred=mock_cred_123; yyuid=1199666914671; udb_uid=mock_uid; username=mock_test_huya_user",
message="App协议登录成功",
sdid="mock_sdid_123",
)
with patch(
"web.backend.routers.huya.login_huya_app_password", return_value=mock_res
):
req = HuyaAppPasswordLoginRequest(
username="mock_test_huya_user",
password="mock_password_123",
tag="test_tag",
)
resp = app_password_login_account(
req=req, db=self.session, current=self.admin
)
assert resp["success"]
assert "App" in resp["message"]
assert resp["account"].username == "mock_test_huya_user"
# Test batch app login router function
acc = (
self.session.query(HuyaAccount)
.filter(HuyaAccount.username == "mock_test_huya_user")
.first()
)
assert acc is not None
batch_req = HuyaPasswordLoginSelectedRequest(account_ids=[acc.id])
batch_resp = app_password_login_selected_accounts(
req=batch_req, db=self.session, current=self.admin
)
assert batch_resp["success"]
assert batch_resp["count"] == 1
assert len(batch_resp["results"]) == 1
assert batch_resp["results"][0]["success"]