"""虎牙 App 密码登录及相关组件测试。""" import pytest import base64 import os import struct from unittest.mock import patch, MagicMock from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from core.huya import ( HuyaAppLoginError, HuyaAppPasswordLogin, HuyaAppQrAuthRequiredError, login_huya_app_password, login_huya_password, ) 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 User, HuyaAccount from web.backend.schemas import ( HuyaAppPasswordLoginRequest, HuyaPasswordLoginSelectedRequest, ) from web.backend.routers.huya import ( app_password_login_account, app_password_login_selected_accounts, ) 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, ): with 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"), ): with 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() ) 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"]