Files
ReleaseKit/cmd/releasekit/internal/cli/config_test.go

272 lines
11 KiB
Go

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)
}
check, err := Parse([]string{"check", "--repo", "repository with spaces", "--config", "profiles/release config.json", "v1.2.3"})
if err != nil {
t.Fatal(err)
}
if check.Command != CheckAction || check.Repository != "repository with spaces" || check.ConfigPath != "profiles/release config.json" || check.Version != "v1.2.3" {
t.Fatalf("Parse(check) = %#v", check)
}
publish, err := Parse([]string{"publish", "--repo", "candidate", "--config", "releasekit.json", "v1.2.4"})
if err != nil {
t.Fatal(err)
}
if publish.Command != PublishAction || publish.Repository != "candidate" || publish.ConfigPath != "releasekit.json" || publish.Version != "v1.2.4" {
t.Fatalf("Parse(publish) = %#v", publish)
}
build, err := Parse([]string{"build", "--repo", "candidate", "--config", "releasekit.json", "--output", "output with spaces", "v1.2.5"})
if err != nil {
t.Fatal(err)
}
if build.Command != BuildAction || build.Repository != "candidate" || build.ConfigPath != "releasekit.json" || build.OutputDir != "output with spaces" || build.Version != "v1.2.5" {
t.Fatalf("Parse(build) = %#v", build)
}
deliver, err := Parse([]string{"deliver", "--repo", "candidate", "--config", "releasekit.json", "--assets", "assets with spaces", "v1.2.6"})
if err != nil {
t.Fatal(err)
}
if deliver.Command != DeliveryAction || deliver.Repository != "candidate" || deliver.ConfigPath != "releasekit.json" || deliver.AssetsDir != "assets with spaces" || deliver.Version != "v1.2.6" {
t.Fatalf("Parse(deliver) = %#v", deliver)
}
ciDelivery, err := Parse([]string{"ci-deliver", "--repo", "candidate", "--config", "profiles/release config.json", "--remote", "upstream", "--commit", "0123456789abcdef0123456789abcdef01234567", "v1.2.7"})
if err != nil {
t.Fatal(err)
}
if ciDelivery.Command != CIDeliveryAction || ciDelivery.Repository != "candidate" || ciDelivery.ConfigPath != "profiles/release config.json" || ciDelivery.Remote != "upstream" || ciDelivery.Commit != "0123456789abcdef0123456789abcdef01234567" || ciDelivery.Version != "v1.2.7" {
t.Fatalf("Parse(ci-deliver) = %#v", ciDelivery)
}
ciDefault, err := Parse([]string{"ci-deliver", "--commit", "0123456789abcdef0123456789abcdef01234567", "v1.2.8"})
if err != nil || ciDefault.Remote != "origin" {
t.Fatalf("Parse(ci-deliver defaults) = %#v, error = %v", ciDefault, err)
}
if _, err := Parse([]string{"ci-deliver", "v1.2.3"}); err == nil || !strings.Contains(err.Error(), "--commit") {
t.Fatalf("Parse(ci-deliver without commit) error = %v, want required commit", err)
}
if _, err := Parse([]string{"ci-deliver", "--commit", "abc123", "v1.2.3"}); err == nil || !strings.Contains(err.Error(), "full Git object ID") {
t.Fatalf("Parse(ci-deliver with abbreviated commit) error = %v, want full object ID error", err)
}
if _, err := Parse([]string{"check", "v1.2.3", "--repo", "repository"}); err == nil {
t.Fatal("Parse accepted a flag after the version")
}
if _, err := Parse([]string{"ci-deliver", "--commit", "0123456789abcdef0123456789abcdef01234567", "v1.2.3", "--repo", "repository"}); err == nil {
t.Fatal("Parse accepted a ci-deliver flag after the version")
}
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{"unknown"}); err == nil || !strings.Contains(err.Error(), "unknown command") {
t.Fatalf("Parse(unknown) 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 TestLoadSettingsReadsGoValidation(t *testing.T) {
repoDir := t.TempDir()
profilePath := filepath.Join(repoDir, "releasekit.json")
if err := os.WriteFile(profilePath, []byte(`{"product":"Example","validation":{"go":{"race":true,"full_vet":true}}}`), 0o600); err != nil {
t.Fatal(err)
}
settings, err := LoadSettings(repoDir, "")
if err != nil {
t.Fatal(err)
}
if settings.Validation == nil || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet {
t.Fatalf("validation settings = %#v, want Go race and full vet enabled", settings.Validation)
}
}
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) != 0 {
t.Fatalf("example settings = %#v", settings)
}
}
func TestBuildProfileLoads(t *testing.T) {
repoDir := t.TempDir()
path := filepath.Join(repoDir, "profile.json")
if err := os.WriteFile(path, []byte(`{"product":"Example","build":{"main_package":"./cmd/app","binary_name":"example","targets":["linux/amd64","darwin/arm64"]}}`), 0o600); err != nil {
t.Fatal(err)
}
settings, err := LoadSettings(repoDir, path)
if err != nil {
t.Fatal(err)
}
if settings.Build == nil || settings.Build.MainPackage != "./cmd/app" || settings.Build.BinaryName != "example" || len(settings.Build.Targets) != 2 {
t.Fatalf("build settings = %#v", settings.Build)
}
repoDir, err = filepath.Abs(filepath.Join("..", "..", "..", ".."))
if err != nil {
t.Fatal(err)
}
example, err := LoadSettings(repoDir, filepath.Join("examples", "releasekit-binary.json"))
if err != nil {
t.Fatal(err)
}
if example.Build == nil || len(example.Build.Targets) != 3 {
t.Fatalf("binary example settings = %#v", example.Build)
}
}
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","future":{}}`, want: "unknown field"},
{name: "build package required", content: `{"product":"Example","build":{"binary_name":"example","targets":["linux/amd64"]}}`, want: "build.main_package is required"},
{name: "build target format", content: `{"product":"Example","build":{"main_package":".","binary_name":"example","targets":["linux"]}}`, want: "must be GOOS/GOARCH"},
{name: "unsafe binary name", content: `{"product":"Example","build":{"main_package":".","binary_name":"../example","targets":["linux/amd64"]}}`, want: "filename component"},
{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",
"RELEASEKIT_VERSION": "profile value",
},
}}, map[string]string{"RELEASEKIT_VERSION": "captured version", "RELEASEKIT_CANDIDATE": "captured commit"}, &stdout, &stderr)
if err != nil {
t.Fatal(err)
}
want := fmt.Sprintf("%s|argument with spaces|selected value|captured version|captured commit", 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|%s|%s", workdir, os.Args[len(os.Args)-1], os.Getenv("RELEASEKIT_CONFIGURED_VALUE"), os.Getenv("RELEASEKIT_VERSION"), os.Getenv("RELEASEKIT_CANDIDATE"))
os.Exit(0)
}
func helperExecutable(t *testing.T) string {
t.Helper()
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
return executable
}