Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

461 lines
16 KiB
Python

"""Tests for the check-author-identity pre-commit hook.
All test data uses throwaway addresses (``*.test`` / ``example.com``) or public
GitHub noreply addresses; no real personal email appears in this file.
"""
import importlib.util
import json
from pathlib import Path
import pytest
# Load the hook module by path (hyphenated filename isn't importable directly).
_HOOK_PATH = (
Path(__file__).resolve().parents[2]
/ ".pre-commit-hooks"
/ "check-author-identity.py"
)
_spec = importlib.util.spec_from_file_location(
"check_author_identity", _HOOK_PATH
)
mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(mod)
PYPROJECT = """\
[project]
name = "x"
authors = [
{name = "LearningCircuit", email = "185559241+LearningCircuit@users.noreply.github.com"},
{name = "djpetti", email = "djpetti@example.com"},
]
"""
DECLARED = {
"learningcircuit": {"185559241+learningcircuit@users.noreply.github.com"},
"djpetti": {"djpetti@example.com"},
}
class TestParseIdentities:
def test_parses_and_lowercases(self):
assert mod.parse_identities(PYPROJECT) == DECLARED
def test_key_order_independent(self):
text = (
'authors = [\n {email = "bob@example.com", name = "Bob"},\n]\n'
)
assert mod.parse_identities(text) == {"bob": {"bob@example.com"}}
def test_anchored_ignores_other_authors_keys(self):
text = (
'co_authors = ["x"]\n'
'authors = [\n {name = "A", email = "a@b.test"},\n]\n'
)
assert mod.parse_identities(text) == {"a": {"a@b.test"}}
def test_no_block_returns_empty(self):
assert mod.parse_identities('[project]\nname = "x"\n') == {}
def test_multiple_emails_per_name(self):
text = (
"authors = [\n"
' {name = "A", email = "a@x.test"},\n'
' {name = "A", email = "a2@x.test"},\n'
"]\n"
)
assert mod.parse_identities(text) == {"a": {"a@x.test", "a2@x.test"}}
class TestIsNoreply:
def test_user_noreply_allowed(self):
assert mod._is_noreply("123+user@users.noreply.github.com")
assert mod._is_noreply("user@users.noreply.github.com")
def test_personal_and_webflow_not_user_noreply(self):
assert not mod._is_noreply("user@example.com")
assert not mod._is_noreply("noreply@github.com") # web-flow committer
class TestMismatch:
def test_any_noreply_allowed_for_declared_author(self):
# djpetti is declared with a non-noreply email; his GH noreply must pass.
assert (
mod._mismatch(
"author",
"djpetti",
"7475340+djpetti@users.noreply.github.com",
DECLARED,
)
is None
)
def test_declared_email_allowed(self):
assert (
mod._mismatch("author", "djpetti", "djpetti@example.com", DECLARED)
is None
)
def test_declared_author_personal_email_flagged(self):
msg = mod._mismatch(
"author", "LearningCircuit", "personal@nope.test", DECLARED
)
assert msg is not None
assert "LearningCircuit" in msg
def test_case_insensitive_name_match(self):
# A lowercase display name must still be enforced (the hashedviking gap).
assert (
mod._mismatch(
"author", "learningcircuit", "personal@nope.test", DECLARED
)
is not None
)
def test_unknown_name_not_enforced(self):
assert (
mod._mismatch("author", "Outsider", "out@example.com", DECLARED)
is None
)
def test_message_never_contains_offending_email(self):
secret = "do-not-leak@secret.test"
msg = mod._mismatch("author", "LearningCircuit", secret, DECLARED)
assert msg is not None
assert secret not in msg
assert "secret" not in msg.lower()
def test_empty_declared_never_flags(self):
assert mod._mismatch("author", "Anyone", "x@y.test", {}) is None
class TestCoAuthorRegex:
def test_matches_trailer(self):
m = mod._CO_AUTHOR.search("b\n\nCo-authored-by: Some One <a@b.test>\n")
assert m is not None
assert m.group("name") == "Some One"
assert m.group("email") == "a@b.test"
def test_indented_trailer_matches(self):
assert (
mod._CO_AUTHOR.search(" Co-authored-by: X <x@y.test>") is not None
)
def test_no_false_match_on_prose(self):
assert mod._CO_AUTHOR.search("This was co-authored by someone") is None
class TestParseLog:
def test_basic_record(self):
raw = "sha1\x00An\x00ae@x.test\x00Cn\x00ce@x.test\x00body line\x1e"
assert mod._parse_log(raw) == [
("sha1", "An", "ae@x.test", "Cn", "ce@x.test", "body line")
]
def test_nul_in_body_is_preserved(self):
# A NUL earlier in the body must not truncate it: a real trailer (on its
# own line) after the NUL must survive and still be detected.
body = "head\x00more\n\nCo-authored-by: LearningCircuit <bad@nope.test>"
raw = f"sha1\x00An\x00a@x.test\x00Cn\x00c@x.test\x00{body}\x1e"
recs = mod._parse_log(raw)
assert (
recs[0][5] == body
) # full body kept (old fields[5] would truncate)
errs = mod._check(recs[0], DECLARED)
assert any("Co-authored-by" in e for e in errs)
class TestCheck:
@staticmethod
def _rec(an="A", ae="a@x.test", cn="C", ce="c@x.test", body=""):
return ("deadbeef0123", an, ae, cn, ce, body)
def test_author_violation(self):
errs = mod._check(
self._rec(an="LearningCircuit", ae="bad@nope.test"), DECLARED
)
assert len(errs) == 1
assert "author" in errs[0]
assert "deadbeef0" in errs[0]
def test_committer_violation(self):
errs = mod._check(
self._rec(cn="LearningCircuit", ce="bad@nope.test"), DECLARED
)
assert len(errs) == 1
assert "committer" in errs[0]
def test_co_author_trailer_violation(self):
errs = mod._check(
self._rec(
body="m\n\nCo-authored-by: LearningCircuit <bad@nope.test>\n"
),
DECLARED,
)
assert len(errs) == 1
assert "Co-authored-by" in errs[0]
def test_clean_noreply_passes(self):
good = "185559241+learningcircuit@users.noreply.github.com"
errs = mod._check(
self._rec(
an="LearningCircuit", ae=good, cn="LearningCircuit", ce=good
),
DECLARED,
)
assert errs == []
def test_external_contributor_passes(self):
assert (
mod._check(self._rec(an="Outsider", ae="out@example.com"), DECLARED)
== []
)
def test_no_offending_email_in_any_message(self):
secret = "leak-me@secret.test"
errs = mod._check(
self._rec(
an="LearningCircuit",
ae=secret,
body=f"x\n\nCo-authored-by: LearningCircuit <{secret}>\n",
),
DECLARED,
)
assert errs
assert all(secret not in e for e in errs)
@pytest.fixture
def clean_ci_env(monkeypatch):
"""Isolate the GitHub Actions env (set in real CI -> would flake otherwise)."""
monkeypatch.delenv("GITHUB_EVENT_PATH", raising=False)
monkeypatch.delenv("GITHUB_EVENT_NAME", raising=False)
return monkeypatch
class TestParseLogFailClosed:
def test_malformed_record_raises(self):
# An injected record separator splits a record into a <6-field chunk.
raw = "s\x00An\x00a@x.test\x00Cn\x00c@x.test\x00body\x1eINJECTED\x1e"
with pytest.raises(RuntimeError):
mod._parse_log(raw)
def test_multiple_records(self):
raw = (
"s1\x00A\x00a@x.test\x00C\x00c@x.test\x00b1\x1e"
"s2\x00A\x00a@x.test\x00C\x00c@x.test\x00b2\x1e"
)
assert [r[0] for r in mod._parse_log(raw)] == ["s1", "s2"]
class TestResolveMergeBase:
def test_resolves_locally_without_fetching(self, monkeypatch):
# With full history present, merge-base succeeds first try -> NO fetch
# (the hook must not mutate git state inside `pre-commit run`).
runs = []
monkeypatch.setattr(
mod.subprocess, "run", lambda *a, **k: runs.append(a[0])
)
monkeypatch.setattr(mod, "_git", lambda *a: (0, "abc1234\n"))
assert mod._resolve_merge_base("b", "h") == "abc1234"
assert runs == [] # read-only: no git fetch was issued
def test_fetches_then_succeeds(self, monkeypatch):
# Shallow clone: first merge-base fails, a depth fetch makes it resolve.
runs = []
monkeypatch.setattr(
mod.subprocess, "run", lambda *a, **k: runs.append(a[0])
)
seq = iter([(1, ""), (0, "abc1234\n")])
monkeypatch.setattr(mod, "_git", lambda *a: next(seq))
assert mod._resolve_merge_base("b", "h") == "abc1234"
assert any("--depth=1000" in r for r in runs)
assert not any("--unshallow" in r for r in runs)
def test_retries_unshallow_then_succeeds(self, monkeypatch):
runs = []
monkeypatch.setattr(
mod.subprocess, "run", lambda *a, **k: runs.append(a[0])
)
seq = iter([(1, ""), (1, ""), (0, "abc1234\n")])
monkeypatch.setattr(mod, "_git", lambda *a: next(seq))
assert mod._resolve_merge_base("b", "h") == "abc1234"
assert any("--unshallow" in r for r in runs) # full-history path taken
def test_raises_when_unresolvable(self, monkeypatch):
monkeypatch.setattr(mod.subprocess, "run", lambda *a, **k: None)
monkeypatch.setattr(mod, "_git", lambda *a: (1, ""))
with pytest.raises(RuntimeError):
mod._resolve_merge_base("b", "h")
class TestPrContext:
@staticmethod
def _event(tmp_path):
ev = tmp_path / "event.json"
ev.write_text(
json.dumps(
{
"pull_request": {
"base": {"sha": "BASE"},
"head": {"sha": "HEAD"},
}
}
)
)
return ev
def test_reads_allowlist_and_range_from_base(self, clean_ci_env, tmp_path):
mp = clean_ci_env
mp.setenv("GITHUB_EVENT_NAME", "pull_request")
mp.setenv("GITHUB_EVENT_PATH", str(self._event(tmp_path)))
mp.setattr(mod, "_resolve_merge_base", lambda b, h: "MB")
calls = []
def fake_git(*a):
calls.append(a)
if a[0] == "log":
return (0, "s\x00n\x00e@x.test\x00n\x00e@x.test\x00b\x1e")
if a[0] == "show":
return (0, PYPROJECT)
return (0, "")
mp.setattr(mod, "_git", fake_git)
records, text = mod._pr_context()
assert ("show", "BASE:pyproject.toml") in calls # base, not work-tree
assert any(c[0] == "log" and "MB..HEAD" in c[1] for c in calls)
assert text == PYPROJECT
assert records and records[0][0] == "s"
def test_base_pyproject_read_fails_closed(self, clean_ci_env, tmp_path):
mp = clean_ci_env
mp.setenv("GITHUB_EVENT_NAME", "pull_request")
mp.setenv("GITHUB_EVENT_PATH", str(self._event(tmp_path)))
mp.setattr(mod, "_resolve_merge_base", lambda b, h: "MB")
def fake_git(*a):
if a[0] == "log":
return (0, "s\x00n\x00e@x.test\x00n\x00e@x.test\x00b\x1e")
if a[0] == "show":
return (1, "") # base pyproject read fails
return (0, "")
mp.setattr(mod, "_git", fake_git)
with pytest.raises(RuntimeError):
mod._pr_context()
def test_non_pr_event_returns_none(self, clean_ci_env):
# No event + not a PR event -> local mode (None), not fail-closed.
assert mod._pr_context() is None
def test_pr_event_without_payload_fails_closed(self, clean_ci_env):
clean_ci_env.setenv("GITHUB_EVENT_NAME", "pull_request")
with pytest.raises(RuntimeError):
mod._pr_context()
class TestMain:
def test_fails_closed_on_unresolvable_range(self, monkeypatch, capsys):
def boom():
raise RuntimeError("could not resolve the PR commit range")
monkeypatch.setattr(mod, "_pr_context", boom)
assert mod.main() == 1
assert "failing closed" in capsys.readouterr().err
def test_violation_in_pr_range_returns_1(self, monkeypatch, capsys):
rec = (
"deadbeef0",
"LearningCircuit",
"bad@nope.test",
"C",
"c@x.test",
"",
)
monkeypatch.setattr(mod, "_pr_context", lambda: ([rec], PYPROJECT))
assert mod.main() == 1
err = capsys.readouterr().err
assert "LearningCircuit" in err
assert "bad@nope.test" not in err # offending email never printed
def test_clean_pr_range_returns_0(self, monkeypatch):
good = "185559241+learningcircuit@users.noreply.github.com"
rec = (
"deadbeef0",
"LearningCircuit",
good,
"LearningCircuit",
good,
"",
)
monkeypatch.setattr(mod, "_pr_context", lambda: ([rec], PYPROJECT))
assert mod.main() == 0
def test_no_declared_authors_returns_0(self, monkeypatch):
rec = (
"deadbeef0",
"LearningCircuit",
"bad@nope.test",
"C",
"c@x.test",
"",
)
monkeypatch.setattr(
mod, "_pr_context", lambda: ([rec], '[project]\nname = "x"\n')
)
assert mod.main() == 0
class TestPendingRecords:
def test_parses_git_idents(self, monkeypatch):
ident = "LearningCircuit <bad@nope.test> 1700000000 +0000"
monkeypatch.setattr(mod, "_git", lambda *a: (0, ident))
recs = mod._pending_records()
assert recs[0][1] == "LearningCircuit"
assert recs[0][2] == "bad@nope.test"
class TestAnchorBoundary:
def test_authors_prefixed_key_not_hijacked(self):
text = (
'authors_extra = [{name = "E", email = "evil@x.test"}]\n'
'authors = [\n {name = "A", email = "a@b.test"},\n]\n'
)
assert mod.parse_identities(text) == {"a": {"a@b.test"}}
class TestAllowlistFailClosed:
"""The guard must not silently disable itself if the authors block changes."""
def test_real_repo_pyproject_still_parses_to_nonempty(self):
# If a future reformat of the real pyproject.toml breaks parsing, this
# trips here (and the CI path fails closed) instead of silently passing.
root = mod.Path(__file__).resolve().parents[2]
declared = mod.parse_identities(
(root / "pyproject.toml").read_text(encoding="utf-8")
)
assert declared, (
"pyproject.toml authors no longer parse -> guard disabled"
)
assert "learningcircuit" in declared
assert any(
e.endswith("@users.noreply.github.com")
for e in declared["learningcircuit"]
)
def test_ci_fails_closed_when_authors_declared_but_unparsed(
self, monkeypatch
):
# array-of-tables form parse_identities does not understand: authors ARE
# declared but none parse -> fail closed rather than return 0.
unparsable = '[[project.authors]]\nname = "X"\nemail = "x@y.test"\n'
monkeypatch.setattr(mod, "_pr_context", lambda: ([], unparsable))
assert mod.main() == 1
def test_ci_passes_when_no_authors_block_at_all(self, monkeypatch):
# genuinely no authors declared -> not enforced -> pass (no over-block).
monkeypatch.setattr(
mod, "_pr_context", lambda: ([], '[project]\nname = "x"\n')
)
assert mod.main() == 0