feat(huya): 支持四段式账号导入接码及旋转/文本点选多级验证码闭环过验
- 账号导入:支持'账号----密码----手机号----接码链接'四段式解析与持久化存储 - 旋转验证码 (rotate_auth / xz):逆向分析官方协议,基于圆周边界采样互相关算法秒级计算旋转还原角度 - 文本点选验证码 (dx_auth):内置成语与古诗名句语料,支持题干菜名提取与古诗/成语挖空正则求解及小图动态合成 - 短信二次验证 (sms_auth2):结合 sms_url 自动轮询接码链接并 Hex 编码提交官方过验 - 前端交互:增加接码标签、在线查询短信验证码、一键自动短信登录与测试接码链接功能 - 回归测试:新增旋转验证、短信接码流与文本点选测试用例(173 passed)
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""虎牙旋转验证码 (rotate_auth / xz) 求解与捕获测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from core.huya.verification.capture import CaptchaCaptureRecord
|
||||
from core.huya.verification.solver import HuyaVerificationSolver
|
||||
|
||||
|
||||
def test_rotate_strategy_recognized():
|
||||
"""验证 rotate_auth.html 能被正确解析为 rotate_auth 类型。"""
|
||||
url = (
|
||||
"https://aq.huya.com/p/safe_auth/rotate_auth.html?"
|
||||
"hideShareButton=1¶m=test_param&sceneType=4&appId=5008&mobile=001******5865"
|
||||
)
|
||||
solver = HuyaVerificationSolver(page_url=url, account="test_rotate")
|
||||
solver._parse_strategy(url)
|
||||
assert solver.auth_type == "rotate_auth"
|
||||
|
||||
|
||||
def test_opencv_rotate_angle_accuracy():
|
||||
"""验证真实旋转验证码图片能够精准计算出旋转还原角度。"""
|
||||
sample_dir = Path("data/captcha_captures/20260906_132605_pt_auth_348f71b3")
|
||||
if not sample_dir.exists():
|
||||
return
|
||||
|
||||
bg_bytes = (sample_dir / "bg_restored.jpg").read_bytes()
|
||||
puzzle_bytes = (sample_dir / "puzzle.png").read_bytes()
|
||||
|
||||
angle = HuyaVerificationSolver._opencv_rotate_angle(bg_bytes, puzzle_bytes)
|
||||
# 该样本的真值在 142 度左右
|
||||
assert 138 <= angle <= 146
|
||||
|
||||
|
||||
def test_capture_rotate_data(tmp_path: Path):
|
||||
"""测试旋转验证码捕获能正确落盘并生成标注图与元数据。"""
|
||||
sample_dir = Path("data/captcha_captures/20260906_132605_pt_auth_348f71b3")
|
||||
if not sample_dir.exists():
|
||||
return
|
||||
|
||||
bg_bytes = (sample_dir / "bg_restored.jpg").read_bytes()
|
||||
puzzle_bytes = (sample_dir / "puzzle.png").read_bytes()
|
||||
|
||||
record = CaptchaCaptureRecord(
|
||||
auth_type="rotate_auth",
|
||||
account="test_user",
|
||||
page_url="https://aq.huya.com/p/safe_auth/rotate_auth.html?param=123",
|
||||
scene_type="4",
|
||||
output_base_dir=tmp_path,
|
||||
)
|
||||
out_dir = record.capture_rotate_data(
|
||||
code="test_code_1234",
|
||||
dtu_bytes=bg_bytes,
|
||||
puzzle_bytes=puzzle_bytes,
|
||||
restored_bg_bytes=bg_bytes,
|
||||
angle=142,
|
||||
raw_data={"code": "test_code_1234", "title": "旋转验证"},
|
||||
)
|
||||
assert (out_dir / "annotated.jpg").exists()
|
||||
assert (out_dir / "meta.json").exists()
|
||||
assert record.meta["solution"]["angle"] == 142
|
||||
assert record.meta["auth_type"] == "rotate_auth"
|
||||
@@ -0,0 +1,142 @@
|
||||
"""虎牙四段式账号 (账号----密码----手机号----接码链接) 导入与短信自动求解测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from core.huya.verification.solver import (
|
||||
HuyaSmsAuthRequiredError,
|
||||
HuyaVerificationSolver,
|
||||
)
|
||||
from web.backend.services.huya_service import (
|
||||
import_huya_password_accounts,
|
||||
split_huya_password_line,
|
||||
)
|
||||
|
||||
|
||||
def test_parse_password_line_with_sms_url():
|
||||
"""测试四段式账号行解析 (账号----密码----手机号----接码链接)。"""
|
||||
line = (
|
||||
"hy_299112483----aa778899----+14504409332----"
|
||||
"http://194.163.45.144/api/get_sms?key=c16fe33219924363fa362ed69dc6fd4f"
|
||||
)
|
||||
parsed = split_huya_password_line(line)
|
||||
assert parsed is not None
|
||||
assert parsed.username == "hy_299112483"
|
||||
assert parsed.password == "aa778899"
|
||||
assert parsed.phone == "+14504409332"
|
||||
assert (
|
||||
parsed.sms_url
|
||||
== "http://194.163.45.144/api/get_sms?key=c16fe33219924363fa362ed69dc6fd4f"
|
||||
)
|
||||
|
||||
|
||||
def test_parse_password_line_backward_compatible():
|
||||
"""测试老格式向后兼容性。"""
|
||||
# 2段
|
||||
p2 = split_huya_password_line("user123----pass456")
|
||||
assert p2 is not None
|
||||
assert p2.username == "user123"
|
||||
assert p2.password == "pass456"
|
||||
assert p2.phone == ""
|
||||
assert p2.sms_url == ""
|
||||
|
||||
# 3段 (带手机号或cookie)
|
||||
p3 = split_huya_password_line("user123----pass456----13800138000")
|
||||
assert p3 is not None
|
||||
assert p3.username == "user123"
|
||||
assert p3.password == "pass456"
|
||||
assert p3.cookie == "13800138000"
|
||||
assert p3.sms_url == ""
|
||||
|
||||
|
||||
def test_import_password_accounts_persists_sms_url():
|
||||
"""测试导入账号密码服务会正确写入 sms_url 和 game_phone。"""
|
||||
mock_db = MagicMock()
|
||||
# 模拟无现有账号
|
||||
mock_db.query().filter().first.return_value = None
|
||||
|
||||
text = (
|
||||
"hy_299112483----aa778899----+14504409332----"
|
||||
"http://194.163.45.144/api/get_sms?key=c16fe33219924363fa362ed69dc6fd4f"
|
||||
)
|
||||
created, skipped = import_huya_password_accounts(mock_db, text, tag="测试接码")
|
||||
assert created == 1
|
||||
assert skipped == 0
|
||||
|
||||
added_account = mock_db.add.call_args[0][0]
|
||||
assert added_account.username == "hy_299112483"
|
||||
assert added_account.game_phone == "+14504409332"
|
||||
assert (
|
||||
added_account.sms_url
|
||||
== "http://194.163.45.144/api/get_sms?key=c16fe33219924363fa362ed69dc6fd4f"
|
||||
)
|
||||
assert added_account.tag == "测试接码"
|
||||
|
||||
|
||||
def test_solver_strategy_with_and_without_sms_url():
|
||||
"""测试在遇到 sms_auth2 时,有接码链接不抛异常,无接码链接抛出短信异常。"""
|
||||
url = "https://aq.huya.com/p/safe_auth/sms_auth2.html?param=xyz&mobile=14504409332"
|
||||
|
||||
# 无 sms_url,应触发 HuyaSmsAuthRequiredError
|
||||
solver_no_sms = HuyaVerificationSolver(page_url=url)
|
||||
with pytest.raises(HuyaSmsAuthRequiredError):
|
||||
solver_no_sms._parse_strategy(url)
|
||||
|
||||
# 有 sms_url,应正常解析为 sms_auth
|
||||
solver_with_sms = HuyaVerificationSolver(
|
||||
page_url=url,
|
||||
sms_url="http://194.163.45.144/api/get_sms?key=test",
|
||||
)
|
||||
solver_with_sms._parse_strategy(url)
|
||||
assert solver_with_sms.auth_type == "sms_auth"
|
||||
|
||||
|
||||
def test_solver_sms_flow():
|
||||
"""测试 solver._solve_sms_strategy 自动发码、接码与提码闭环。"""
|
||||
from core.sms_provider import SmsPollResult
|
||||
|
||||
url = "https://aq.huya.com/p/safe_auth/sms_auth2.html?param=mock_param&mobile=14504409332&appId=5002"
|
||||
solver = HuyaVerificationSolver(
|
||||
page_url=url,
|
||||
sms_url="http://194.163.45.144/api/get_sms?key=test_key",
|
||||
account="hy_test",
|
||||
)
|
||||
solver._parse_strategy(url)
|
||||
|
||||
mock_send_res = {"returnCode": 0, "message": "发送成功"}
|
||||
mock_verify_res = {
|
||||
"returnCode": 0,
|
||||
"data": {"authId": "auth_token_success_123"},
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
solver,
|
||||
"_request_json",
|
||||
return_value=mock_send_res,
|
||||
) as mock_req_json,
|
||||
patch.object(
|
||||
solver,
|
||||
"_submit_verify",
|
||||
return_value=mock_verify_res["data"],
|
||||
) as mock_submit,
|
||||
patch(
|
||||
"core.sms_provider.SmsProviderClient.poll",
|
||||
return_value=SmsPollResult(status="code", code="654321"),
|
||||
) as mock_poll,
|
||||
):
|
||||
data = solver._solve_sms_strategy()
|
||||
|
||||
assert data["authId"] == "auth_token_success_123"
|
||||
mock_req_json.assert_called_once()
|
||||
assert "send-sms-code" in mock_req_json.call_args[0][1]
|
||||
|
||||
mock_poll.assert_called_once()
|
||||
|
||||
mock_submit.assert_called_once()
|
||||
verify_url, verify_payload = mock_submit.call_args[0]
|
||||
assert "verify-sms-code" in verify_url
|
||||
assert verify_payload["data"]["smsCode"] == "654321".encode().hex()
|
||||
@@ -0,0 +1,37 @@
|
||||
"""虎牙文本点选验证码 (dx_auth 填空/菜名/成语) 目标提取与提示图合成测试。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
from core.huya.verification.solver import HuyaVerificationSolver
|
||||
|
||||
|
||||
def test_extract_target_chars_from_dish_question() -> None:
|
||||
question = '请按照菜名"客家盐焗鸡"中汉字的顺序,在给定图片中点选对应的汉字'
|
||||
chars = HuyaVerificationSolver._extract_target_chars_from_question(question)
|
||||
assert chars == ["客", "家", "盐", "焗", "鸡"]
|
||||
|
||||
|
||||
def test_extract_target_chars_from_poem_cloze() -> None:
|
||||
question = '请按照古诗"千_鸟_绝"中汉字被挖空的顺序,在给定图片中点选对应的汉字以填充完整给定古诗'
|
||||
chars = HuyaVerificationSolver._extract_target_chars_from_question(question)
|
||||
assert chars == ["山", "飞"]
|
||||
|
||||
|
||||
def test_extract_target_chars_from_idiom_cloze() -> None:
|
||||
question = '请按照成语"_眸皓_"中汉字被挖空的顺序,在给定图片中点选对应的汉字以填充完整给定成语'
|
||||
chars = HuyaVerificationSolver._extract_target_chars_from_question(question)
|
||||
assert chars == ["明", "齿"]
|
||||
|
||||
|
||||
def test_synthesize_text_tip() -> None:
|
||||
chars = ["客", "家", "盐", "焗", "鸡"]
|
||||
img_bytes = HuyaVerificationSolver._synthesize_text_tip(chars)
|
||||
assert len(img_bytes) > 0
|
||||
arr = np.frombuffer(img_bytes, np.uint8)
|
||||
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
|
||||
assert img is not None
|
||||
assert img.shape[0] == 44
|
||||
assert img.shape[1] == 220
|
||||
Reference in New Issue
Block a user