feat(scripts): 添加 PEP 723 脚本元数据并修复 HTTP/2 伪头过滤问题

- 为 build_studio_urls.py 和 lookup_groups.py 添加 PEP 723 依赖声明
- 修复 HTTP/2 伪头导致 requests 抛 InvalidHeader 的问题
- 添加 CDP 端口连通性预检查及启动指引
This commit is contained in:
2026-08-27 13:33:34 +08:00
parent b8bd749f43
commit 9020752eed
23 changed files with 885 additions and 294 deletions

View File

@@ -53,6 +53,63 @@ class FakeSession:
return FakeResp(200, {"groupDatas": []})
# ---------------------------------------------------------------------------
# HTTP/2 伪头过滤
# ---------------------------------------------------------------------------
class TestPseudoHeaderFilter:
def test_regex_matches_pseudo_headers(self, lookup_groups):
assert lookup_groups.PSEUDO_HEADER_RE.match(":authority")
assert lookup_groups.PSEUDO_HEADER_RE.match(":method")
assert not lookup_groups.PSEUDO_HEADER_RE.match("Authorization")
assert not lookup_groups.PSEUDO_HEADER_RE.match("X-YouTube-Delegation-Context")
def test_search_strips_pseudo_headers(self, lookup_groups):
"""套件里残留伪头时回放请求不得携带requests 会抛 InvalidHeader"""
bundle = {
"ownerId": "O1", "ownerDisplay": "一号", "url": "https://example/post",
"headers": {
"Authorization": "SAPISIDHASH x", "Cookie": "SID=1",
":authority": "studio.youtube.com", ":method": "POST",
":path": "/youtubei/v1/yta_web/search_groups", ":scheme": "https",
},
"bodyTemplate": {"query": ""},
}
sess = FakeSession({})
sess.routing["::x"] = FakeResp(200, {"groupDatas": [{"displayName": "x", "groupId": "G1"}]})
r = lookup_groups.search(bundle, "x", sess)
assert r["status"] == "OK"
sent = sess.calls[-1][2]
assert all(not k.startswith(":") for k in sent)
assert sent["Authorization"] == "SAPISIDHASH x"
# ---------------------------------------------------------------------------
# CDP 端口预检
# ---------------------------------------------------------------------------
class TestCdpPrecheck:
def test_unreachable_port_exits_with_guidance(self, lookup_groups):
"""端口不通时 SystemExit输出含可执行的启动指引含残留进程提示"""
with pytest.raises(SystemExit) as exc:
lookup_groups._assert_cdp_reachable("http://localhost:59999")
msg = str(exc.value)
assert "remote-debugging-port" in msg
assert "残留" in msg
def test_reachable_port_passes(self, lookup_groups, monkeypatch):
class FakeUrlopen:
def __enter__(self):
return self
def __exit__(self, *a):
return False
monkeypatch.setattr(
"urllib.request.urlopen",
lambda url, timeout=3: FakeUrlopen(),
)
lookup_groups._assert_cdp_reachable("http://localhost:9222") # 不抛即通过
@pytest.fixture()
def bundle():
tpl = {"context": {"user": {"delegationContext": {"externalOwnerId": "O1"}}},

View File

@@ -185,6 +185,29 @@ class TestDecodeZippedData:
with zipfile.ZipFile(io.BytesIO(out)) as zf:
assert zf.read("x.csv") == b"1,2"
def test_urlsafe_nopad_variant(self, downloader):
"""真实接口返回 URL-safe-/_且无 = 填充的 base64必须与标准表等价解码。"""
data = make_zip_bytes({"表格数据.csv": "a,b\n1,2"})
std = base64.b64encode(data).decode("ascii")
urlsafe_nopad = std.replace("+", "-").replace("/", "_").rstrip("=")
assert downloader.decode_zipped_data({"zippedData": urlsafe_nopad}) == data
def test_std_and_urlsafe_agree(self, downloader):
"""同一字节流的标准/URL-safe 两种编码解出相同结果。"""
data = make_zip_bytes({"x.csv": "1,2"})
std = base64.b64encode(data).decode("ascii")
urlsafe = base64.urlsafe_b64encode(data).decode("ascii").rstrip("=")
a = downloader.decode_zipped_data({"zippedData": std})
b = downloader.decode_zipped_data({"zippedData": urlsafe})
assert a == b == data
def test_non_zip_payload_raises_pk_error(self, downloader):
"""解码结果不是 zip缺 PK 头)时报错,而非静默落盘损坏文件。"""
import base64 as b64
bogus = b64.b64encode(b"not a zip at all------").decode("ascii")
with pytest.raises(ValueError, match="PK"):
downloader.decode_zipped_data({"zippedData": bogus})
def test_missing_field_raises(self, downloader):
with pytest.raises(ValueError, match="zippedData"):
downloader.decode_zipped_data({"foo": "bar"})
@@ -195,6 +218,24 @@ class TestDecodeZippedData:
downloader.decode_zipped_data({"zippedData": empty})
# ---------------------------------------------------------------------------
# verify_zipzip 完整性校验
# ---------------------------------------------------------------------------
class TestVerifyZip:
def test_valid_zip_passes(self, downloader):
downloader.verify_zip(make_zip_bytes({"a.csv": "1,2", "b.csv": "3,4"}))
def test_truncated_zip_raises_value_error(self, downloader):
good = make_zip_bytes({"a.csv": "1,2"})
bad = good[: len(good) // 2] # 截断中央目录
with pytest.raises(ValueError):
downloader.verify_zip(bad)
def test_not_a_zip_raises(self, downloader):
with pytest.raises(ValueError):
downloader.verify_zip(b"plain text")
# ---------------------------------------------------------------------------
# intercept_and_save拦截器FakePage 模拟)
# ---------------------------------------------------------------------------