Files
keyhunter/cmd/hook_test.go
salvacybersec aa8daf8de2 feat(07-05): implement keyhunter hook install/uninstall with embedded pre-commit script
- cmd/hook.go: install/uninstall subcommands with --force flag
- cmd/hook_script.sh: embedded via go:embed, runs keyhunter scan on staged files
- KEYHUNTER-HOOK v1 marker prevents accidental deletion of non-owned hooks
- Backup existing hooks on --force install
- cmd/hook_test.go: 10 tests covering fresh install, non-repo, force/backup, overwrite, uninstall lifecycle
- Remove hookCmd stub from cmd/stubs.go
2026-04-05 23:58:44 +03:00

185 lines
4.9 KiB
Go

package cmd
import (
"os"
"path/filepath"
"strings"
"testing"
)
func setupGitRepo(t *testing.T) string {
t.Helper()
tmp := t.TempDir()
t.Chdir(tmp)
if err := os.MkdirAll(filepath.Join(tmp, ".git", "hooks"), 0o755); err != nil {
t.Fatalf("mkdir .git/hooks: %v", err)
}
// Reset force flag between tests.
hookForce = false
return tmp
}
func TestHookInstall_FreshRepo(t *testing.T) {
tmp := setupGitRepo(t)
if err := runHookInstall(hookInstallCmd, nil); err != nil {
t.Fatalf("install: %v", err)
}
p := filepath.Join(tmp, ".git", "hooks", "pre-commit")
info, err := os.Stat(p)
if err != nil {
t.Fatalf("stat: %v", err)
}
if info.Mode().Perm() != 0o755 {
t.Errorf("mode = %v, want 0o755", info.Mode().Perm())
}
data, _ := os.ReadFile(p)
if !strings.Contains(string(data), hookMarker) {
t.Errorf("hook missing marker %q", hookMarker)
}
}
func TestHookInstall_NotAGitRepo(t *testing.T) {
tmp := t.TempDir()
t.Chdir(tmp)
hookForce = false
err := runHookInstall(hookInstallCmd, nil)
if err == nil || !strings.Contains(err.Error(), "not a git repository") {
t.Fatalf("want not-a-git-repo error, got %v", err)
}
}
func TestHookInstall_ExistingNonKeyhunterRefuses(t *testing.T) {
tmp := setupGitRepo(t)
p := filepath.Join(tmp, ".git", "hooks", "pre-commit")
original := "# my hook\n"
if err := os.WriteFile(p, []byte(original), 0o755); err != nil {
t.Fatal(err)
}
err := runHookInstall(hookInstallCmd, nil)
if err == nil {
t.Fatal("expected error, got nil")
}
data, _ := os.ReadFile(p)
if string(data) != original {
t.Errorf("file changed; got %q", string(data))
}
}
func TestHookInstall_ForceBackupsExisting(t *testing.T) {
tmp := setupGitRepo(t)
p := filepath.Join(tmp, ".git", "hooks", "pre-commit")
original := "# my hook\n"
if err := os.WriteFile(p, []byte(original), 0o755); err != nil {
t.Fatal(err)
}
hookForce = true
defer func() { hookForce = false }()
if err := runHookInstall(hookInstallCmd, nil); err != nil {
t.Fatalf("install --force: %v", err)
}
// backup file should exist
entries, _ := os.ReadDir(filepath.Join(tmp, ".git", "hooks"))
foundBackup := false
for _, e := range entries {
if strings.HasPrefix(e.Name(), "pre-commit.bak.") {
foundBackup = true
data, _ := os.ReadFile(filepath.Join(tmp, ".git", "hooks", e.Name()))
if string(data) != original {
t.Errorf("backup content mismatch: %q", string(data))
}
}
}
if !foundBackup {
t.Error("no backup file found")
}
data, _ := os.ReadFile(p)
if !strings.Contains(string(data), hookMarker) {
t.Error("new hook missing marker")
}
}
func TestHookInstall_ExistingKeyhunterOverwrites(t *testing.T) {
tmp := setupGitRepo(t)
p := filepath.Join(tmp, ".git", "hooks", "pre-commit")
stale := "#!/bin/sh\n# " + hookMarker + " stale\n"
if err := os.WriteFile(p, []byte(stale), 0o755); err != nil {
t.Fatal(err)
}
if err := runHookInstall(hookInstallCmd, nil); err != nil {
t.Fatalf("install: %v", err)
}
data, _ := os.ReadFile(p)
if string(data) == stale {
t.Error("hook not updated")
}
if !strings.Contains(string(data), hookMarker) {
t.Error("updated hook missing marker")
}
}
func TestHookUninstall_RemovesKeyhunter(t *testing.T) {
tmp := setupGitRepo(t)
if err := runHookInstall(hookInstallCmd, nil); err != nil {
t.Fatal(err)
}
if err := runHookUninstall(hookUninstallCmd, nil); err != nil {
t.Fatalf("uninstall: %v", err)
}
p := filepath.Join(tmp, ".git", "hooks", "pre-commit")
if _, err := os.Stat(p); !os.IsNotExist(err) {
t.Errorf("file still exists: %v", err)
}
}
func TestHookUninstall_RefusesForeign(t *testing.T) {
tmp := setupGitRepo(t)
p := filepath.Join(tmp, ".git", "hooks", "pre-commit")
original := "#!/bin/sh\n# foreign\n"
if err := os.WriteFile(p, []byte(original), 0o755); err != nil {
t.Fatal(err)
}
err := runHookUninstall(hookUninstallCmd, nil)
if err == nil {
t.Fatal("expected error")
}
data, _ := os.ReadFile(p)
if string(data) != original {
t.Error("file was modified")
}
}
func TestHookUninstall_Force(t *testing.T) {
tmp := setupGitRepo(t)
p := filepath.Join(tmp, ".git", "hooks", "pre-commit")
if err := os.WriteFile(p, []byte("# foreign\n"), 0o755); err != nil {
t.Fatal(err)
}
hookForce = true
defer func() { hookForce = false }()
if err := runHookUninstall(hookUninstallCmd, nil); err != nil {
t.Fatalf("uninstall --force: %v", err)
}
if _, err := os.Stat(p); !os.IsNotExist(err) {
t.Error("file still exists")
}
}
func TestHookUninstall_Missing(t *testing.T) {
setupGitRepo(t)
if err := runHookUninstall(hookUninstallCmd, nil); err != nil {
t.Fatalf("uninstall missing: %v", err)
}
}
func TestHookScript_ContainsRequired(t *testing.T) {
if !strings.Contains(hookScript, "keyhunter scan") {
t.Error("embedded script missing 'keyhunter scan'")
}
if !strings.Contains(hookScript, "git diff --cached") {
t.Error("embedded script missing 'git diff --cached'")
}
if !strings.Contains(hookScript, hookMarker) {
t.Error("embedded script missing marker")
}
}