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