package vociferate_test import ( "os" "os/exec" "path/filepath" "strings" "testing" "git.hrafn.xyz/aether/vociferate/internal/vociferate" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) type PrepareSuite struct { suite.Suite rootDir string } func TestPrepareSuite(t *testing.T) { suite.Run(t, new(PrepareSuite)) } func (s *PrepareSuite) SetupTest() { s.rootDir = s.T().TempDir() s.T().Setenv("GITHUB_SERVER_URL", "") s.T().Setenv("GITHUB_REPOSITORY", "") runGit(s.T(), s.rootDir, "init") runGit(s.T(), s.rootDir, "config", "user.name", "Vociferate Tests") runGit(s.T(), s.rootDir, "config", "user.email", "vociferate-tests@example.com") require.NoError(s.T(), os.WriteFile(filepath.Join(s.rootDir, ".gitkeep"), []byte("\n"), 0o644)) runGit(s.T(), s.rootDir, "add", ".gitkeep") runGit(s.T(), s.rootDir, "commit", "-m", "chore: initial test commit") runGit(s.T(), s.rootDir, "remote", "add", "origin", "git@git.hrafn.xyz:aether/vociferate.git") require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "release-version"), []byte("1.1.6\n"), 0o644, )) require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n### Breaking\n\n### Added\n\n- New thing.\n\n### Fixed\n\n- Old thing.\n\n## [1.1.6] - 2017-12-20\n\n### Fixed\n\n- Historical note.\n"), 0o644, )) } func runGit(t *testing.T, rootDir string, args ...string) string { t.Helper() command := exec.Command("git", append([]string{"-C", rootDir}, args...)...) output, err := command.CombinedOutput() require.NoError(t, err, "git %s failed:\n%s", strings.Join(args, " "), string(output)) return strings.TrimSpace(string(output)) } func firstCommitShortHash(t *testing.T, rootDir string) string { t.Helper() return runGit(t, rootDir, "rev-list", "--max-parents=0", "--abbrev-commit", "HEAD") } func (s *PrepareSuite) TestPrepare_UpdatesVersionAndPromotesUnreleasedNotes() { err := vociferate.Prepare(s.rootDir, "v1.1.7", "2026-03-20", vociferate.Options{}) require.NoError(s.T(), err) versionBytes, err := os.ReadFile(filepath.Join(s.rootDir, "release-version")) require.NoError(s.T(), err) require.Equal(s.T(), "1.1.7\n", string(versionBytes)) changelogBytes, err := os.ReadFile(filepath.Join(s.rootDir, "CHANGELOG.md")) require.NoError(s.T(), err) firstCommit := firstCommitShortHash(s.T(), s.rootDir) require.Equal(s.T(), "# Changelog\n\n## [Unreleased]\n\n### Breaking\n\n### Added\n\n### Changed\n\n### Removed\n\n### Fixed\n\n## [1.1.7] - 2026-03-20\n\n### Breaking\n\n### Added\n\n- New thing.\n\n### Fixed\n\n- Old thing.\n\n## [1.1.6] - 2017-12-20\n\n### Fixed\n\n- Historical note.\n\n[Unreleased]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.7...main\n[1.1.7]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.6...v1.1.7\n[1.1.6]: https://git.hrafn.xyz/aether/vociferate/compare/"+firstCommit+"...v1.1.6\n", string(changelogBytes)) } func (s *PrepareSuite) TestPrepare_ReturnsErrorWhenUnreleasedSectionMissing() { require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [1.1.6] - 2017-12-20\n"), 0o644, )) err := vociferate.Prepare(s.rootDir, "1.1.7", "2026-03-20", vociferate.Options{}) require.ErrorContains(s.T(), err, "unreleased section") } func (s *PrepareSuite) TestPrepare_ReturnsErrorWhenUnreleasedSectionIsEmpty() { require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n## [1.1.6] - 2017-12-20\n"), 0o644, )) err := vociferate.Prepare(s.rootDir, "1.1.7", "2026-03-20", vociferate.Options{}) require.ErrorContains(s.T(), err, "unreleased section is empty") } func (s *PrepareSuite) TestRecommendedTag_UsesMinorBumpWhenBreakingHeadingIsEmpty() { tag, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{}) require.NoError(s.T(), err) require.Equal(s.T(), "v1.2.0", tag) } func (s *PrepareSuite) TestUnreleasedBody_ReturnsStructuredPendingReleaseNotes() { body, err := vociferate.UnreleasedBody(s.rootDir, vociferate.Options{}) require.NoError(s.T(), err) require.Equal(s.T(), "### Breaking\n\n### Added\n\n- New thing.\n\n### Fixed\n\n- Old thing.\n", body) } func (s *PrepareSuite) TestUnreleasedBody_ReturnsErrorWhenUnreleasedSectionMissing() { require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [1.1.6] - 2017-12-20\n"), 0o644, )) _, err := vociferate.UnreleasedBody(s.rootDir, vociferate.Options{}) require.ErrorContains(s.T(), err, "unreleased section") } func (s *PrepareSuite) TestReleaseNotes_ReturnsReleaseSectionForVersion() { notes, err := vociferate.ReleaseNotes(s.rootDir, "1.1.6", vociferate.Options{}) require.NoError(s.T(), err) require.Equal(s.T(), "## [1.1.6] - 2017-12-20\n\n### Fixed\n\n- Historical note.\n", notes) } func (s *PrepareSuite) TestReleaseNotes_ReturnsErrorWhenVersionSectionMissing() { _, err := vociferate.ReleaseNotes(s.rootDir, "9.9.9", vociferate.Options{}) require.ErrorContains(s.T(), err, "release notes section") } func (s *PrepareSuite) TestRecommendedTag_ReturnsErrorWhenUnreleasedHasOnlyTemplateHeadings() { require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n### Breaking\n\n### Added\n\n### Changed\n\n### Removed\n\n### Fixed\n\n## [1.1.6] - 2017-12-20\n"), 0o644, )) _, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{}) require.ErrorContains(s.T(), err, "unreleased section is empty") } func (s *PrepareSuite) TestRecommendedTag_UsesPatchBumpForFixOnlyChanges() { require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n### Fixed\n\n- Patch note.\n\n## [1.1.6] - 2017-12-20\n"), 0o644, )) tag, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{}) require.NoError(s.T(), err) require.Equal(s.T(), "v1.1.7", tag) } func (s *PrepareSuite) TestRecommendedTag_UsesMajorBumpWhenRemovedEntriesExist() { require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n### Removed\n\n- Breaking removal.\n\n## [1.1.6] - 2017-12-20\n"), 0o644, )) tag, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{}) require.NoError(s.T(), err) require.Equal(s.T(), "v2.0.0", tag) } func (s *PrepareSuite) TestRecommendedTag_UsesMajorBumpWhenBreakingEntriesExist() { require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n### Breaking\n\n- Changed API contract.\n\n### Changed\n\n- Updated defaults.\n\n## [1.1.6] - 2017-12-20\n"), 0o644, )) tag, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{}) require.NoError(s.T(), err) require.Equal(s.T(), "v2.0.0", tag) } func (s *PrepareSuite) TestPrepare_UsesCustomVersionFileAndPattern() { customVersionFile := filepath.Join("custom", "VERSION.txt") require.NoError(s.T(), os.MkdirAll(filepath.Join(s.rootDir, "custom"), 0o755)) require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, customVersionFile), []byte("VERSION=1.1.6\n"), 0o644, )) err := vociferate.Prepare(s.rootDir, "1.1.8", "2026-03-20", vociferate.Options{ VersionFile: customVersionFile, VersionPattern: `VERSION=([^\n]+)`, }) require.NoError(s.T(), err) versionBytes, err := os.ReadFile(filepath.Join(s.rootDir, customVersionFile)) require.NoError(s.T(), err) require.Equal(s.T(), "VERSION=1.1.8\n", string(versionBytes)) } func (s *PrepareSuite) TestPrepare_AllowsUnchangedVersionValue() { require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "release-version"), []byte("1.1.6\n"), 0o644, )) err := vociferate.Prepare(s.rootDir, "1.1.6", "2026-03-20", vociferate.Options{}) require.NoError(s.T(), err) versionBytes, readErr := os.ReadFile(filepath.Join(s.rootDir, "release-version")) require.NoError(s.T(), readErr) require.Equal(s.T(), "1.1.6\n", string(versionBytes)) } func (s *PrepareSuite) TestRecommendedTag_UsesChangelogVersionWhenNoVersionFileConfigured() { // The default release-version file is present from SetupTest but should be ignored; // the current version must be read from the changelog, not the file. require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "release-version"), []byte("99.99.99\n"), // deliberately wrong value 0o644, )) require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n### Fixed\n\n- A fix.\n\n## [3.0.0] - 2026-01-01\n\n### Fixed\n\n- Historical.\n"), 0o644, )) tag, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{}) require.NoError(s.T(), err) require.Equal(s.T(), "v3.0.1", tag) } func (s *PrepareSuite) TestRecommendedTag_DefaultsToV1WhenNoPriorReleasesInChangelog() { require.NoError(s.T(), os.Remove(filepath.Join(s.rootDir, "release-version"))) require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n### Breaking\n\n### Added\n\n- First feature.\n"), 0o644, )) tag, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{}) require.NoError(s.T(), err) require.Equal(s.T(), "v1.0.0", tag) } func (s *PrepareSuite) TestPrepare_CreatesVersionFileWhenNotPresent() { require.NoError(s.T(), os.Remove(filepath.Join(s.rootDir, "release-version"))) err := vociferate.Prepare(s.rootDir, "2.0.0", "2026-03-20", vociferate.Options{}) require.NoError(s.T(), err) versionBytes, readErr := os.ReadFile(filepath.Join(s.rootDir, "release-version")) require.NoError(s.T(), readErr) require.Equal(s.T(), "2.0.0\n", string(versionBytes)) } func (s *PrepareSuite) TestRecommendedTag_UsesCustomVersionFileAndPattern() { customVersionFile := filepath.Join("custom", "VERSION.txt") require.NoError(s.T(), os.MkdirAll(filepath.Join(s.rootDir, "custom"), 0o755)) require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, customVersionFile), []byte("VERSION=2.3.4\n"), 0o644, )) require.NoError(s.T(), os.WriteFile( filepath.Join(s.rootDir, "CHANGELOG.md"), []byte("# Changelog\n\n## [Unreleased]\n\n### Added\n\n- Feature.\n\n## [2.3.4] - 2026-03-10\n"), 0o644, )) tag, err := vociferate.RecommendedTag(s.rootDir, vociferate.Options{ VersionFile: customVersionFile, VersionPattern: `VERSION=([^\n]+)`, }) require.NoError(s.T(), err) require.Equal(s.T(), "v2.4.0", tag) } func (s *PrepareSuite) TestPrepare_UsesGitHrafnXYZEnvironmentForChangelogLinks() { s.T().Setenv("GITHUB_SERVER_URL", "https://git.hrafn.xyz") s.T().Setenv("GITHUB_REPOSITORY", "aether/vociferate") err := vociferate.Prepare(s.rootDir, "1.1.7", "2026-03-20", vociferate.Options{}) require.NoError(s.T(), err) changelogBytes, readErr := os.ReadFile(filepath.Join(s.rootDir, "CHANGELOG.md")) require.NoError(s.T(), readErr) changelog := string(changelogBytes) firstCommit := firstCommitShortHash(s.T(), s.rootDir) require.Contains(s.T(), changelog, "## [Unreleased]\n") require.Contains(s.T(), changelog, "### Changed\n") require.Contains(s.T(), changelog, "### Removed\n") require.Contains(s.T(), changelog, "## [1.1.7] - 2026-03-20") require.Contains(s.T(), changelog, "## [1.1.6] - 2017-12-20") require.Contains(s.T(), changelog, "[Unreleased]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.7...main") require.Contains(s.T(), changelog, "[1.1.7]: https://git.hrafn.xyz/aether/vociferate/compare/v1.1.6...v1.1.7") require.Contains(s.T(), changelog, "[1.1.6]: https://git.hrafn.xyz/aether/vociferate/compare/"+firstCommit+"...v1.1.6") } func (s *PrepareSuite) TestPrepare_UsesGitHubEnvironmentForChangelogLinks() { s.T().Setenv("GITHUB_SERVER_URL", "https://github.com") s.T().Setenv("GITHUB_REPOSITORY", "aether/vociferate") err := vociferate.Prepare(s.rootDir, "1.1.7", "2026-03-20", vociferate.Options{}) require.NoError(s.T(), err) changelogBytes, readErr := os.ReadFile(filepath.Join(s.rootDir, "CHANGELOG.md")) require.NoError(s.T(), readErr) changelog := string(changelogBytes) firstCommit := firstCommitShortHash(s.T(), s.rootDir) require.Contains(s.T(), changelog, "## [Unreleased]\n") require.Contains(s.T(), changelog, "### Changed\n") require.Contains(s.T(), changelog, "### Removed\n") require.Contains(s.T(), changelog, "## [1.1.7] - 2026-03-20") require.Contains(s.T(), changelog, "## [1.1.6] - 2017-12-20") require.Contains(s.T(), changelog, "[Unreleased]: https://github.com/aether/vociferate/compare/v1.1.7...main") require.Contains(s.T(), changelog, "[1.1.7]: https://github.com/aether/vociferate/compare/v1.1.6...v1.1.7") require.Contains(s.T(), changelog, "[1.1.6]: https://github.com/aether/vociferate/compare/"+firstCommit+"...v1.1.6") }