fix(website): handle UTF-8 output and slash refs in link audits (#4123)

This commit is contained in:
kuxiaowo authored and GitHub committed 2026-10-23 02:31:11 +08:00
1 parent 1ba178befb
commit 3f35eacad2
2 files changed
+113 -2

No files matched your search

+3 -2
View File
@@ -19,7 +19,7 @@ from website import authored_markdown
@lru_cache(maxsize=None) @lru_cache(maxsize=None)
def github_api(endpoint): def github_api(endpoint):
"""Read GitHub evidence through the caller's gh installation and authentication.""" """Read GitHub evidence through the caller's gh installation and authentication."""
result = subprocess.run(["gh", "api", endpoint], capture_output=True, text=True, timeout=20) result = subprocess.run(["gh", "api", endpoint], capture_output=True, encoding="utf-8", timeout=20)
if result.returncode: if result.returncode:
raise ValueError(result.stderr.strip()) raise ValueError(result.stderr.strip())
return json.loads(result.stdout) return json.loads(result.stdout)
@@ -32,7 +32,8 @@ def github_target(url):
return None return None
repo = f"repos/{parts[0]}/{parts[1]}" repo = f"repos/{parts[0]}/{parts[1]}"
if url.hostname == "raw.githubusercontent.com" and len(parts) >= 4: if url.hostname == "raw.githubusercontent.com" and len(parts) >= 4:
return f"{repo}/contents/{'/'.join(parts[3:])}?ref={quote(parts[2], safe='')}" # Raw URLs omit the blob segment; share ref/path resolution below.
parts = [*parts[:2], "blob", *parts[2:]]
if len(parts) == 2: if len(parts) == 2:
return repo return repo
kind = parts[2] kind = parts[2]
+110
View File
@@ -7,7 +7,10 @@ import io
import json import json
from pathlib import Path from pathlib import Path
import shutil import shutil
import subprocess
import sys
import tempfile import tempfile
from textwrap import dedent
from threading import Thread from threading import Thread
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
@@ -35,6 +38,113 @@ def load_script(name):
PREPARE = load_script("prepare-website") PREPARE = load_script("prepare-website")
VERIFY = load_script("verify-website") VERIFY = load_script("verify-website")
WEBSITE = load_script("website") WEBSITE = load_script("website")
AUDIT = load_script("audit-website-links")
class WebsiteLinkAuditChecks(unittest.TestCase):
"""Check GitHub transport and URL classification without live network access."""
def test_github_json_is_decoded_as_utf8_under_a_legacy_locale(self):
# Exercise real subprocess pipe decoding with UTF-8 mode disabled. Only
# the external gh executable and the default locale are substituted.
program = dedent('''\
import importlib.util
import json
from pathlib import Path
import subprocess
import sys
from unittest.mock import patch
scripts = Path(sys.argv[1])
sys.path.insert(0, str(scripts))
spec = importlib.util.spec_from_file_location("audit", scripts / "audit-website-links.py")
audit = importlib.util.module_from_spec(spec)
spec.loader.exec_module(audit)
payload = json.dumps({"title": "Release \\u2603"}, ensure_ascii=False).encode("utf-8")
run = subprocess.run
def gh(command, **options):
assert command == ["gh", "api", "repos/example/project"]
return run([sys.executable, "-c", "import sys; sys.stdout.buffer.write(" + repr(payload) + ")"], **options)
with patch("locale.getencoding", return_value="cp936"), patch.object(audit.subprocess, "run", side_effect=gh):
print(json.dumps(audit.github_api("repos/example/project")))
''')
result = subprocess.run(
[sys.executable, "-X", "utf8=0", "-c", program, str(ROOT / "scripts")],
capture_output=True, encoding="utf-8", timeout=20,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(json.loads(result.stdout), {"title": "Release \u2603"})
def test_raw_and_browser_links_resolve_the_same_ref_and_encoded_path(self):
# Prefix lookalikes must not win, and tags use the same ref/path split.
references = {
"repos/example/project/git/matching-refs/heads/topic": [
{"ref": "refs/heads/topic"},
{"ref": "refs/heads/topic/nested"},
{"ref": "refs/heads/topic/nestedness"},
],
"repos/example/project/git/matching-refs/tags/topic": [
{"ref": "refs/tags/topic/nested/v1"},
],
"repos/example/project/git/matching-refs/heads/topic%2Fnested": [
{"ref": "refs/heads/topic/nested"},
{"ref": "refs/heads/topic/nestedness"},
],
"repos/example/project/git/matching-refs/tags/topic%2Fnested": [
{"ref": "refs/tags/topic/nested/v1"},
],
}
cases = (
("topic/nested/guide%20%23%3F.md", "guide%20%23%3F.md", "topic%2Fnested"),
("topic/nested/v1/README.md", "README.md", "topic%2Fnested%2Fv1"),
("topic%2Fnested/README.md", "README.md", "topic%2Fnested"),
)
for suffix, path, ref in cases:
endpoint = f"repos/example/project/contents/{path}?ref={ref}"
responses = {**references, endpoint: {"name": "README.md"}}
def github_api(target):
if target not in responses:
raise ValueError("gh: Not Found (HTTP 404)")
return responses[target]
for base in (
"https://raw.githubusercontent.com/example/project/",
"https://github.com/example/project/blob/",
"https://github.com/example/project/tree/",
):
with self.subTest(url=base + suffix), patch.object(AUDIT, "github_api", side_effect=github_api):
report = AUDIT.audit_url((base + suffix, ["fixture.html"]))
self.assertEqual(report["status"], "ok")
self.assertEqual(report["endpoint"], endpoint)
def test_plain_refs_and_pinned_commits_keep_their_contents_target(self):
for ref in ("main", "a" * 40):
responses = {
"repos/example/project/git/matching-refs/heads/main": [{"ref": "refs/heads/main"}],
"repos/example/project/git/matching-refs/tags/main": [],
f"repos/example/project/contents/README.md?ref={ref}": {"name": "README.md"},
}
for base in ("https://raw.githubusercontent.com/example/project/", "https://github.com/example/project/blob/"):
with self.subTest(ref=ref, base=base), patch.object(AUDIT, "github_api", side_effect=responses.__getitem__):
report = AUDIT.audit_url((base + ref + "/README.md", ["fixture.html"]))
self.assertEqual(report["status"], "ok")
self.assertEqual(report["endpoint"], f"repos/example/project/contents/README.md?ref={ref}")
def test_missing_contents_and_unavailable_github_probes_remain_distinct(self):
cases = (
(ValueError("gh: Not Found (HTTP 404)"), "broken"),
(ValueError("gh: Forbidden (HTTP 403)"), "unknown"),
(subprocess.TimeoutExpired(["gh", "api"], 20), "unknown"),
)
for failure, status in cases:
for base in ("https://raw.githubusercontent.com/example/project/", "https://github.com/example/project/blob/"):
with self.subTest(failure=str(failure), base=base), patch.object(AUDIT, "github_api", side_effect=failure):
report = AUDIT.audit_url((base + "a" * 40 + "/missing.md", ["fixture.html"]))
self.assertEqual(report["status"], status)
self.assertEqual(report["reason"], str(failure))
class WebsiteSearchChecks(unittest.TestCase): class WebsiteSearchChecks(unittest.TestCase):