import shutil
import subprocess
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
from subprocess_utils import (
ExecutableNotFoundError,
check_git_history,
check_git_repo,
get_git_commit_hash,
get_git_remote_url,
get_safe_executable,
run_cargo_command,
run_git_command,
run_git_command_with_input,
run_safe_command,
)
class TestGetSafeExecutable:
@pytest.mark.parametrize("command", ["echo", "git", "ls"])
def test_finds_existing_executables(self, command):
result = get_safe_executable(command)
assert isinstance(result, str)
assert len(result) > 0
assert Path(result).name.startswith(command) assert Path(result).is_absolute()
if sys.platform.startswith("win") and command in {"ls", "echo"}:
pytest.skip(f"{command} may not be an external executable on Windows")
@pytest.mark.parametrize("fake_command", ["definitely-nonexistent-command-xyz", "fake-command-for-testing", "nonexistent123"])
def test_raises_on_nonexistent_executables(self, fake_command):
with pytest.raises(ExecutableNotFoundError, match="not found in PATH") as exc_info:
get_safe_executable(fake_command)
assert fake_command in str(exc_info.value)
class TestRunGitCommand:
def test_git_version(self):
result = run_git_command(["--version"])
assert result.returncode == 0
assert "git version" in result.stdout.lower()
assert isinstance(result.stdout, str)
def test_git_command_with_custom_params(self):
result = run_git_command(["status", "--porcelain"], check=False)
assert isinstance(result.returncode, int)
assert isinstance(result.stdout, str)
def test_git_command_failure_handling(self):
with pytest.raises(subprocess.CalledProcessError):
run_git_command(["invalid-git-subcommand-xyz"], check=True)
def test_git_command_no_failure_with_check_false(self):
result = run_git_command(["invalid-git-subcommand-xyz"], check=False)
assert result.returncode != 0
assert isinstance(result.stdout, str)
class TestRunCargoCommand:
@pytest.mark.skipif(shutil.which("cargo") is None, reason="cargo not installed in PATH")
def test_cargo_version(self):
result = run_cargo_command(["--version"])
assert result.returncode == 0
assert "cargo" in result.stdout.lower()
assert isinstance(result.stdout, str)
@pytest.mark.skipif(shutil.which("cargo") is None, reason="cargo not installed in PATH")
def test_cargo_command_with_custom_params(self):
result = run_cargo_command(["check", "--dry-run"], check=False)
assert isinstance(result.returncode, int)
assert isinstance(result.stdout, str)
class TestRunSafeCommand:
def test_basic_command_execution(self):
result = run_safe_command("echo", ["hello world"])
assert result.returncode == 0
assert result.stdout.strip() == "hello world"
assert isinstance(result.stdout, str)
def test_secure_defaults_are_applied(self):
result = run_safe_command("echo", ["test"])
assert isinstance(result.stdout, str)
assert result.stdout.strip() == "test"
def test_text_parameter_enforced(self):
result = run_safe_command("echo", ["test output"], text=False) assert isinstance(result.stdout, str) assert "test output" in result.stdout
def test_custom_check_parameter(self):
result = run_safe_command("git", ["invalid-git-subcommand-xyz"], check=False)
assert result.returncode != 0
def test_custom_capture_output_parameter(self):
if sys.platform.startswith("win"):
pytest.skip("echo may not be an external executable on Windows")
result = run_safe_command("echo", ["no capture"], capture_output=False)
assert result.stdout is None
def test_multiple_custom_parameters(self):
result = run_safe_command("echo", ["multi param test"], text=False, check=False, capture_output=True)
assert isinstance(result.stdout, str) assert result.returncode == 0
assert "multi param test" in result.stdout
def test_nonexistent_command_raises_error(self):
with pytest.raises(ExecutableNotFoundError):
run_safe_command("definitely-nonexistent-command", ["arg"])
def test_additional_kwargs_passed_through(self):
result = run_safe_command("echo", ["timeout test"], timeout=10)
assert result.returncode == 0
assert "timeout test" in result.stdout
class TestGitRepositoryFunctions:
def test_check_git_repo_in_git_repo(self):
if not check_git_repo():
pytest.skip("Not running inside a git repository")
assert check_git_repo() is True
def test_check_git_history_with_history(self):
if not check_git_history():
pytest.skip("Repository has no commit history")
assert check_git_history() is True
def test_get_git_commit_hash_returns_hash(self):
commit_hash = get_git_commit_hash()
assert isinstance(commit_hash, str)
assert len(commit_hash) >= 7 assert all(c in "0123456789abcdef" for c in commit_hash.lower())
def test_get_git_remote_url_returns_url(self):
remotes = run_git_command(["remote"]).stdout.split()
if "origin" not in remotes:
pytest.skip("No 'origin' remote configured")
remote_url = get_git_remote_url("origin")
assert isinstance(remote_url, str)
assert len(remote_url) > 0
assert any(remote_url.startswith(prefix) for prefix in ["https://", "git@", "ssh://"])
class TestErrorHandling:
def test_executable_not_found_error_attributes(self):
error = ExecutableNotFoundError("test message")
assert str(error) == "test message"
assert isinstance(error, Exception)
def test_git_functions_handle_missing_git(self, monkeypatch):
def mock_get_safe_executable(command):
if command == "git":
raise ExecutableNotFoundError(f"Required executable '{command}' not found in PATH")
return "/bin/echo"
monkeypatch.setattr("subprocess_utils.get_safe_executable", mock_get_safe_executable)
assert check_git_repo() is False
assert check_git_history() is False
with pytest.raises(ExecutableNotFoundError):
get_git_commit_hash()
with pytest.raises(ExecutableNotFoundError):
get_git_remote_url()
class TestSecurityFeatures:
def test_uses_full_executable_paths(self):
git_path = get_safe_executable("git")
assert Path(git_path).is_absolute() assert "git" in git_path
def test_no_shell_execution(self):
result = run_safe_command("echo", ["$HOME"]) assert result.stdout.strip() == "$HOME"
def test_check_parameter_security_default(self):
with pytest.raises(subprocess.CalledProcessError):
run_safe_command("git", ["invalid-git-subcommand-xyz"])
@pytest.mark.parametrize(
("function", "args", "kwargs"),
[
(run_git_command, (["status"],), {"executable": "/malicious/fake/git"}),
(run_cargo_command, (["--version"],), {"executable": "/malicious/fake/cargo"}),
(run_safe_command, ("echo", ["test"]), {"executable": "/malicious/fake/command"}),
],
)
def test_rejects_executable_override(self, function, args, kwargs, monkeypatch):
called = {"run": False}
def fake_run(*_a, **_k):
called["run"] = True msg = "subprocess.run should not be called on override"
raise AssertionError(msg)
monkeypatch.setattr("subprocess.run", fake_run)
with pytest.raises(ValueError, match="Overriding 'executable' is not allowed"):
function(*args, **kwargs)
assert called["run"] is False
def test_run_git_command_with_input_rejects_executable_override(self):
with pytest.raises(ValueError, match="Overriding 'executable' is not allowed"):
run_git_command_with_input(["hash-object", "--stdin"], "test content", executable="/malicious/fake/git")
if __name__ == "__main__":
pytest.main([__file__, "-v"])