diff --git a/tests/test_hooks.py b/tests/test_hooks.py index 43943df1a..e672459b5 100644 --- a/tests/test_hooks.py +++ b/tests/test_hooks.py @@ -2,6 +2,7 @@ import os import shutil import subprocess +import textwrap from types import SimpleNamespace from pathlib import Path import pytest @@ -332,6 +333,56 @@ def test_rebuild_bodies_with_graphify_root_are_valid_python(): ast.parse(body) +def _extract_root_resolution(body: str) -> str: + """Pull the `.graphify_root` -> `_root` snippet out of a rebuild body, so a + test can execute the shipped logic itself rather than a hand copy that could + quietly drift from it.""" + match = re.search(r"( _root = Path\('\.'\).*?)\n _rebuild_code\(", body, re.DOTALL) + assert match, "root resolution snippet not found" + return textwrap.dedent(match.group(1)) + + +@pytest.mark.parametrize( + "name,body", + [("post-commit", _REBUILD_BODY_COMMIT), ("post-checkout", _REBUILD_BODY_CHECKOUT)], +) +def test_rebuild_bodies_reject_an_out_of_repo_graphify_root(name, body, tmp_path, monkeypatch): + """#3265: `.graphify_root` sits inside graphify-out/, a directory the + documented team workflow says to commit, so its contents are checkout + controlled. Without a bound, a value planted there by a malicious fork or PR + (an absolute path outside the repository) would steer the rebuild -- and so + what gets read, and what gets written into the same committed graphify-out/ + -- to wherever the checkout names, not the repository the hook was installed + into. The recovered root must stay inside the working tree the hook actually + runs from.""" + repo = tmp_path / "repo" + outside = tmp_path / "outside" + (repo / "graphify-out").mkdir(parents=True) + outside.mkdir() + (repo / "graphify-out" / ".graphify_root").write_text(str(outside), encoding="utf-8") + monkeypatch.chdir(repo) + ns = {"Path": Path, "os": os} + exec(compile(_extract_root_resolution(body), "", "exec"), ns) + assert ns["_root"].resolve() == repo.resolve(), f"{name} honoured an out-of-repo root" + + +@pytest.mark.parametrize( + "name,body", + [("post-commit", _REBUILD_BODY_COMMIT), ("post-checkout", _REBUILD_BODY_CHECKOUT)], +) +def test_rebuild_bodies_honour_an_in_repo_graphify_root(name, body, tmp_path, monkeypatch): + """The legitimate case the #3265 guard must not break: a subdirectory-scoped + root (#1173) is still recovered.""" + repo = tmp_path / "repo" + (repo / "graphify-out").mkdir(parents=True) + (repo / "backend").mkdir() + (repo / "graphify-out" / ".graphify_root").write_text("backend", encoding="utf-8") + monkeypatch.chdir(repo) + ns = {"Path": Path, "os": os} + exec(compile(_extract_root_resolution(body), "", "exec"), ns) + assert ns["_root"].resolve() == (repo / "backend").resolve(), f"{name} lost the scoped root" + + @pytest.mark.parametrize( "name,body", [("post-commit", _REBUILD_BODY_COMMIT), ("post-checkout", _REBUILD_BODY_CHECKOUT)],