mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-01-26 03:04:10 +00:00
* 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
620 lines
16 KiB
Go
620 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/ivuorinen/gh-action-readme/appconstants"
|
|
"github.com/ivuorinen/gh-action-readme/internal"
|
|
"github.com/ivuorinen/gh-action-readme/internal/wizard"
|
|
"github.com/ivuorinen/gh-action-readme/testutil"
|
|
)
|
|
|
|
// TestCLICommands tests the main CLI commands using subprocess execution.
|
|
func TestCLICommands(t *testing.T) {
|
|
t.Parallel()
|
|
// Build the binary for testing
|
|
binaryPath := buildTestBinary(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
setupFunc func(t *testing.T, tmpDir string)
|
|
wantExit int
|
|
wantStdout string
|
|
wantStderr string
|
|
}{
|
|
{
|
|
name: "version command",
|
|
args: []string{"version"},
|
|
wantExit: 0,
|
|
wantStdout: "dev",
|
|
},
|
|
{
|
|
name: "about command",
|
|
args: []string{"about"},
|
|
wantExit: 0,
|
|
wantStdout: "gh-action-readme: Generates README.md and HTML for GitHub Actions",
|
|
},
|
|
{
|
|
name: "help command",
|
|
args: []string{"--help"},
|
|
wantExit: 0,
|
|
wantStdout: "gh-action-readme is a CLI tool for parsing one or many action.yml files and " +
|
|
"generating informative, modern, and customizable documentation",
|
|
},
|
|
{
|
|
name: "gen command with valid action",
|
|
args: []string{"gen", "--output-format", "md"},
|
|
setupFunc: func(t *testing.T, tmpDir string) {
|
|
t.Helper()
|
|
createTestActionFile(t, tmpDir, appconstants.TestFixtureJavaScriptSimple)
|
|
},
|
|
wantExit: 0,
|
|
},
|
|
{
|
|
name: "gen command with theme flag",
|
|
args: []string{"gen", "--theme", "github", "--output-format", "json"},
|
|
setupFunc: func(t *testing.T, tmpDir string) {
|
|
t.Helper()
|
|
createTestActionFile(t, tmpDir, appconstants.TestFixtureJavaScriptSimple)
|
|
},
|
|
wantExit: 0,
|
|
},
|
|
{
|
|
name: "gen command with no action files",
|
|
args: []string{"gen"},
|
|
wantExit: 1,
|
|
wantStderr: "no GitHub Action files found for documentation generation [NO_ACTION_FILES]",
|
|
},
|
|
{
|
|
name: "validate command with valid action",
|
|
args: []string{"validate"},
|
|
setupFunc: func(t *testing.T, tmpDir string) {
|
|
t.Helper()
|
|
createTestActionFile(t, tmpDir, appconstants.TestFixtureJavaScriptSimple)
|
|
},
|
|
wantExit: 0,
|
|
wantStdout: "All validations passed successfully",
|
|
},
|
|
{
|
|
name: "validate command with invalid action",
|
|
args: []string{"validate"},
|
|
setupFunc: func(t *testing.T, tmpDir string) {
|
|
t.Helper()
|
|
createTestActionFile(t, tmpDir, appconstants.TestFixtureInvalidMissingDescription)
|
|
},
|
|
wantExit: 1,
|
|
},
|
|
{
|
|
name: "schema command",
|
|
args: []string{"schema"},
|
|
wantExit: 0,
|
|
wantStdout: "schemas/action.schema.json",
|
|
},
|
|
{
|
|
name: "config command default",
|
|
args: []string{"config"},
|
|
wantExit: 0,
|
|
wantStdout: "Configuration file location:",
|
|
},
|
|
{
|
|
name: "config show command",
|
|
args: []string{"config", "show"},
|
|
wantExit: 0,
|
|
wantStdout: "Current Configuration:",
|
|
},
|
|
{
|
|
name: "config themes command",
|
|
args: []string{"config", "themes"},
|
|
wantExit: 0,
|
|
wantStdout: "Available Themes:",
|
|
},
|
|
{
|
|
name: "deps list command no files",
|
|
args: []string{"deps", "list"},
|
|
wantExit: 0, // Changed: deps list now outputs warning instead of error when no files found
|
|
wantStdout: "No action files found",
|
|
},
|
|
{
|
|
name: "deps list command with composite action",
|
|
args: []string{"deps", "list"},
|
|
setupFunc: func(t *testing.T, tmpDir string) {
|
|
t.Helper()
|
|
actionPath := filepath.Join(tmpDir, appconstants.TestPathActionYML)
|
|
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture(appconstants.TestFixtureCompositeBasic))
|
|
},
|
|
wantExit: 0,
|
|
},
|
|
{
|
|
name: "cache path command",
|
|
args: []string{"cache", "path"},
|
|
wantExit: 0,
|
|
wantStdout: "Cache Directory:",
|
|
},
|
|
{
|
|
name: "cache stats command",
|
|
args: []string{"cache", "stats"},
|
|
wantExit: 0,
|
|
wantStdout: "Cache Statistics:",
|
|
},
|
|
{
|
|
name: "invalid command",
|
|
args: []string{"invalid-command"},
|
|
wantExit: 1,
|
|
wantStderr: "unknown command",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create temporary directory for test
|
|
tmpDir, cleanup := testutil.TempDir(t)
|
|
defer cleanup()
|
|
|
|
// Setup test environment if needed
|
|
if tt.setupFunc != nil {
|
|
tt.setupFunc(t, tmpDir)
|
|
}
|
|
|
|
// Run the command in the temporary directory
|
|
result := runTestCommand(binaryPath, tt.args, tmpDir)
|
|
assertCommandResult(t, result, tt.wantExit, tt.wantStdout, tt.wantStderr)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCLIFlags tests various flag combinations.
|
|
func TestCLIFlags(t *testing.T) {
|
|
t.Parallel()
|
|
binaryPath := buildTestBinary(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
wantExit int
|
|
contains string
|
|
}{
|
|
{
|
|
name: "verbose flag",
|
|
args: []string{"--verbose", "config", "show"},
|
|
wantExit: 0,
|
|
contains: "Current Configuration:",
|
|
},
|
|
{
|
|
name: "quiet flag",
|
|
args: []string{"--quiet", "config", "show"},
|
|
wantExit: 0,
|
|
},
|
|
{
|
|
name: "config file flag",
|
|
args: []string{"--config", "nonexistent.yml", "config", "show"},
|
|
wantExit: 1,
|
|
},
|
|
{
|
|
name: "help flag",
|
|
args: []string{"-h"},
|
|
wantExit: 0,
|
|
contains: "Usage:",
|
|
},
|
|
{
|
|
name: "version short flag",
|
|
args: []string{"-v", "version"}, // -v is verbose, not version
|
|
wantExit: 0,
|
|
contains: "dev",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir, cleanup := testutil.TempDir(t)
|
|
defer cleanup()
|
|
|
|
result := runTestCommand(binaryPath, tt.args, tmpDir)
|
|
|
|
if result.exitCode != tt.wantExit {
|
|
t.Errorf(appconstants.TestMsgExitCode, tt.wantExit, result.exitCode)
|
|
t.Logf(appconstants.TestMsgStdout, result.stdout)
|
|
t.Logf(appconstants.TestMsgStderr, result.stderr)
|
|
}
|
|
|
|
if tt.contains != "" {
|
|
// For contains check, look in both stdout and stderr
|
|
assertCommandResult(t, result, tt.wantExit, tt.contains, "")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCLIRecursiveFlag tests the recursive flag functionality.
|
|
func TestCLIRecursiveFlag(t *testing.T) {
|
|
t.Parallel()
|
|
binaryPath := buildTestBinary(t)
|
|
|
|
tmpDir, cleanup := testutil.TempDir(t)
|
|
defer cleanup()
|
|
|
|
// Create nested directory structure with action files
|
|
testutil.WriteActionFixture(t, tmpDir, appconstants.TestFixtureJavaScriptSimple)
|
|
testutil.CreateActionSubdir(t, tmpDir, appconstants.TestDirSubdir, appconstants.TestFixtureCompositeBasic)
|
|
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
wantExit int
|
|
minFiles int // minimum number of files that should be processed
|
|
}{
|
|
{
|
|
name: "without recursive flag",
|
|
args: []string{"gen", "--output-format", "json"},
|
|
wantExit: 0,
|
|
minFiles: 1, // should only process root action.yml
|
|
},
|
|
{
|
|
name: "with recursive flag",
|
|
args: []string{"gen", "--recursive", "--output-format", "json"},
|
|
wantExit: 0,
|
|
minFiles: 2, // should process both action.yml files
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := runTestCommand(binaryPath, tt.args, tmpDir)
|
|
assertCommandResult(t, result, tt.wantExit, "", "")
|
|
|
|
// For recursive tests, check that appropriate number of files were processed
|
|
// This is a simple heuristic - could be made more sophisticated
|
|
if tt.minFiles > 1 && !strings.Contains(result.stdout, appconstants.TestDirSubdir) {
|
|
t.Errorf("expected recursive processing to include subdirectory")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCLIErrorHandling tests error scenarios.
|
|
func TestCLIErrorHandling(t *testing.T) {
|
|
t.Parallel()
|
|
binaryPath := buildTestBinary(t)
|
|
|
|
tests := []struct {
|
|
name string
|
|
args []string
|
|
setupFunc func(t *testing.T, tmpDir string)
|
|
wantExit int
|
|
wantError string
|
|
}{
|
|
{
|
|
name: "permission denied on output directory",
|
|
args: []string{"gen", "--output-dir", "/root/restricted"},
|
|
setupFunc: func(t *testing.T, tmpDir string) {
|
|
t.Helper()
|
|
createTestActionFile(t, tmpDir, appconstants.TestFixtureJavaScriptSimple)
|
|
},
|
|
wantExit: 1,
|
|
wantError: "encountered 1 errors during batch processing",
|
|
},
|
|
{
|
|
name: "invalid YAML in action file",
|
|
args: []string{"validate"},
|
|
setupFunc: func(t *testing.T, tmpDir string) {
|
|
t.Helper()
|
|
testutil.WriteTestFile(
|
|
t,
|
|
filepath.Join(tmpDir, appconstants.TestPathActionYML),
|
|
"invalid: yaml: content: [",
|
|
)
|
|
},
|
|
wantExit: 1,
|
|
},
|
|
{
|
|
name: "unknown output format",
|
|
args: []string{"gen", "--output-format", "unknown"},
|
|
setupFunc: func(t *testing.T, tmpDir string) {
|
|
t.Helper()
|
|
createTestActionFile(t, tmpDir, appconstants.TestFixtureJavaScriptSimple)
|
|
},
|
|
wantExit: 1,
|
|
},
|
|
{
|
|
name: "unknown theme",
|
|
args: []string{"gen", "--theme", "nonexistent-theme"},
|
|
setupFunc: func(t *testing.T, tmpDir string) {
|
|
t.Helper()
|
|
createTestActionFile(t, tmpDir, appconstants.TestFixtureJavaScriptSimple)
|
|
},
|
|
wantExit: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir, cleanup := testutil.TempDir(t)
|
|
defer cleanup()
|
|
|
|
if tt.setupFunc != nil {
|
|
tt.setupFunc(t, tmpDir)
|
|
}
|
|
|
|
result := runTestCommand(binaryPath, tt.args, tmpDir)
|
|
|
|
if result.exitCode != tt.wantExit {
|
|
t.Errorf(appconstants.TestMsgExitCode, tt.wantExit, result.exitCode)
|
|
t.Logf(appconstants.TestMsgStdout, result.stdout)
|
|
t.Logf(appconstants.TestMsgStderr, result.stderr)
|
|
}
|
|
|
|
if tt.wantError != "" {
|
|
output := result.stdout + result.stderr
|
|
if !strings.Contains(strings.ToLower(output), strings.ToLower(tt.wantError)) {
|
|
t.Errorf("expected error containing %q, got: %s", tt.wantError, output)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCLIConfigInitialization tests configuration initialization.
|
|
func TestCLIConfigInitialization(t *testing.T) {
|
|
t.Parallel()
|
|
binaryPath := buildTestBinary(t)
|
|
|
|
tmpDir, cleanup := testutil.TempDir(t)
|
|
defer cleanup()
|
|
|
|
// Test config init command
|
|
cmd := exec.Command(binaryPath, "config", "init") // #nosec G204 -- controlled test input
|
|
cmd.Dir = tmpDir
|
|
|
|
// Set XDG_CONFIG_HOME to temp directory
|
|
cmd.Env = append(os.Environ(), "XDG_CONFIG_HOME="+tmpDir)
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() != 0 {
|
|
t.Errorf("config init failed: %v\nstdout: %s\nstderr: %s", err, stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
// Check if config file was created (note: uses .yaml extension, not .yml)
|
|
expectedConfigPath := filepath.Join(tmpDir, "gh-action-readme", "config.yaml")
|
|
testutil.AssertFileExists(t, expectedConfigPath)
|
|
}
|
|
|
|
// Unit Tests for Helper Functions
|
|
// These test the actual functions directly rather than through subprocess execution.
|
|
|
|
func TestCreateOutputManager(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
quiet bool
|
|
}{
|
|
{"normal mode", false},
|
|
{"quiet mode", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
output := createOutputManager(tt.quiet)
|
|
if output == nil {
|
|
t.Fatal("createOutputManager returned nil")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatSize(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
size int64
|
|
expected string
|
|
}{
|
|
{"zero bytes", 0, "0 bytes"},
|
|
{"bytes", 500, "500 bytes"},
|
|
{"kilobyte boundary", 1024, "1.00 KB"},
|
|
{"kilobytes", 2048, "2.00 KB"},
|
|
{"megabyte boundary", 1024 * 1024, "1.00 MB"},
|
|
{"megabytes", 5 * 1024 * 1024, "5.00 MB"},
|
|
{"gigabyte boundary", 1024 * 1024 * 1024, "1.00 GB"},
|
|
{"gigabytes", 3 * 1024 * 1024 * 1024, "3.00 GB"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := formatSize(tt.size)
|
|
if result != tt.expected {
|
|
t.Errorf("formatSize(%d) = %q, want %q", tt.size, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveExportFormat(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
format string
|
|
expected wizard.ExportFormat
|
|
}{
|
|
{"json format", appconstants.OutputFormatJSON, wizard.FormatJSON},
|
|
{"toml format", appconstants.OutputFormatTOML, wizard.FormatTOML},
|
|
{"yaml format", appconstants.OutputFormatYAML, wizard.FormatYAML},
|
|
{"default format", "unknown", wizard.FormatYAML},
|
|
{"empty format", "", wizard.FormatYAML},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := resolveExportFormat(tt.format)
|
|
if result != tt.expected {
|
|
t.Errorf("resolveExportFormat(%q) = %v, want %v", tt.format, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateErrorHandler(t *testing.T) {
|
|
t.Parallel()
|
|
output := internal.NewColoredOutput(false)
|
|
handler := createErrorHandler(output)
|
|
|
|
if handler == nil {
|
|
t.Fatal("createErrorHandler returned nil")
|
|
}
|
|
}
|
|
|
|
func TestSetupOutputAndErrorHandling(t *testing.T) {
|
|
// Note: This test cannot use t.Parallel() because it modifies globalConfig
|
|
// Setup globalConfig for the test
|
|
originalConfig := globalConfig
|
|
defer func() { globalConfig = originalConfig }()
|
|
|
|
globalConfig = &internal.AppConfig{Quiet: false}
|
|
|
|
output, errorHandler := setupOutputAndErrorHandling()
|
|
|
|
if output == nil {
|
|
t.Fatal("setupOutputAndErrorHandling returned nil output")
|
|
}
|
|
if errorHandler == nil {
|
|
t.Fatal("setupOutputAndErrorHandling returned nil errorHandler")
|
|
}
|
|
}
|
|
|
|
// Unit Tests for Command Creation Functions
|
|
|
|
func TestNewGenCmd(t *testing.T) {
|
|
t.Parallel()
|
|
cmd := newGenCmd()
|
|
|
|
if cmd.Use != "gen [directory_or_file]" {
|
|
t.Errorf("expected Use to be 'gen [directory_or_file]', got %q", cmd.Use)
|
|
}
|
|
|
|
if cmd.Short == "" {
|
|
t.Error("expected Short description to be non-empty")
|
|
}
|
|
|
|
if cmd.RunE == nil && cmd.Run == nil {
|
|
t.Error("expected command to have a Run or RunE function")
|
|
}
|
|
|
|
// Check that required flags exist
|
|
flags := []string{"output-format", "output-dir", "theme", "recursive"}
|
|
for _, flag := range flags {
|
|
if cmd.Flags().Lookup(flag) == nil {
|
|
t.Errorf("expected flag %q to exist", flag)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewValidateCmd(t *testing.T) {
|
|
t.Parallel()
|
|
cmd := newValidateCmd()
|
|
|
|
if cmd.Use != "validate" {
|
|
t.Errorf("expected Use to be 'validate', got %q", cmd.Use)
|
|
}
|
|
|
|
if cmd.Short == "" {
|
|
t.Error("expected Short description to be non-empty")
|
|
}
|
|
|
|
if cmd.RunE == nil && cmd.Run == nil {
|
|
t.Error("expected command to have a Run or RunE function")
|
|
}
|
|
}
|
|
|
|
func TestNewSchemaCmd(t *testing.T) {
|
|
t.Parallel()
|
|
cmd := newSchemaCmd()
|
|
|
|
if cmd.Use != "schema" {
|
|
t.Errorf("expected Use to be 'schema', got %q", cmd.Use)
|
|
}
|
|
|
|
if cmd.Short == "" {
|
|
t.Error("expected Short description to be non-empty")
|
|
}
|
|
|
|
if cmd.RunE == nil && cmd.Run == nil {
|
|
t.Error("expected command to have a Run or RunE function")
|
|
}
|
|
}
|
|
|
|
// cmdResult holds the results of a command execution.
|
|
type cmdResult struct {
|
|
stdout string
|
|
stderr string
|
|
exitCode int
|
|
}
|
|
|
|
// runTestCommand executes a command with the given args in the specified directory.
|
|
// It returns the stdout, stderr, and exit code.
|
|
func runTestCommand(binaryPath string, args []string, dir string) cmdResult {
|
|
cmd := exec.Command(binaryPath, args...) // #nosec G204 -- controlled test input
|
|
cmd.Dir = dir
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
exitCode := 0
|
|
if err != nil {
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
exitCode = exitError.ExitCode()
|
|
}
|
|
}
|
|
|
|
return cmdResult{
|
|
stdout: stdout.String(),
|
|
stderr: stderr.String(),
|
|
exitCode: exitCode,
|
|
}
|
|
}
|
|
|
|
// createTestActionFile is a helper that creates a test action file from a fixture.
|
|
// It writes the specified fixture to action.yml in the given temporary directory.
|
|
func createTestActionFile(t *testing.T, tmpDir, fixture string) {
|
|
t.Helper()
|
|
actionPath := filepath.Join(tmpDir, appconstants.TestPathActionYML)
|
|
testutil.WriteTestFile(t, actionPath, testutil.MustReadFixture(fixture))
|
|
}
|
|
|
|
// assertCommandResult is a helper that asserts the result of a command execution.
|
|
// It checks the exit code, and optionally checks for expected content in stdout and stderr.
|
|
func assertCommandResult(t *testing.T, result cmdResult, wantExit int, wantStdout, wantStderr string) {
|
|
t.Helper()
|
|
|
|
if result.exitCode != wantExit {
|
|
t.Errorf(appconstants.TestMsgExitCode, wantExit, result.exitCode)
|
|
t.Logf(appconstants.TestMsgStdout, result.stdout)
|
|
t.Logf(appconstants.TestMsgStderr, result.stderr)
|
|
}
|
|
|
|
// Check stdout if specified
|
|
if wantStdout != "" {
|
|
if !strings.Contains(result.stdout, wantStdout) {
|
|
t.Errorf("expected stdout to contain %q, got: %s", wantStdout, result.stdout)
|
|
}
|
|
}
|
|
|
|
// Check stderr if specified
|
|
if wantStderr != "" {
|
|
if !strings.Contains(result.stderr, wantStderr) {
|
|
t.Errorf("expected stderr to contain %q, got: %s", wantStderr, result.stderr)
|
|
}
|
|
}
|
|
}
|