package hooks import ( "context" "fmt" "io/fs" "os" "path/filepath" "testing" "time" ) const expectedHookBody = `#!/bin/sh # simplegit-managed git hook. Forwards this invocation to the simplegit daemon # hook bus via ` + "`simplegit hook`" + `. The daemon exports SIMPLEGIT_BIN (absolute # path to the simplegit binary) and SIMPLEGIT_ROOT (data root, where hook.sock # lives). If SIMPLEGIT_BIN is unset -- the hook running outside the daemon -- # fail open (exit 0) so the git operation is never blocked by the event bus. test -n "$SIMPLEGIT_BIN" || exit 0 exec "$SIMPLEGIT_BIN" hook --root="${SIMPLEGIT_ROOT:-}" %s "$@" ` func TestTemplateInSync(t *testing.T) { entries, err := fs.ReadDir(templateFS, "template/hooks") if err != nil { t.Fatalf("read embedded template/hooks: %v", err) } got := map[string]bool{} for _, e := range entries { got[e.Name()] = true } for _, name := range hookNames { if !got[name] { t.Errorf("hook %q: no checked-in template file", name) } } for name := range got { found := false for _, n := range hookNames { if n == name { found = true break } } if !found { t.Errorf("template/hooks/%s: not in hookNames (stale file)", name) } } } func TestEnsureTemplate(t *testing.T) { root := t.TempDir() dir, err := EnsureTemplate(root) if err != nil { t.Fatalf("EnsureTemplate: %v", err) } if dir != TemplateDir(root) { t.Fatalf("template dir = %s, want %s", dir, TemplateDir(root)) } for _, name := range hookNames { p := filepath.Join(dir, "hooks", name) fi, err := os.Stat(p) if err != nil { t.Errorf("hook %s: not installed: %v", name, err) continue } if fi.Mode()&0o111 == 0 { t.Errorf("hook %s: not executable", name) } body, err := os.ReadFile(p) if err != nil { t.Errorf("hook %s: read: %v", name, err) continue } if want := fmt.Sprintf(expectedHookBody, name); string(body) != want { t.Errorf("hook %s: body mismatch\n got: %q\nwant: %q", name, string(body), want) } } if _, err := EnsureTemplate(root); err != nil { t.Fatalf("EnsureTemplate second run: %v", err) } } func TestStartRoundTrip(t *testing.T) { root := t.TempDir() ctx, cancel := context.WithCancel(context.Background()) defer cancel() resolver, err := Start(ctx, root) if err != nil { t.Fatalf("Start: %v", err) } deadline := time.Now().Add(2 * time.Second) for { if _, err := os.Stat(SockPath(root)); err == nil { break } if time.Now().After(deadline) { t.Fatalf("socket never appeared") } time.Sleep(5 * time.Millisecond) } want := HookEvent{ Type: "post-receive", Repo: "acme/widget", RepoPath: filepath.Join(root, "repos", "acme", "widget.git"), Args: []string{}, Stdin: "0000000000000000000000000000000000000000 1234567890abcdef refs/heads/main\n", Env: map[string]string{"GIT_DIR": "."}, } if exit := forward(want, SockPath(root)); exit != 0 { t.Fatalf("forward exit = %d, want 0", exit) } select { case got := <-resolver: if got.Type != want.Type || got.Repo != want.Repo || got.Stdin != want.Stdin { t.Fatalf("events got %+v, want %+v", got, want) } case <-time.After(2 * time.Second): t.Fatalf("events channel did not receive the event") } } func TestForwardFailOpen(t *testing.T) { root := t.TempDir() exit := forward(HookEvent{Type: "pre-receive"}, SockPath(root)) if exit != 0 { t.Fatalf("forward exit = %d, want 0 (fail open)", exit) } } func TestResolveRepo(t *testing.T) { root := t.TempDir() cases := []struct{ gitDir, cwd, want string }{ {filepath.Join(root, "repos", "acme", "widget.git"), "/somewhere", "acme/widget"}, {filepath.Join(root, "repos", "org", "team", "svc.git"), "/somewhere", "org/team/svc"}, {"", filepath.Join(root, "repos", "acme", "widget.git"), "acme/widget"}, {"", "/tmp/elsewhere.git", ""}, {filepath.Join(root, "repos"), "/somewhere", ""}, } for _, c := range cases { if got := resolveRepo(root, c.gitDir, c.cwd); got != c.want { t.Errorf("resolveRepo(gitDir=%q, cwd=%q) = %q, want %q", c.gitDir, c.cwd, got, c.want) } } }