mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-02-22 07:52:24 +00:00
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:
@@ -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() {}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user