chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
"""Integration tests exercising git-dependent code with real temporary repos.
|
||||
|
||||
Tests cover:
|
||||
- get_changed_files with real git history
|
||||
- parse_git_diff_ranges with real diffs
|
||||
- incremental_update detecting real file modifications
|
||||
- base ref injection rejection
|
||||
- wiki page path traversal protection
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from code_review_graph.changes import parse_git_diff_ranges
|
||||
from code_review_graph.graph import GraphStore
|
||||
from code_review_graph.incremental import (
|
||||
collect_all_files,
|
||||
full_build,
|
||||
get_all_tracked_files,
|
||||
get_changed_files,
|
||||
incremental_update,
|
||||
)
|
||||
from code_review_graph.wiki import get_wiki_page
|
||||
|
||||
|
||||
def _git(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
"""Run a git command inside *repo* and return the result."""
|
||||
return subprocess.run(
|
||||
["git", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=str(repo),
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def git_repo(tmp_path: Path) -> Path:
|
||||
"""Create a real git repo with two commits.
|
||||
|
||||
Commit 1 adds ``hello.py`` with a single function.
|
||||
Commit 2 modifies ``hello.py`` (adds a second function).
|
||||
"""
|
||||
repo = tmp_path / "repo"
|
||||
repo.mkdir()
|
||||
|
||||
_git(repo, "init")
|
||||
_git(repo, "config", "user.email", "test@test.com")
|
||||
_git(repo, "config", "user.name", "Test")
|
||||
|
||||
# First commit
|
||||
py_file = repo / "hello.py"
|
||||
py_file.write_text("def greet():\n return 'hello'\n")
|
||||
_git(repo, "add", "hello.py")
|
||||
_git(repo, "commit", "-m", "initial commit")
|
||||
|
||||
# Second commit — modify the file
|
||||
py_file.write_text(
|
||||
"def greet():\n return 'hello'\n\n"
|
||||
"def farewell():\n return 'goodbye'\n"
|
||||
)
|
||||
_git(repo, "add", "hello.py")
|
||||
_git(repo, "commit", "-m", "add farewell function")
|
||||
|
||||
return repo
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. get_changed_files with a real git repo
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_get_changed_files_real_git(git_repo: Path) -> None:
|
||||
"""get_changed_files should list hello.py as changed between HEAD~1..HEAD."""
|
||||
changed = get_changed_files(git_repo, base="HEAD~1")
|
||||
assert "hello.py" in changed
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. parse_git_diff_ranges with a real git repo
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_parse_git_diff_ranges_real_git(git_repo: Path) -> None:
|
||||
"""parse_git_diff_ranges should return non-empty line ranges for hello.py."""
|
||||
ranges = parse_git_diff_ranges(str(git_repo), base="HEAD~1")
|
||||
assert "hello.py" in ranges
|
||||
assert len(ranges["hello.py"]) > 0
|
||||
# Each entry is a (start, end) tuple with positive line numbers
|
||||
for start, end in ranges["hello.py"]:
|
||||
assert start >= 1
|
||||
assert end >= start
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. incremental_update detects real modifications
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_incremental_update_real_git(git_repo: Path) -> None:
|
||||
"""Full build then incremental update should detect the second commit."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
||||
db_path = f.name
|
||||
|
||||
try:
|
||||
store = GraphStore(db_path)
|
||||
|
||||
# Reset to first commit, do a full build
|
||||
_git(git_repo, "checkout", "HEAD~1", "--detach")
|
||||
full_build(git_repo, store)
|
||||
initial_nodes = store.get_stats().total_nodes
|
||||
assert initial_nodes > 0, "full_build should create at least one node"
|
||||
|
||||
# Move back to tip (second commit) and do incremental update
|
||||
_git(git_repo, "checkout", "-")
|
||||
result = incremental_update(
|
||||
git_repo, store, changed_files=["hello.py"]
|
||||
)
|
||||
assert result["files_updated"] >= 1
|
||||
assert "hello.py" in result["changed_files"]
|
||||
|
||||
# The graph should now contain more nodes (farewell function added)
|
||||
assert store.get_stats().total_nodes >= initial_nodes
|
||||
|
||||
store.close()
|
||||
finally:
|
||||
Path(db_path).unlink(missing_ok=True)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. base ref injection is rejected
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_base_validation_rejects_injection(git_repo: Path) -> None:
|
||||
"""Passing a malicious --flag as base should be rejected (empty list)."""
|
||||
result = get_changed_files(git_repo, base="--output=/tmp/evil")
|
||||
assert result == []
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 5. wiki page path traversal is blocked
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def git_repo_with_submodule(tmp_path: Path) -> Path:
|
||||
"""Create a parent repo containing a git submodule with a Python file."""
|
||||
# Create the "library" repo that will become a submodule
|
||||
lib_repo = tmp_path / "lib"
|
||||
lib_repo.mkdir()
|
||||
_git(lib_repo, "init")
|
||||
_git(lib_repo, "config", "user.email", "test@test.com")
|
||||
_git(lib_repo, "config", "user.name", "Test")
|
||||
(lib_repo / "util.py").write_text("def helper():\n pass\n")
|
||||
_git(lib_repo, "add", "util.py")
|
||||
_git(lib_repo, "commit", "-m", "lib initial")
|
||||
|
||||
# Create the parent repo and add lib as a submodule
|
||||
parent = tmp_path / "parent"
|
||||
parent.mkdir()
|
||||
_git(parent, "init")
|
||||
_git(parent, "config", "user.email", "test@test.com")
|
||||
_git(parent, "config", "user.name", "Test")
|
||||
(parent / "main.py").write_text("def main():\n pass\n")
|
||||
_git(parent, "add", "main.py")
|
||||
_git(parent, "commit", "-m", "parent initial")
|
||||
_git(
|
||||
parent, "-c", "protocol.file.allow=always",
|
||||
"submodule", "add", str(lib_repo), "lib",
|
||||
)
|
||||
_git(parent, "commit", "-m", "add lib submodule")
|
||||
|
||||
return parent
|
||||
|
||||
|
||||
def test_get_all_tracked_files_without_recurse(
|
||||
git_repo_with_submodule: Path,
|
||||
) -> None:
|
||||
"""Without recurse_submodules, submodule files are NOT listed."""
|
||||
files = get_all_tracked_files(
|
||||
git_repo_with_submodule, recurse_submodules=False
|
||||
)
|
||||
assert "main.py" in files
|
||||
# Submodule entry appears as a gitlink, not as individual files
|
||||
assert not any(f.startswith("lib/") for f in files)
|
||||
|
||||
|
||||
def test_get_all_tracked_files_with_recurse(
|
||||
git_repo_with_submodule: Path,
|
||||
) -> None:
|
||||
"""With recurse_submodules=True, submodule files ARE listed."""
|
||||
files = get_all_tracked_files(
|
||||
git_repo_with_submodule, recurse_submodules=True
|
||||
)
|
||||
assert "main.py" in files
|
||||
assert "lib/util.py" in files
|
||||
|
||||
|
||||
def test_collect_all_files_with_recurse(
|
||||
git_repo_with_submodule: Path,
|
||||
) -> None:
|
||||
"""collect_all_files with recurse_submodules includes submodule code."""
|
||||
files = collect_all_files(
|
||||
git_repo_with_submodule, recurse_submodules=True
|
||||
)
|
||||
assert "main.py" in files
|
||||
assert "lib/util.py" in files
|
||||
|
||||
|
||||
def test_full_build_with_recurse_submodules(
|
||||
git_repo_with_submodule: Path,
|
||||
) -> None:
|
||||
"""full_build with recurse_submodules parses submodule files."""
|
||||
db_path = git_repo_with_submodule / ".code-review-graph" / "graph.db"
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
store = GraphStore(db_path)
|
||||
try:
|
||||
result = full_build(
|
||||
git_repo_with_submodule, store, recurse_submodules=True
|
||||
)
|
||||
assert result["files_parsed"] >= 2 # main.py + lib/util.py
|
||||
assert result["errors"] == []
|
||||
|
||||
# Verify both parent and submodule nodes exist
|
||||
parent_nodes = store.get_nodes_by_file(
|
||||
str(git_repo_with_submodule / "main.py")
|
||||
)
|
||||
sub_nodes = store.get_nodes_by_file(
|
||||
str(git_repo_with_submodule / "lib" / "util.py")
|
||||
)
|
||||
assert len(parent_nodes) > 0
|
||||
assert len(sub_nodes) > 0
|
||||
finally:
|
||||
store.close()
|
||||
|
||||
|
||||
def test_wiki_page_path_traversal_blocked(tmp_path: Path) -> None:
|
||||
"""get_wiki_page must not serve files outside the wiki directory."""
|
||||
wiki_dir = tmp_path / "wiki"
|
||||
wiki_dir.mkdir()
|
||||
|
||||
# Create a legitimate page
|
||||
(wiki_dir / "my-module.md").write_text("# My Module\n")
|
||||
|
||||
# Attempt a path traversal — should return None
|
||||
result = get_wiki_page(str(wiki_dir), "../../etc/passwd")
|
||||
assert result is None
|
||||
Reference in New Issue
Block a user