package hooks
import (
"errors"
"os"
"path/filepath"
"reflect"
"testing"
)
// TestPushOptions covers the environment protocol git actually uses. The
// count/index pair is only set for pre-receive and post-receive — `update`
// observably runs without it — which is why pre-receive exists in this
// package at all.
func TestPushOptions(t *testing.T) {
tests := []struct {
name string
env map[string]string
want []string
err string
}{
{
name: "no push-options phase",
env: map[string]string{},
want: nil,
},
{
name: "negotiated but empty",
env: map[string]string{"GIT_PUSH_OPTION_COUNT": "0"},
want: []string{},
},
{
name: "one option",
env: map[string]string{
"GIT_PUSH_OPTION_COUNT": "1",
"GIT_PUSH_OPTION_0": "skip-validation",
},
want: []string{"skip-validation"},
},
{
name: "several options keep their order",
env: map[string]string{
"GIT_PUSH_OPTION_COUNT": "3",
"GIT_PUSH_OPTION_0": "a",
"GIT_PUSH_OPTION_1": "skip-validation",
"GIT_PUSH_OPTION_2": "c",
},
want: []string{"a", "skip-validation", "c"},
},
{
name: "an unparseable count is refused, not treated as none",
env: map[string]string{"GIT_PUSH_OPTION_COUNT": "many"},
err: "is not a number",
},
{
name: "a negative count is refused",
env: map[string]string{"GIT_PUSH_OPTION_COUNT": "-1"},
err: "is negative",
},
{
name: "a count larger than the variables present is refused",
env: map[string]string{
"GIT_PUSH_OPTION_COUNT": "2",
"GIT_PUSH_OPTION_0": "skip-validation",
},
err: "GIT_PUSH_OPTION_1 is not set",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := PushOptions(envOf(tt.env))
if tt.err != "" {
if err == nil {
t.Fatalf("PushOptions accepted %v", tt.env)
}
mentions(t, "the error", err.Error(), tt.err)
return
}
if err != nil {
t.Fatalf("PushOptions: %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %#v want %#v", got, tt.want)
}
})
}
}
func TestSkipValidationIsMatchedExactly(t *testing.T) {
tests := []struct {
opts []string
want bool
}{
{nil, false},
{[]string{}, false},
{[]string{"skip-validation"}, true},
{[]string{"other", "skip-validation"}, true},
{[]string{"skip-validaton"}, false},
{[]string{"Skip-Validation"}, false},
{[]string{"skip-validation=yes"}, false},
{[]string{" skip-validation"}, false},
}
for _, tt := range tests {
if got := SkipValidation(tt.opts); got != tt.want {
t.Errorf("SkipValidation(%q) = %v, want %v", tt.opts, got, tt.want)
}
}
}
func TestUnknownOptions(t *testing.T) {
got := UnknownOptions([]string{"skip-validation", "skip-validaton", "reindex"})
want := []string{"skip-validaton", "reindex"}
if !reflect.DeepEqual(got, want) {
t.Errorf("got %q want %q", got, want)
}
if got := UnknownOptions([]string{"skip-validation"}); got != nil {
t.Errorf("got %q, want none", got)
}
}
func TestCredentialFromEnv(t *testing.T) {
tests := []struct {
name string
env map[string]string
want Credential
err string
}{
{
name: "owner",
env: map[string]string{EnvPrincipal: "owner"},
want: Credential{Kind: PrincipalOwner},
},
{
name: "agent with provenance",
env: map[string]string{
EnvPrincipal: "agent",
EnvAgentToken: " s3cret ",
EnvAgent: "claude-code/spec-writer",
EnvAgentSession: "6f1c",
},
want: Credential{
Kind: PrincipalAgent, Token: "s3cret",
Agent: "claude-code/spec-writer", Session: "6f1c",
},
},
{
name: "no principal is refused rather than defaulted",
env: map[string]string{},
err: "is not set",
},
{
name: "a blank principal is refused",
env: map[string]string{EnvPrincipal: " "},
err: "is not set",
},
{
name: "an unknown principal is refused",
env: map[string]string{EnvPrincipal: "anonymous"},
err: "is not a principal",
},
{
name: "an agent with no token is refused",
env: map[string]string{EnvPrincipal: "agent"},
err: "must present its token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := CredentialFromEnv(envOf(tt.env))
if tt.err != "" {
if err == nil {
t.Fatalf("CredentialFromEnv accepted %v", tt.env)
}
mentions(t, "the error", err.Error(), tt.err)
return
}
if err != nil {
t.Fatalf("CredentialFromEnv: %v", err)
}
if got != tt.want {
t.Errorf("got %+v want %+v", got, tt.want)
}
})
}
}
func TestSocketDerivation(t *testing.T) {
root := "/var/lib/spec"
if got, want := SocketPath(root), "/var/lib/spec/.specsrht/hook.sock"; got != want {
t.Errorf("SocketPath = %q want %q", got, want)
}
// A hook knows only its repository; the socket must fall out of that.
repo := filepath.Join(root, "~bigbes", "rfcs")
if got, want := SocketForRepo(repo), SocketPath(root); got != want {
t.Errorf("SocketForRepo(%q) = %q want %q", repo, got, want)
}
if got, want := ResolveSocket(envOf(nil), repo), SocketPath(root); got != want {
t.Errorf("ResolveSocket without an override = %q want %q", got, want)
}
override := "/run/specsrht/hook.sock"
if got := ResolveSocket(envOf(map[string]string{EnvSocket: override}), repo); got != override {
t.Errorf("ResolveSocket ignored the override: %q", got)
}
// A blank override is not an override; it must not resolve to "".
if got, want := ResolveSocket(envOf(map[string]string{EnvSocket: " "}), repo), SocketPath(root); got != want {
t.Errorf("a blank override produced %q want %q", got, want)
}
}
// TestRepoDir mirrors what git actually does: it chdirs into the repository
// and sets GIT_DIR to ".".
func TestRepoDir(t *testing.T) {
dir := shortTempDir(t)
getwd := func() (string, error) { return dir, nil }
got, err := RepoDir(envOf(map[string]string{envGitDir: "."}), getwd, filepath.EvalSymlinks)
if err != nil {
t.Fatalf("RepoDir: %v", err)
}
if got != dir {
t.Errorf("GIT_DIR=. resolved to %q want %q", got, dir)
}
got, err = RepoDir(envOf(nil), getwd, filepath.EvalSymlinks)
if err != nil {
t.Fatalf("RepoDir with no GIT_DIR: %v", err)
}
if got != dir {
t.Errorf("no GIT_DIR resolved to %q want %q", got, dir)
}
abs := filepath.Join(dir, "sub")
if err := os.Mkdir(abs, 0o755); err != nil {
t.Fatalf("Mkdir: %v", err)
}
got, err = RepoDir(envOf(map[string]string{envGitDir: abs}), getwd, filepath.EvalSymlinks)
if err != nil {
t.Fatalf("RepoDir with an absolute GIT_DIR: %v", err)
}
if got != abs {
t.Errorf("absolute GIT_DIR resolved to %q want %q", got, abs)
}
if _, err := RepoDir(envOf(nil), func() (string, error) { return "", errors.New("no cwd") },
filepath.EvalSymlinks); err == nil {
t.Error("RepoDir invented a repository when the working directory was unreadable")
}
if _, err := RepoDir(envOf(map[string]string{envGitDir: filepath.Join(dir, "gone")}),
getwd, filepath.EvalSymlinks); err == nil {
t.Error("RepoDir accepted a repository that does not exist")
}
}