feat: go 1.25.5, dependency updates, renamed internal/errors (#129)

* feat: rename internal/errors to internal/apperrors

* fix(tests): clear env values before using in tests

* feat: rename internal/errors to internal/apperrors

* chore(deps): update go and all dependencies

* chore: remove renovate from pre-commit, formatting

* chore: sonarcloud fixes

* feat: consolidate constants to appconstants/constants.go

* chore: sonarcloud fixes

* feat: simplification, deduplication, test utils

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: sonarcloud fixes

* chore: clean up

* fix: config discovery, const deduplication

* chore: fixes
This commit is contained in:
2026-01-01 23:17:29 +02:00
committed by GitHub
parent 85a439d804
commit 7f80105ff5
65 changed files with 2321 additions and 1710 deletions

View File

@@ -14,6 +14,8 @@ import (
"time"
"github.com/google/go-github/v74/github"
"github.com/ivuorinen/gh-action-readme/appconstants"
)
// MockHTTPClient is a mock HTTP client for testing.
@@ -91,20 +93,155 @@ func TempDir(t *testing.T) (string, func()) {
}
}
// CleanupCache provides a standard cache cleanup helper for deferred cleanup.
// It returns a function that closes the cache and fails the test on errors.
func CleanupCache(tb testing.TB, cache interface{ Close() error }) func() {
tb.Helper()
return func() {
tb.Helper()
if err := cache.Close(); err != nil {
tb.Fatalf("failed to close cache: %v", err)
}
}
}
// ExpectPanic asserts that the provided function panics with a message containing the expected substring.
// This helper reduces panic recovery test boilerplate from 12-15 lines to 3-4 lines.
func ExpectPanic(t *testing.T, fn func(), expectedSubstring string) {
t.Helper()
defer func() {
if r := recover(); r == nil {
t.Error("expected panic but got none")
} else {
var errStr string
switch v := r.(type) {
case string:
errStr = v
case error:
errStr = v.Error()
default:
errStr = fmt.Sprintf("%v", v)
}
if !strings.Contains(errStr, expectedSubstring) {
t.Errorf("expected panic message containing %q, got: %v", expectedSubstring, r)
}
}
}()
fn()
}
// MustLoadActionFixture loads an action fixture and fails the test on error.
// This helper consolidates the load + assertion pattern.
func MustLoadActionFixture(t *testing.T, path string) *ActionFixture {
t.Helper()
fixture, err := LoadActionFixture(path)
AssertNoError(t, err)
return fixture
}
// LoadAndWriteFixture loads an action fixture and writes it to the specified path.
// This helper reduces the common 3-line pattern to a single line.
func LoadAndWriteFixture(t *testing.T, fixturePath, targetPath string) {
t.Helper()
fixture := MustLoadActionFixture(t, fixturePath)
WriteTestFile(t, targetPath, fixture.Content)
}
// WriteTestFile writes a test file to the given path.
func WriteTestFile(t *testing.T, path, content string) {
t.Helper()
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0750); err != nil { // #nosec G301 -- test directory permissions
// #nosec G301 -- test directory permissions
if err := os.MkdirAll(dir, appconstants.FilePermDir); err != nil {
t.Fatalf("failed to create dir %s: %v", dir, err)
}
if err := os.WriteFile(path, []byte(content), 0600); err != nil { // #nosec G306 -- test file permissions
// #nosec G306 -- test file permissions
if err := os.WriteFile(path, []byte(content), appconstants.FilePermDefault); err != nil {
t.Fatalf("failed to write test file %s: %v", path, err)
}
}
// WriteActionFixture writes an action fixture to a standard action.yml file.
func WriteActionFixture(t *testing.T, dir, fixturePath string) string {
t.Helper()
actionPath := filepath.Join(dir, appconstants.TestPathActionYML)
fixture := MustLoadActionFixture(t, fixturePath)
WriteTestFile(t, actionPath, fixture.Content)
return actionPath
}
// WriteActionFixtureAs writes an action fixture with a custom filename.
func WriteActionFixtureAs(t *testing.T, dir, filename, fixturePath string) string {
t.Helper()
actionPath := filepath.Join(dir, filename)
fixture := MustLoadActionFixture(t, fixturePath)
WriteTestFile(t, actionPath, fixture.Content)
return actionPath
}
// CreateConfigDir creates a standard .config/gh-action-readme directory.
func CreateConfigDir(t *testing.T, baseDir string) string {
t.Helper()
configDir := filepath.Join(baseDir, appconstants.TestDirConfigGhActionReadme)
// #nosec G301 -- test directory permissions
if err := os.MkdirAll(configDir, appconstants.FilePermDir); err != nil {
t.Fatalf("failed to create config dir: %v", err)
}
return configDir
}
// WriteConfigFile writes a config file to the standard location.
func WriteConfigFile(t *testing.T, baseDir, content string) string {
t.Helper()
configDir := CreateConfigDir(t, baseDir)
configPath := filepath.Join(configDir, appconstants.ConfigFileNameFull)
WriteTestFile(t, configPath, content)
return configPath
}
// CreateActionSubdir creates a subdirectory and writes an action fixture to it.
func CreateActionSubdir(t *testing.T, baseDir, subdirName, fixturePath string) string {
t.Helper()
subDir := filepath.Join(baseDir, subdirName)
// #nosec G301 -- test directory permissions
if err := os.MkdirAll(subDir, appconstants.FilePermDir); err != nil {
t.Fatalf("failed to create subdir: %v", err)
}
return WriteActionFixture(t, subDir, fixturePath)
}
// AssertFileExists fails if the file does not exist.
func AssertFileExists(t *testing.T, path string) {
t.Helper()
if _, err := os.Stat(path); os.IsNotExist(err) {
t.Fatalf("expected file to exist: %s", path)
}
}
// AssertFileNotExists fails if the file exists.
func AssertFileNotExists(t *testing.T, path string) {
t.Helper()
_, err := os.Stat(path)
if err == nil {
// File exists
t.Fatalf("expected file not to exist: %s", path)
}
if err != nil && !os.IsNotExist(err) {
// Error occurred but it's not a "does not exist" error
t.Fatalf("error checking file existence: %v", err)
}
// err != nil && os.IsNotExist(err) - this is the success case
}
// MockColoredOutput captures output for testing.
type MockColoredOutput struct {
Messages []string
@@ -192,14 +329,14 @@ func CreateTestAction(name, description string, inputs map[string]string) string
inputsYAML.WriteString(fmt.Sprintf(" %s:\n description: %s\n required: true\n", key, desc))
}
result := fmt.Sprintf("name: %s\n", name)
result += fmt.Sprintf("description: %s\n", description)
result := fmt.Sprintf(appconstants.YAMLFieldName, name)
result += fmt.Sprintf(appconstants.YAMLFieldDescription, description)
result += "inputs:\n"
result += inputsYAML.String()
result += "outputs:\n"
result += " result:\n"
result += " description: 'The result'\n"
result += "runs:\n"
result += appconstants.YAMLFieldRuns
result += " using: 'node20'\n"
result += " main: 'index.js'\n"
result += "branding:\n"
@@ -220,16 +357,17 @@ func SetupTestTemplates(t *testing.T, dir string) {
// Create directories
for _, theme := range []string{"github", "gitlab", "minimal", "professional"} {
themeDir := filepath.Join(themesDir, theme)
if err := os.MkdirAll(themeDir, 0750); err != nil { // #nosec G301 -- test directory permissions
// #nosec G301 -- test directory permissions
if err := os.MkdirAll(themeDir, appconstants.FilePermDir); err != nil {
t.Fatalf("failed to create theme dir %s: %v", themeDir, err)
}
// Write theme template
templatePath := filepath.Join(themeDir, "readme.tmpl")
templatePath := filepath.Join(themeDir, appconstants.TemplateReadme)
WriteTestFile(t, templatePath, SimpleTemplate)
}
// Create default template
defaultTemplatePath := filepath.Join(templatesDir, "readme.tmpl")
defaultTemplatePath := filepath.Join(templatesDir, appconstants.TemplateReadme)
WriteTestFile(t, defaultTemplatePath, SimpleTemplate)
}
@@ -240,9 +378,9 @@ func CreateCompositeAction(name, description string, steps []string) string {
stepsYAML.WriteString(fmt.Sprintf(" - name: Step %d\n uses: %s\n", i+1, step))
}
result := fmt.Sprintf("name: %s\n", name)
result += fmt.Sprintf("description: %s\n", description)
result += "runs:\n"
result := fmt.Sprintf(appconstants.YAMLFieldName, name)
result += fmt.Sprintf(appconstants.YAMLFieldDescription, description)
result += appconstants.YAMLFieldRuns
result += " using: 'composite'\n"
result += " steps:\n"
result += stepsYAML.String()
@@ -373,6 +511,27 @@ func AssertEqual(t *testing.T, expected, actual any) {
}
}
// AssertSliceContainsAll fails if any of expectedSubstrings is not found in any item of the slice.
// This is useful for checking that suggestions or messages contain expected content.
func AssertSliceContainsAll(t *testing.T, slice []string, expectedSubstrings []string) {
t.Helper()
if len(slice) == 0 {
t.Fatal("slice is empty")
}
allItems := strings.Join(slice, " ")
for _, expected := range expectedSubstrings {
if !strings.Contains(allItems, expected) {
t.Errorf(
"expected to find %q in slice, got:\n%s",
expected,
strings.Join(slice, "\n"),
)
}
}
}
// NewStringReader creates an io.ReadCloser from a string.
func NewStringReader(s string) io.ReadCloser {
return io.NopCloser(strings.NewReader(s))
@@ -392,8 +551,8 @@ func GetGitHubTokenHierarchyTests() []GitHubTokenTestCase {
Name: "GH_README_GITHUB_TOKEN has highest priority",
SetupFunc: func(t *testing.T) func() {
t.Helper()
cleanup1 := SetEnv(t, "GH_README_GITHUB_TOKEN", "priority-token")
cleanup2 := SetEnv(t, "GITHUB_TOKEN", "fallback-token")
cleanup1 := SetEnv(t, appconstants.EnvGitHubToken, "priority-token")
cleanup2 := SetEnv(t, appconstants.EnvGitHubTokenStandard, appconstants.TokenFallback)
return func() {
cleanup1()
@@ -406,19 +565,19 @@ func GetGitHubTokenHierarchyTests() []GitHubTokenTestCase {
Name: "GITHUB_TOKEN as fallback",
SetupFunc: func(t *testing.T) func() {
t.Helper()
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
cleanup := SetEnv(t, "GITHUB_TOKEN", "fallback-token")
_ = os.Unsetenv(appconstants.EnvGitHubToken)
cleanup := SetEnv(t, appconstants.EnvGitHubTokenStandard, appconstants.TokenFallback)
return cleanup
},
ExpectedToken: "fallback-token",
ExpectedToken: appconstants.TokenFallback,
},
{
Name: "no environment variables",
SetupFunc: func(t *testing.T) func() {
t.Helper()
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
_ = os.Unsetenv("GITHUB_TOKEN")
_ = os.Unsetenv(appconstants.EnvGitHubToken)
_ = os.Unsetenv(appconstants.EnvGitHubTokenStandard)
return func() {}
},