package cli import ( "bytes" "context" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" ) func TestParse(t *testing.T) { invocation, err := Parse([]string{"version"}) if err != nil { t.Fatal(err) } if invocation.Command != VersionCommand { t.Fatalf("Parse() command = %q, want %q", invocation.Command, VersionCommand) } if _, err := Parse(nil); err == nil || !strings.Contains(err.Error(), "command is required") { t.Fatalf("Parse(nil) error = %v, want missing-command error", err) } if _, err := Parse([]string{"publish"}); err == nil || !strings.Contains(err.Error(), "unknown command") { t.Fatalf("Parse(publish) error = %v, want unknown-command error", err) } } func TestLoadSettingsResolvesPathsAndPreservesArguments(t *testing.T) { repoDir := filepath.Join(t.TempDir(), "repository with spaces") profileDir := filepath.Join(repoDir, "profiles with spaces") if err := os.MkdirAll(profileDir, 0o700); err != nil { t.Fatal(err) } want := Settings{ Product: "Example Product", Remote: "upstream", Checks: []CheckCommand{{ Args: []string{"sh", "scripts/check release.sh", "two words"}, Env: map[string]string{"RELEASE_MODE": "candidate check"}, }}, } contents, err := json.Marshal(want) if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(profileDir, "release profile.json"), contents, 0o600); err != nil { t.Fatal(err) } got, err := LoadSettings(repoDir, filepath.Join("profiles with spaces", "release profile.json")) if err != nil { t.Fatal(err) } if got.Product != want.Product || got.Remote != want.Remote || len(got.Checks) != 1 { t.Fatalf("LoadSettings() = %#v, want product, remote, and one check", got) } if got.Checks[0].Args[1] != "scripts/check release.sh" || got.Checks[0].Args[2] != "two words" { t.Fatalf("check arguments were changed: %#v", got.Checks[0].Args) } if got.Checks[0].Env["RELEASE_MODE"] != "candidate check" { t.Fatalf("check environment = %#v", got.Checks[0].Env) } } func TestLoadSettingsDefaultsRemoteAndConfigPath(t *testing.T) { repoDir := t.TempDir() contents := `{"product":"Example"}` profilePath := filepath.Join(repoDir, "releasekit.json") if err := os.WriteFile(profilePath, []byte(contents), 0o600); err != nil { t.Fatal(err) } for _, test := range []struct { name string path string }{ {name: "default path"}, {name: "absolute path", path: profilePath}, } { t.Run(test.name, func(t *testing.T) { settings, err := LoadSettings(repoDir, test.path) if err != nil { t.Fatal(err) } if settings.Remote != "origin" { t.Fatalf("default remote = %q, want origin", settings.Remote) } }) } } func TestExampleProfileLoads(t *testing.T) { repoDir, err := filepath.Abs(filepath.Join("..", "..", "..", "..")) if err != nil { t.Fatal(err) } settings, err := LoadSettings(repoDir, filepath.Join("examples", "releasekit.json")) if err != nil { t.Fatal(err) } if settings.Product != "Example" || settings.Remote != "origin" || len(settings.Checks) != 1 { t.Fatalf("example settings = %#v", settings) } } func TestLoadSettingsRejectsInvalidProfiles(t *testing.T) { tests := []struct { name string content string want string }{ {name: "missing product", content: `{}`, want: "product is required"}, {name: "unknown top-level field", content: `{"product":"Example","delivery":{}}`, want: "unknown field"}, {name: "unsupported build field", content: `{"product":"Example","build":{}}`, want: "unknown field"}, {name: "unknown command field", content: `{"product":"Example","checks":[{"args":["true"],"shell":true}]}`, want: "unknown field"}, {name: "command is required", content: `{"product":"Example","checks":[{"args":[]}]}`, want: "must include a command"}, {name: "multiple JSON values", content: `{"product":"Example"}{"product":"Other"}`, want: "multiple JSON values"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { repoDir := t.TempDir() path := filepath.Join(repoDir, "profile.json") if err := os.WriteFile(path, []byte(test.content), 0o600); err != nil { t.Fatal(err) } if _, err := LoadSettings(repoDir, path); err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("LoadSettings() error = %v, want text %q", err, test.want) } }) } } func TestRunChecksUsesConfiguredArgumentsEnvironmentAndDirectory(t *testing.T) { workdir := filepath.Join(t.TempDir(), "repo with spaces") if err := os.Mkdir(workdir, 0o700); err != nil { t.Fatal(err) } var stdout, stderr bytes.Buffer err := RunChecks(context.Background(), workdir, []CheckCommand{{ Args: []string{helperExecutable(t), "-test.run=TestConfiguredCommandHelper", "--", "argument with spaces"}, Env: map[string]string{"RELEASEKIT_CONFIGURED_HELPER": "1", "RELEASEKIT_CONFIGURED_VALUE": "selected value"}, }}, &stdout, &stderr) if err != nil { t.Fatal(err) } want := fmt.Sprintf("%s|argument with spaces|selected value", workdir) if stdout.String() != want { t.Fatalf("stdout = %q, want %q", stdout.String(), want) } } func TestConfiguredCommandHelper(t *testing.T) { if os.Getenv("RELEASEKIT_CONFIGURED_HELPER") != "1" { return } workdir, err := os.Getwd() if err != nil { os.Exit(9) } fmt.Fprintf(os.Stdout, "%s|%s|%s", workdir, os.Args[len(os.Args)-1], os.Getenv("RELEASEKIT_CONFIGURED_VALUE")) os.Exit(0) } func helperExecutable(t *testing.T) string { t.Helper() executable, err := os.Executable() if err != nil { t.Fatal(err) } return executable }