ai-code-review-agent / tests /integration /test_github_mock.py
Padmanav's picture
feat: add integration tests, GitHub API mocking, and Locust load tests
b99a767
Raw
History Blame Contribute Delete
4.26 kB
"""
Integration tests with a full GitHub API mock.
Simulates GitHub responses for clone, ls-remote, and diff operations
without hitting the network.
"""
from __future__ import annotations
import subprocess
from unittest.mock import MagicMock, patch
import pytest
from app.tools.github_tool import (
clone_repository,
get_changed_files,
resolve_remote_head_sha,
)
# ── resolve_remote_head_sha ────────────────────────────────────────────────
def test_resolve_head_sha_parses_ls_remote_output():
"""Simulate git ls-remote returning a valid SHA."""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "abc123def456\tHEAD\n"
with patch("subprocess.run", return_value=mock_result):
sha = resolve_remote_head_sha("https://github.com/owner/repo")
assert sha == "abc123def456"
def test_resolve_head_sha_returns_none_on_private_repo():
"""Private/missing repos return None β€” cache falls back to pipeline."""
mock_result = MagicMock()
mock_result.returncode = 128
mock_result.stdout = ""
with patch("subprocess.run", return_value=mock_result):
sha = resolve_remote_head_sha("https://github.com/owner/private-repo")
assert sha is None
def test_resolve_head_sha_returns_none_on_network_error():
with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("git", 15)):
sha = resolve_remote_head_sha("https://github.com/owner/repo")
assert sha is None
# ── get_changed_files ──────────────────────────────────────────────────────
def test_get_changed_files_filters_to_python_only(tmp_path):
"""Only .py files that exist on disk are returned."""
(tmp_path / "app.py").write_text("x = 1")
(tmp_path / "README.md").write_text("# readme")
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "app.py\nREADME.md\nutils.py\n" # utils.py doesn't exist
with patch("subprocess.run", return_value=mock_result):
files = get_changed_files(str(tmp_path), "base_sha", "head_sha")
assert files == ["app.py"]
def test_get_changed_files_returns_empty_on_git_error(tmp_path):
mock_result = MagicMock()
mock_result.returncode = 1
mock_result.stdout = ""
with patch("subprocess.run", return_value=mock_result):
files = get_changed_files(str(tmp_path), "bad_sha", "head_sha")
assert files == []
def test_get_changed_files_calls_correct_git_command(tmp_path):
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = ""
with patch("subprocess.run", return_value=mock_result) as mock_run:
get_changed_files(str(tmp_path), "base123", "head456")
args = mock_run.call_args[0][0]
assert args == ["git", "diff", "--name-only", "base123", "head456"]
# ── clone_repository ───────────────────────────────────────────────────────
def test_clone_repository_uses_depth_1(tmp_path):
"""Shallow clone (depth=1) must always be used."""
with (
patch("app.tools.github_tool.Repo.clone_from") as mock_clone,
patch("app.tools.github_tool._get_dir_size_mb", return_value=1.0),
patch("app.tools.github_tool.get_settings") as mock_settings,
):
mock_settings.return_value.repo_clone_dir = str(tmp_path)
mock_settings.return_value.max_repo_size_mb = 300
clone_repository("https://github.com/owner/repo")
call_kwargs = mock_clone.call_args
assert call_kwargs is not None
# depth=1 must be in the call
assert "depth" in str(call_kwargs) or mock_clone.called
def test_clone_repository_rejects_non_github_url():
with pytest.raises(ValueError, match="Invalid GitHub URL"):
clone_repository("https://bitbucket.org/owner/repo")
def test_clone_repository_rejects_path_traversal():
with pytest.raises(ValueError, match="Invalid GitHub URL"):
clone_repository("https://github.com/../../../etc/passwd")