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:
@@ -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"}}},
|
||||
|
||||
@@ -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_zip:zip 完整性校验
|
||||
# ---------------------------------------------------------------------------
|
||||
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 模拟)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user