diff --git a/tests/test_hooks.py b/tests/test_hooks.py new file mode 100644 index 0000000..9f6016d --- /dev/null +++ b/tests/test_hooks.py @@ -0,0 +1,234 @@ +"""Tests for git hooks module.""" + +import os +import tempfile +from pathlib import Path + +from src.hooks import ( + HookManager, + is_hook_mode, +) + + +class TestHookManager: + """Tests for HookManager class.""" + + def test_init_with_repo_path(self): + """Test initialization with repository path.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + assert manager.repo_path == Path(tmpdir) + assert manager.hooks_dir == Path(tmpdir) / ".git" / "hooks" + + def test_get_hook_path(self): + """Test getting hook file path.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + expected = Path(tmpdir) / ".git" / "hooks" / "prepare-commit-msg" + assert manager.get_hook_path() == expected + + def test_hook_exists_false_when_no_file(self): + """Test hook_exists returns False when no hook file.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + assert manager.hook_exists() is False + + def test_hook_exists_true_when_file_exists(self): + """Test hook_exists returns True when hook file exists.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + hook_file = hooks_dir / "prepare-commit-msg" + hook_file.write_text("# existing hook") + + assert manager.hook_exists() is True + + def test_has_our_hook_false_when_no_file(self): + """Test has_our_hook returns False when no hook.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + assert manager.has_our_hook() is False + + def test_has_our_hook_true_when_our_hook(self): + """Test has_our_hook returns True when our hook is installed.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + hook_file = hooks_dir / "prepare-commit-msg" + hook_file.write_text("# commit-gen hook") + + assert manager.has_our_hook() is True + + def test_has_our_hook_false_when_other_hook(self): + """Test has_our_hook returns False for other hooks.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + hook_file = hooks_dir / "prepare-commit-msg" + hook_file.write_text("# other hook") + + assert manager.has_our_hook() is False + + def test_backup_existing_hook(self): + """Test backing up existing hook.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + hook_file = hooks_dir / "prepare-commit-msg" + hook_file.write_text("# existing hook") + + backup = manager.backup_existing_hook() + + assert backup is not None + assert backup.exists() + assert backup.read_text() == "# existing hook" + + def test_backup_existing_hook_no_file(self): + """Test backup returns None when no file.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + backup = manager.backup_existing_hook() + assert backup is None + + def test_install_hook_success(self): + """Test successful hook installation.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + + result = manager.install_hook() + + assert result.success is True + assert manager.has_our_hook() is True + + def test_install_hook_creates_executable(self): + """Test installed hook is executable.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + + manager.install_hook() + + hook_file = manager.get_hook_path() + mode = os.stat(hook_file).st_mode + assert mode & 0o111 + + def test_install_hook_backs_up_existing(self): + """Test hook installation backs up existing hook.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + hook_file = hooks_dir / "prepare-commit-msg" + hook_file.write_text("# existing hook") + + result = manager.install_hook() + + assert result.success is True + assert result.backup_path is not None + + def test_install_hook_no_backup_when_our_hook(self): + """Test no backup when reinstalling our hook.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + hook_file = hooks_dir / "prepare-commit-msg" + hook_file.write_text("# commit-gen hook") + + result = manager.install_hook() + + assert result.success is True + assert result.backup_path is None + + def test_install_hook_no_hooks_dir(self): + """Test install fails when .git/hooks doesn't exist.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + + result = manager.install_hook() + + assert result.success is False + assert "not found" in result.message + + def test_uninstall_hook_success(self): + """Test successful hook uninstallation.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + hook_file = hooks_dir / "prepare-commit-msg" + hook_file.write_text("# commit-gen hook") + + result = manager.uninstall_hook() + + assert result.success is True + assert not manager.has_our_hook() + + def test_uninstall_hook_no_our_hook(self): + """Test uninstall fails when our hook isn't installed.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + hook_file = hooks_dir / "prepare-commit-msg" + hook_file.write_text("# other hook") + + result = manager.uninstall_hook() + + assert result.success is False + + def test_uninstall_hook_no_file(self): + """Test uninstall fails when no hook file.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + + result = manager.uninstall_hook() + + assert result.success is False + + def test_restore_hook_success(self): + """Test restoring backed up hook.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + hooks_dir = Path(tmpdir) / ".git" / "hooks" + hooks_dir.mkdir(parents=True) + hook_file = hooks_dir / "prepare-commit-msg" + backup_file = hooks_dir / "prepare-commit-msg.backup" + backup_file.write_text("# original hook") + + result = manager.restore_hook() + + assert result.success is True + assert hook_file.exists() + assert hook_file.read_text() == "# original hook" + + def test_restore_hook_no_backup(self): + """Test restore fails when no backup exists.""" + with tempfile.TemporaryDirectory() as tmpdir: + manager = HookManager(tmpdir) + + result = manager.restore_hook() + + assert result.success is False + + +class TestIsHookMode: + """Tests for is_hook_mode function.""" + + def test_hook_mode_true(self): + """Test detecting hook mode.""" + assert is_hook_mode(["hook"]) is True + assert is_hook_mode(["hook", "msgfile", "source"]) is True + + def test_hook_mode_false(self): + """Test not hook mode.""" + assert is_hook_mode([]) is False + assert is_hook_mode(["generate"]) is False + assert is_hook_mode(["--help"]) is False