package hooks
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
)
// fakeBinary is something Install will accept as the specsrht binary: it only
// has to exist and be executable.
func fakeBinary(t *testing.T, dir string) string {
t.Helper()
path := filepath.Join(dir, "specsrht")
if err := os.WriteFile(path, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write a stand-in binary: %v", err)
}
return path
}
func TestInstall(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skipf("git is not on PATH: %v", err)
}
root := shortTempDir(t)
repo := bareRepo(t, root, testSpace)
binary := fakeBinary(t, root)
if err := InstallSpace(root, testSpace, InstallOptions{Binary: binary}); err != nil {
t.Fatalf("InstallSpace: %v", err)
}
for _, mode := range Modes() {
path := filepath.Join(repo, "hooks", string(mode))
target, err := os.Readlink(path)
if err != nil {
t.Fatalf("%s is not a symlink: %v", mode, err)
}
if target != binary {
t.Errorf("%s points at %q, want %q", mode, target, binary)
}
// git only runs a hook it can execute; the symlink must resolve.
info, err := os.Stat(path)
if err != nil {
t.Fatalf("stat %s: %v", mode, err)
}
if info.Mode().Perm()&0o111 == 0 {
t.Errorf("%s is not executable", mode)
}
}
// Without this, `git push --push-option=...` fails client-side with "the
// receiving end does not support push options" and the documented escape
// hatch would not exist at all.
out := gitMust(t, repo, "config", "--get", "receive.advertisePushOptions")
if strings.TrimSpace(out) != "true" {
t.Errorf("receive.advertisePushOptions = %q, want true", strings.TrimSpace(out))
}
// The repository must still be usable afterwards: writing the config back
// through go-git must not lose what git init put there.
if got := strings.TrimSpace(gitMust(t, repo, "config", "--get", "core.bare")); got != "true" {
t.Errorf("core.bare = %q after installing hooks; the repository is no longer bare", got)
}
if got := strings.TrimSpace(gitMust(t, repo, "symbolic-ref", "HEAD")); got != "refs/heads/main" {
t.Errorf("HEAD = %q after installing hooks", got)
}
}
// TestInstallIsIdempotent: refreshing is how an upgrade repairs every
// repository, so it has to be safe to run over and over, and over whatever was
// there before.
func TestInstallIsIdempotent(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skipf("git is not on PATH: %v", err)
}
root := shortTempDir(t)
repo := bareRepo(t, root, testSpace)
binary := fakeBinary(t, root)
// Something else got there first: git's own sample hook, and a plain file.
stale := filepath.Join(repo, "hooks", "update")
if err := os.WriteFile(stale, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write a stale hook: %v", err)
}
for i := range 3 {
if err := Install(repo, InstallOptions{Binary: binary}); err != nil {
t.Fatalf("Install (pass %d): %v", i, err)
}
}
target, err := os.Readlink(stale)
if err != nil {
t.Fatalf("the pre-existing hook was not replaced: %v", err)
}
if target != binary {
t.Errorf("update points at %q, want %q", target, binary)
}
if _, err := os.Stat(stale + installSuffix); !os.IsNotExist(err) {
t.Errorf("the temporary link was left behind: %v", err)
}
}
func TestInstallRefusesBadInput(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skipf("git is not on PATH: %v", err)
}
root := shortTempDir(t)
repo := bareRepo(t, root, testSpace)
binary := fakeBinary(t, root)
notExecutable := filepath.Join(root, "notexec")
if err := os.WriteFile(notExecutable, []byte("x"), 0o644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
tests := []struct {
name string
repo string
opts InstallOptions
want string
}{
{"no binary", repo, InstallOptions{}, "no binary"},
{"relative binary", repo, InstallOptions{Binary: "specsrht"}, "not an absolute path"},
{"missing binary", repo, InstallOptions{Binary: filepath.Join(root, "gone")}, "no such file"},
{"non-executable binary", repo, InstallOptions{Binary: notExecutable}, "not an executable file"},
{"non-executable binary is a directory", repo, InstallOptions{Binary: root}, "not an executable file"},
{"no repository", "", InstallOptions{Binary: binary}, "no repository directory"},
{"relative repository", "rfcs", InstallOptions{Binary: binary}, "not absolute"},
{"not a bare repository", root, InstallOptions{Binary: binary}, "does not look like a bare repository"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := Install(tt.repo, tt.opts)
if err == nil {
t.Fatalf("Install accepted %s", tt.name)
}
mentions(t, "the error", err.Error(), tt.want)
})
}
}
func TestInstallSpaceValidatesTheSpace(t *testing.T) {
root := shortTempDir(t)
binary := fakeBinary(t, root)
for _, ref := range []core.SpaceRef{
{Owner: "", Name: "rfcs"},
{Owner: "bigbes", Name: ""},
{Owner: "../etc", Name: "rfcs"},
{Owner: "bigbes", Name: "../../etc"},
} {
if err := InstallSpace(root, ref, InstallOptions{Binary: binary}); err == nil {
t.Errorf("InstallSpace accepted %+v", ref)
}
}
}