355 lines
15 KiB
Go
355 lines
15 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","expected_push_url":"ssh://git.example/owner/repo.git","validation":{"go_format":true,"markdown":{"enforce_checkout_boundary":true},"module_distribution":{"workspace_files":true,"local_replacements":true},"file_size":{"limit_bytes":0,"exclusions":["assets"]},"go":{"race":true,"full_vet":true,"compile_targets":["linux/amd64","linux/arm64","darwin/arm64"]}}}`), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
settings, err := LoadSettings(repoDir, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if settings.Validation == nil || !settings.Validation.GoFormat || settings.Validation.Markdown == nil || !settings.Validation.Markdown.EnforceCheckoutBoundary || !settings.Validation.ModuleDistribution.WorkspaceFiles || !settings.Validation.ModuleDistribution.LocalReplacements || !settings.Validation.Go.Race || !settings.Validation.Go.FullVet {
|
|
t.Fatalf("validation settings = %#v, want Go formatting, workspace inspection, race, and full vet enabled", settings.Validation)
|
|
}
|
|
if settings.Validation.FileSize == nil || settings.Validation.FileSize.LimitBytes == nil || *settings.Validation.FileSize.LimitBytes != 0 || len(settings.Validation.FileSize.Exclusions) != 1 || settings.Validation.FileSize.Exclusions[0] != "assets" {
|
|
t.Fatalf("file size settings = %#v, want explicit zero limit and exclusions", settings.Validation.FileSize)
|
|
}
|
|
if got, want := strings.Join(settings.Validation.Go.CompileTargets, ","), "linux/amd64,linux/arm64,darwin/arm64"; got != want {
|
|
t.Fatalf("compile targets = %q, want %q", got, want)
|
|
}
|
|
if settings.ExpectedPushURL != "ssh://git.example/owner/repo.git" {
|
|
t.Fatalf("expected push URL = %q", settings.ExpectedPushURL)
|
|
}
|
|
}
|
|
|
|
func TestLoadSettingsPreservesMarkdownObjectSelection(t *testing.T) {
|
|
repoDir := t.TempDir()
|
|
profilePath := filepath.Join(repoDir, "releasekit.json")
|
|
for _, test := range []struct {
|
|
name string
|
|
profile string
|
|
want bool
|
|
}{
|
|
{name: "omitted", profile: `{"product":"Example","validation":{}}`},
|
|
{name: "null", profile: `{"product":"Example","validation":{"markdown":null}}`},
|
|
{name: "empty object", profile: `{"product":"Example","validation":{"markdown":{}}}`, want: true},
|
|
{name: "boundary enabled", profile: `{"product":"Example","validation":{"markdown":{"enforce_checkout_boundary":true}}}`, want: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
if err := os.WriteFile(profilePath, []byte(test.profile), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, err := LoadSettings(repoDir, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := settings.Validation != nil && settings.Validation.Markdown != nil
|
|
if got != test.want {
|
|
t.Fatalf("Markdown selected = %t, want %t", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadSettingsPreservesEmptyAndNullFileSize(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
profile string
|
|
enabled bool
|
|
}{
|
|
{name: "empty object", profile: `{"product":"Example","validation":{"file_size":{}}}`, enabled: true},
|
|
{name: "default limit", profile: `{"product":"Example","validation":{"file_size":{"limit_bytes":null}}}`, enabled: true},
|
|
{name: "null object", profile: `{"product":"Example","validation":{"file_size":null}}`},
|
|
{name: "omitted object", profile: `{"product":"Example","validation":{}}`},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
repoDir := t.TempDir()
|
|
profilePath := filepath.Join(repoDir, "releasekit.json")
|
|
if err := os.WriteFile(profilePath, []byte(test.profile), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
settings, err := LoadSettings(repoDir, profilePath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := settings.Validation != nil && settings.Validation.FileSize != nil
|
|
if got != test.enabled {
|
|
t.Fatalf("FileSize enabled = %t, want %t", got, test.enabled)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadSettingsRejectsNegativeFileSizeLimit(t *testing.T) {
|
|
repoDir := t.TempDir()
|
|
profilePath := filepath.Join(repoDir, "releasekit.json")
|
|
profile := `{"product":"Example","validation":{"file_size":{"limit_bytes":-1}}}`
|
|
if err := os.WriteFile(profilePath, []byte(profile), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := LoadSettings(repoDir, profilePath); err == nil || !strings.Contains(err.Error(), "validation.file_size.limit_bytes") {
|
|
t.Fatalf("LoadSettings() error = %v, want invalid negative file-size limit", err)
|
|
}
|
|
}
|
|
|
|
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: "compile target format", content: `{"product":"Example","validation":{"go":{"compile_targets":["linux"]}}}`, want: "validation.go.compile_targets[0] must be GOOS/GOARCH"},
|
|
{name: "compile target missing architecture", content: `{"product":"Example","validation":{"go":{"compile_targets":["linux/"]}}}`, want: "validation.go.compile_targets[0] must be GOOS/GOARCH"},
|
|
{name: "compile target missing operating system", content: `{"product":"Example","validation":{"go":{"compile_targets":["/amd64"]}}}`, want: "validation.go.compile_targets[0] must be GOOS/GOARCH"},
|
|
{name: "compile target repeated separator", content: `{"product":"Example","validation":{"go":{"compile_targets":["linux/amd64/arm64"]}}}`, want: "validation.go.compile_targets[0] 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
|
|
}
|