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
This commit is contained in:
184
cmd/hook_test.go
Normal file
184
cmd/hook_test.go
Normal file
@@ -0,0 +1,184 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user