diff --git a/cmd/vociferate/main_test.go b/cmd/vociferate/main_test.go new file mode 100644 index 0000000..0139349 --- /dev/null +++ b/cmd/vociferate/main_test.go @@ -0,0 +1,120 @@ +package main + +import ( + "flag" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func TestMainRecommendPrintsTag(t *testing.T) { + root := t.TempDir() + writeFile(t, filepath.Join(root, "release-version"), "1.1.6\n") + writeFile(t, filepath.Join(root, "changelog.md"), "# Changelog\n\n## [Unreleased]\n\n### Added\n\n- Feature.\n\n## [1.1.6] - 2017-12-20\n") + + stdout, stderr, code := runMain(t, "--recommend", "--root", root) + if code != 0 { + t.Fatalf("expected exit 0, got %d (stderr: %s)", code, stderr) + } + if strings.TrimSpace(stdout) != "v1.2.0" { + t.Fatalf("unexpected recommended tag: %q", strings.TrimSpace(stdout)) + } +} + +func TestMainUsageExitWhenRequiredFlagsMissing(t *testing.T) { + _, stderr, code := runMain(t) + if code != 2 { + t.Fatalf("expected exit 2, got %d", code) + } + if !strings.Contains(stderr, "usage: vociferate") { + t.Fatalf("expected usage text in stderr, got: %s", stderr) + } +} + +func TestMainPrepareUpdatesFiles(t *testing.T) { + root := t.TempDir() + writeFile(t, filepath.Join(root, ".git", "config"), "[remote \"origin\"]\n\turl = git@git.hrafn.xyz:aether/vociferate.git\n") + writeFile(t, filepath.Join(root, "release-version"), "1.1.6\n") + writeFile(t, filepath.Join(root, "changelog.md"), "# Changelog\n\n## [Unreleased]\n\n### Fixed\n\n- Patch note.\n\n## [1.1.6] - 2017-12-20\n") + + _, stderr, code := runMain(t, "--version", "v1.1.7", "--date", "2026-03-20", "--root", root) + if code != 0 { + t.Fatalf("expected exit 0, got %d (stderr: %s)", code, stderr) + } + + versionBytes, err := os.ReadFile(filepath.Join(root, "release-version")) + if err != nil { + t.Fatalf("read release-version: %v", err) + } + if strings.TrimSpace(string(versionBytes)) != "1.1.7" { + t.Fatalf("unexpected version file value: %q", string(versionBytes)) + } +} + +func TestMainPrepareReturnsExitOneOnFailure(t *testing.T) { + root := t.TempDir() + _, stderr, code := runMain(t, "--version", "v1.1.7", "--date", "2026-03-20", "--root", root) + if code != 1 { + t.Fatalf("expected exit 1, got %d", code) + } + if !strings.Contains(stderr, "prepare release") { + t.Fatalf("expected prepare error in stderr, got: %s", stderr) + } +} + +func TestHelperProcess(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + return + } + + idx := -1 + for i, arg := range os.Args { + if arg == "--" { + idx = i + break + } + } + if idx == -1 { + os.Exit(2) + } + + args := append([]string{"vociferate"}, os.Args[idx+1:]...) + os.Args = args + flag.CommandLine = flag.NewFlagSet(args[0], flag.ExitOnError) + + main() + os.Exit(0) +} + +func runMain(t *testing.T, args ...string) (string, string, int) { + t.Helper() + + cmdArgs := append([]string{"-test.run=TestHelperProcess", "--"}, args...) + cmd := exec.Command(os.Args[0], cmdArgs...) + cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1") + + out, err := cmd.CombinedOutput() + output := string(out) + if err == nil { + return output, "", 0 + } + + if exitErr, ok := err.(*exec.ExitError); ok { + return "", output, exitErr.ExitCode() + } + + t.Fatalf("run helper process: %v", err) + return "", "", -1 +} + +func writeFile(t *testing.T, path, content string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("mkdir for %s: %v", path, err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write file %s: %v", path, err) + } +} diff --git a/internal/vociferate/vociferate_internal_test.go b/internal/vociferate/vociferate_internal_test.go new file mode 100644 index 0000000..c7df7de --- /dev/null +++ b/internal/vociferate/vociferate_internal_test.go @@ -0,0 +1,123 @@ +package vociferate + +import ( + "os" + "path/filepath" + "testing" +) + +func TestNormalizeRepoURL(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + remoteURL string + wantURL string + wantOK bool + }{ + {name: "https", remoteURL: "https://git.hrafn.xyz/aether/vociferate.git", wantURL: "https://git.hrafn.xyz/aether/vociferate", wantOK: true}, + {name: "http", remoteURL: "http://teapot:3000/aether/vociferate.git", wantURL: "http://teapot:3000/aether/vociferate", wantOK: true}, + {name: "ssh with scheme", remoteURL: "ssh://git@git.hrafn.xyz/aether/vociferate.git", wantURL: "https://git.hrafn.xyz/aether/vociferate", wantOK: true}, + {name: "scp style", remoteURL: "git@git.hrafn.xyz:aether/vociferate.git", wantURL: "https://git.hrafn.xyz/aether/vociferate", wantOK: true}, + {name: "empty", remoteURL: "", wantURL: "", wantOK: false}, + {name: "unsupported scheme", remoteURL: "ftp://example.com/repo.git", wantURL: "", wantOK: false}, + {name: "invalid ssh missing user", remoteURL: "ssh://git.hrafn.xyz/aether/vociferate.git", wantURL: "", wantOK: false}, + {name: "invalid scp style", remoteURL: "git.hrafn.xyz:aether/vociferate.git", wantURL: "", wantOK: false}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + gotURL, gotOK := normalizeRepoURL(tt.remoteURL) + if gotOK != tt.wantOK { + t.Fatalf("normalizeRepoURL(%q) ok = %v, want %v", tt.remoteURL, gotOK, tt.wantOK) + } + if gotURL != tt.wantURL { + t.Fatalf("normalizeRepoURL(%q) url = %q, want %q", tt.remoteURL, gotURL, tt.wantURL) + } + }) + } +} + +func TestParseSemver(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + want semver + wantErr bool + }{ + {name: "valid", input: "1.2.3", want: semver{major: 1, minor: 2, patch: 3}, wantErr: false}, + {name: "missing part", input: "1.2", wantErr: true}, + {name: "non numeric", input: "1.two.3", wantErr: true}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := parseSemver(tt.input) + if tt.wantErr { + if err == nil { + t.Fatalf("parseSemver(%q) expected error", tt.input) + } + return + } + if err != nil { + t.Fatalf("parseSemver(%q) unexpected error: %v", tt.input, err) + } + if got != tt.want { + t.Fatalf("parseSemver(%q) = %+v, want %+v", tt.input, got, tt.want) + } + }) + } +} + +func TestOriginRemoteURLFromGitConfig(t *testing.T) { + t.Parallel() + + t.Run("origin exists", func(t *testing.T) { + t.Parallel() + config := "[core]\n\trepositoryformatversion = 0\n[remote \"origin\"]\n\turl = git@git.hrafn.xyz:aether/vociferate.git\n" + url, ok := originRemoteURLFromGitConfig(config) + if !ok { + t.Fatal("expected origin url to be found") + } + if url != "git@git.hrafn.xyz:aether/vociferate.git" { + t.Fatalf("unexpected url: %q", url) + } + }) + + t.Run("origin missing", func(t *testing.T) { + t.Parallel() + config := "[core]\n\trepositoryformatversion = 0\n[remote \"upstream\"]\n\turl = git@git.hrafn.xyz:aether/vociferate.git\n" + _, ok := originRemoteURLFromGitConfig(config) + if ok { + t.Fatal("expected origin url to be absent") + } + }) +} + +func TestDeriveRepositoryURLFromGitConfigFallback(t *testing.T) { + t.Setenv("GITHUB_SERVER_URL", "") + t.Setenv("GITHUB_REPOSITORY", "") + + root := t.TempDir() + configPath := filepath.Join(root, ".git", "config") + if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { + t.Fatalf("mkdir .git: %v", err) + } + if err := os.WriteFile(configPath, []byte("[remote \"origin\"]\n\turl = git@git.hrafn.xyz:aether/vociferate.git\n"), 0o644); err != nil { + t.Fatalf("write git config: %v", err) + } + + url, ok := deriveRepositoryURL(root) + if !ok { + t.Fatal("expected repository URL from git config") + } + if url != "https://git.hrafn.xyz/aether/vociferate" { + t.Fatalf("unexpected repository URL: %q", url) + } +}