"""开通支付码二维码内容:1600+ 字符的支付地址 → 手机扫得动、打得开的地址。 生产实测(2026-10-15):`payOrderSubmitV5` 返回的支付宝网关地址有 **1626** 字符 (`https://openapi.alipay.com/gateway.do?...&sign=...`)。直接把它编成二维码是 version 29+(140+ 个模块),在 150px 的画面上每个模块只有约 1 像素 —— 手机扫不上。 现在的优先级: 1. **支付宝收银台直达地址**(`resolve_pay_entry_url`):网关地址 302 两次后落到 `https://excashier.alipay.com/standard/auth.htm?payOrderId=...`(约 74 字符), 公网域名,后端跑在 localhost 时客户也扫得开; 2. **本站短链** `/api/huya/pay/?k=`(约 38 字符):后端 302 跳到网关地址, 但要求本页域名手机能访问到。 """ import pytest from fastapi import HTTPException from web.backend.models import HuyaAccount, HuyaTask, User from web.backend.routers.huya.pay import redirect_huya_pay from web.backend.services import huya_pay_link from web.backend.services.huya_pay_link import ( PAY_PATH_PREFIX, PayTargets, extract_pay_qr_code, is_alipay_url, pay_qr_path, pay_token, resolve_pay_entry_url, resolve_pay_targets, verify_pay_token, ) # 真机取到的支付宝网关地址形态(长度同量级) ALIPAY_LONG_URL = ( "https://openapi.alipay.com/gateway.do" "?alipay_sdk=alipay-sdk-java-4.40.272.ALL&app_id=2017123456789012" "&method=alipay.trade.page.pay&sign=" + "a" * 1500 ) # 真机实测:上面的网关地址 302 两次后的落点(任务 191 原样取回) CASHIER_URL = ( "https://excashier.alipay.com/standard/auth.htm" "?payOrderId=6cb86f1d259642ac843522b85dab8a52.25" ) # 收银台页面里真正的扫码内容(真机页面原样片段) QR_CODE_LITERAL = "https://qr.alipay.com/upx03110ghym9vm37sx7255c" CASHIER_HTML = ( '\n' f'\n' '\n' f'\n' '' ) def _create_pay_task(db, pay_url: str) -> HuyaTask: user = User(username="pay-link-test", password_hash="x", role="support") db.add(user) db.commit() account = HuyaAccount(uid="1199665170519", cookie="yyuid=1199665170519", tag="") db.add(account) db.commit() task = HuyaTask( batch_id="test-batch", account_id=account.id, task_type="create_recharge_order", handbook_scope="elite", status="running", created_by=user.id, result={"pay_url": pay_url} if pay_url else {}, ) db.add(task) db.commit() return task def test_pay_token_round_trip_and_rejects_others(): token = pay_token(587) assert len(token) == 16 assert verify_pay_token(587, token) is True # 大小写不同就是另一个签名(hex 严格比对) assert verify_pay_token(587, token.upper()) is False assert verify_pay_token(587, "") is False assert verify_pay_token(588, token) is False def test_pay_qr_path_is_short_enough_to_scan(): path = pay_qr_path(587) assert path.startswith(f"{PAY_PATH_PREFIX}/587?k=") # 短链必须足够短:36 字符 → 二维码约 30 个模块(长链是 140+ 个模块) assert len(path) <= 48 # 原始支付地址长度对比(就是它导致扫不上) assert len(ALIPAY_LONG_URL) > 1000 def test_pay_redirect_route_returns_pay_url(db): task = _create_pay_task(db, ALIPAY_LONG_URL) response = redirect_huya_pay(task.id, pay_token(task.id), db) assert response.status_code == 302 assert response.headers["location"] == ALIPAY_LONG_URL def test_pay_redirect_route_rejects_bad_token(db): task = _create_pay_task(db, ALIPAY_LONG_URL) with pytest.raises(HTTPException) as excinfo: redirect_huya_pay(task.id, "deadbeefdeadbeef", db) assert excinfo.value.status_code == 404 def test_pay_redirect_route_rejects_task_without_pay_url(db): task = _create_pay_task(db, "") with pytest.raises(HTTPException) as excinfo: redirect_huya_pay(task.id, pay_token(task.id), db) assert excinfo.value.status_code == 404 class _FakeResponse: """假的 requests 响应:只需要 ``url`` / ``text`` / ``close()``。""" def __init__(self, url: str, text: str = ""): self.url = url self.text = text self.closed = False def close(self) -> None: self.closed = True def _patch_get( monkeypatch, *, final_url: str | None = None, body: str = "", error=None ): calls: list[dict] = [] def fake_get(url, **kwargs): calls.append({"url": url, **kwargs}) if error is not None: raise error return _FakeResponse(final_url or "", body) monkeypatch.setattr(huya_pay_link.requests, "get", fake_get) return calls def test_resolve_pay_entry_url_follows_redirect_to_cashier(monkeypatch): calls = _patch_get(monkeypatch, final_url=CASHIER_URL) assert resolve_pay_entry_url(ALIPAY_LONG_URL) == CASHIER_URL assert calls[0]["url"] == ALIPAY_LONG_URL # 必须跟随 302,否则拿到的是网关页自己 assert calls[0]["allow_redirects"] is True assert "Mozilla" in calls[0]["headers"]["User-Agent"] def test_resolve_pay_entry_url_rejects_non_alipay_target(monkeypatch): """落到非支付宝域名就当没解析出来(调用方回落到本站短链)。""" _patch_get(monkeypatch, final_url="https://evil.example.com/cashier") assert resolve_pay_entry_url(ALIPAY_LONG_URL) == "" def test_resolve_pay_entry_url_swallows_errors(monkeypatch): _patch_get(monkeypatch, error=RuntimeError("network down")) assert resolve_pay_entry_url(ALIPAY_LONG_URL) == "" def test_resolve_pay_entry_url_ignores_blank_or_relative(): assert resolve_pay_entry_url("") == "" assert resolve_pay_entry_url("/api/huya/pay/191?k=x") == "" def test_resolve_pay_entry_url_is_shorter_than_raw_url(monkeypatch): _patch_get(monkeypatch, final_url=CASHIER_URL) resolved = resolve_pay_entry_url(ALIPAY_LONG_URL) # 收银台直达地址约 93 字符 → 二维码模块数远低于原始地址 assert len(resolved) < 120 assert len(resolved) < len(ALIPAY_LONG_URL) def test_extract_pay_qr_code_from_cashier_html(): """收银台页面里的 J_qrCode 才是手机该扫的内容(真机抓到的原样片段)。""" assert extract_pay_qr_code(CASHIER_HTML) == QR_CODE_LITERAL def test_extract_pay_qr_code_ignores_other_alipay_urls(): """只有 qr.alipay.com 才算扫码内容:qrImgUrl(mobilecodec)不算。""" html = ( '' ) assert extract_pay_qr_code(html) == "" assert extract_pay_qr_code("") == "" def test_resolve_pay_targets_returns_cashier_and_qr_code(monkeypatch): _patch_get(monkeypatch, final_url=CASHIER_URL, body=CASHIER_HTML) targets = resolve_pay_targets(ALIPAY_LONG_URL) assert targets.cashier_url == CASHIER_URL assert targets.qr_code == QR_CODE_LITERAL def test_resolve_pay_targets_keeps_cashier_when_qr_code_missing(monkeypatch): """拿不到 J_qrCode(比如停在中间页)时只回落到收银台地址。""" _patch_get(monkeypatch, final_url=CASHIER_URL, body="no qr here") targets = resolve_pay_targets(ALIPAY_LONG_URL) assert targets.cashier_url == CASHIER_URL assert targets.qr_code == "" def test_resolve_pay_targets_empty_on_error(monkeypatch): _patch_get(monkeypatch, error=RuntimeError("boom")) targets = resolve_pay_targets(ALIPAY_LONG_URL) assert targets == PayTargets() def test_is_alipay_url_matches_only_alipay_domains(): assert is_alipay_url(CASHIER_URL) is True assert is_alipay_url(QR_CODE_LITERAL) is True assert is_alipay_url("https://openapi.alipay.com/gateway.do?x=1") is True assert is_alipay_url("https://alipay.com/") is True assert is_alipay_url("https://alipay.com.evil.com/") is False assert is_alipay_url("https://excashier.alipay.com.evil.com/") is False assert is_alipay_url("") is False