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") } }