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