mirror of
https://github.com/ivuorinen/gh-action-readme.git
synced 2026-02-07 09:46:47 +00:00
This commit represents a comprehensive refactoring of the codebase focused on improving code quality, testability, and maintainability. Key improvements: - Implement dependency injection and interface-based architecture - Add comprehensive test framework with fixtures and test suites - Fix all linting issues (errcheck, gosec, staticcheck, goconst, etc.) - Achieve full EditorConfig compliance across all files - Replace hardcoded test data with proper fixture files - Add configuration loader with hierarchical config support - Improve error handling with contextual information - Add progress indicators for better user feedback - Enhance Makefile with help system and improved editorconfig commands - Consolidate constants and remove deprecated code - Strengthen validation logic for GitHub Actions - Add focused consumer interfaces for better separation of concerns Testing improvements: - Add comprehensive integration tests - Implement test executor pattern for better test organization - Create extensive YAML fixture library for testing - Fix all failing tests and improve test coverage - Add validation test fixtures to avoid embedded YAML in Go files Build and tooling: - Update Makefile to show help by default - Fix editorconfig commands to use eclint properly - Add comprehensive help documentation to all make targets - Improve file selection patterns to avoid glob errors This refactoring maintains backward compatibility while significantly improving the internal architecture and developer experience.
803 lines
21 KiB
Go
803 lines
21 KiB
Go
package internal
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/ivuorinen/gh-action-readme/testutil"
|
|
)
|
|
|
|
func TestNewConfigurationLoader(t *testing.T) {
|
|
loader := NewConfigurationLoader()
|
|
|
|
if loader == nil {
|
|
t.Fatal("expected non-nil loader")
|
|
}
|
|
|
|
if loader.viper == nil {
|
|
t.Fatal("expected viper instance to be initialized")
|
|
}
|
|
|
|
// Check default sources are enabled
|
|
expectedSources := []ConfigurationSource{
|
|
SourceDefaults, SourceGlobal, SourceRepoOverride,
|
|
SourceRepoConfig, SourceActionConfig, SourceEnvironment,
|
|
}
|
|
|
|
for _, source := range expectedSources {
|
|
if !loader.sources[source] {
|
|
t.Errorf("expected source %s to be enabled by default", source.String())
|
|
}
|
|
}
|
|
|
|
// CLI flags should be disabled by default
|
|
if loader.sources[SourceCLIFlags] {
|
|
t.Error("expected CLI flags source to be disabled by default")
|
|
}
|
|
}
|
|
|
|
func TestNewConfigurationLoaderWithOptions(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
opts ConfigurationOptions
|
|
expected []ConfigurationSource
|
|
}{
|
|
{
|
|
name: "default options",
|
|
opts: ConfigurationOptions{},
|
|
expected: []ConfigurationSource{
|
|
SourceDefaults, SourceGlobal, SourceRepoOverride,
|
|
SourceRepoConfig, SourceActionConfig, SourceEnvironment,
|
|
},
|
|
},
|
|
{
|
|
name: "custom enabled sources",
|
|
opts: ConfigurationOptions{
|
|
EnabledSources: []ConfigurationSource{SourceDefaults, SourceGlobal},
|
|
},
|
|
expected: []ConfigurationSource{SourceDefaults, SourceGlobal},
|
|
},
|
|
{
|
|
name: "all sources enabled",
|
|
opts: ConfigurationOptions{
|
|
EnabledSources: []ConfigurationSource{
|
|
SourceDefaults, SourceGlobal, SourceRepoOverride,
|
|
SourceRepoConfig, SourceActionConfig, SourceEnvironment, SourceCLIFlags,
|
|
},
|
|
},
|
|
expected: []ConfigurationSource{
|
|
SourceDefaults, SourceGlobal, SourceRepoOverride,
|
|
SourceRepoConfig, SourceActionConfig, SourceEnvironment, SourceCLIFlags,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
loader := NewConfigurationLoaderWithOptions(tt.opts)
|
|
|
|
for _, expectedSource := range tt.expected {
|
|
if !loader.sources[expectedSource] {
|
|
t.Errorf("expected source %s to be enabled", expectedSource.String())
|
|
}
|
|
}
|
|
|
|
// Check that non-expected sources are disabled
|
|
allSources := []ConfigurationSource{
|
|
SourceDefaults, SourceGlobal, SourceRepoOverride,
|
|
SourceRepoConfig, SourceActionConfig, SourceEnvironment, SourceCLIFlags,
|
|
}
|
|
|
|
for _, source := range allSources {
|
|
expected := false
|
|
for _, expectedSource := range tt.expected {
|
|
if source == expectedSource {
|
|
expected = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if loader.sources[source] != expected {
|
|
t.Errorf("source %s enabled=%v, expected=%v", source.String(), loader.sources[source], expected)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigurationLoader_LoadConfiguration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupFunc func(t *testing.T, tempDir string) (configFile, repoRoot, actionDir string)
|
|
expectError bool
|
|
checkFunc func(t *testing.T, config *AppConfig)
|
|
}{
|
|
{
|
|
name: "defaults only",
|
|
setupFunc: func(_ *testing.T, _ string) (string, string, string) {
|
|
return "", "", ""
|
|
},
|
|
checkFunc: func(_ *testing.T, config *AppConfig) {
|
|
testutil.AssertEqual(t, "default", config.Theme)
|
|
testutil.AssertEqual(t, "md", config.OutputFormat)
|
|
testutil.AssertEqual(t, ".", config.OutputDir)
|
|
},
|
|
},
|
|
{
|
|
name: "multi-level configuration hierarchy",
|
|
setupFunc: func(_ *testing.T, tempDir string) (string, string, string) {
|
|
// Create global config
|
|
globalConfigDir := filepath.Join(tempDir, ".config", "gh-action-readme")
|
|
_ = os.MkdirAll(globalConfigDir, 0750) // #nosec G301 -- test directory permissions
|
|
globalConfigPath := filepath.Join(globalConfigDir, "config.yaml")
|
|
testutil.WriteTestFile(t, globalConfigPath, `
|
|
theme: default
|
|
output_format: md
|
|
github_token: global-token
|
|
verbose: false
|
|
`)
|
|
|
|
// Create repo root with repo-specific config
|
|
repoRoot := filepath.Join(tempDir, "repo")
|
|
_ = os.MkdirAll(repoRoot, 0750) // #nosec G301 -- test directory permissions
|
|
testutil.WriteTestFile(t, filepath.Join(repoRoot, ".ghreadme.yaml"), `
|
|
theme: github
|
|
output_format: html
|
|
verbose: true
|
|
`)
|
|
|
|
// Create action directory with action-specific config
|
|
actionDir := filepath.Join(repoRoot, "action")
|
|
_ = os.MkdirAll(actionDir, 0750) // #nosec G301 -- test directory permissions
|
|
testutil.WriteTestFile(t, filepath.Join(actionDir, "config.yaml"), `
|
|
theme: professional
|
|
output_dir: output
|
|
quiet: false
|
|
`)
|
|
|
|
return globalConfigPath, repoRoot, actionDir
|
|
},
|
|
checkFunc: func(_ *testing.T, config *AppConfig) {
|
|
// Should have action-level overrides
|
|
testutil.AssertEqual(t, "professional", config.Theme)
|
|
testutil.AssertEqual(t, "output", config.OutputDir)
|
|
// Should inherit from repo level
|
|
testutil.AssertEqual(t, "html", config.OutputFormat)
|
|
testutil.AssertEqual(t, true, config.Verbose)
|
|
// Should inherit GitHub token from global config
|
|
testutil.AssertEqual(t, "global-token", config.GitHubToken)
|
|
},
|
|
},
|
|
{
|
|
name: "environment variable overrides",
|
|
setupFunc: func(_ *testing.T, tempDir string) (string, string, string) {
|
|
// Set environment variables
|
|
_ = os.Setenv("GH_README_GITHUB_TOKEN", "env-token")
|
|
t.Cleanup(func() {
|
|
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
|
})
|
|
|
|
// Create config file with different token
|
|
configPath := filepath.Join(tempDir, "config.yml")
|
|
testutil.WriteTestFile(t, configPath, `
|
|
theme: minimal
|
|
github_token: config-token
|
|
`)
|
|
|
|
return configPath, tempDir, ""
|
|
},
|
|
checkFunc: func(_ *testing.T, config *AppConfig) {
|
|
// Environment variable should override config file
|
|
testutil.AssertEqual(t, "env-token", config.GitHubToken)
|
|
testutil.AssertEqual(t, "minimal", config.Theme)
|
|
},
|
|
},
|
|
{
|
|
name: "hidden config file priority",
|
|
setupFunc: func(_ *testing.T, tempDir string) (string, string, string) {
|
|
repoRoot := filepath.Join(tempDir, "repo")
|
|
_ = os.MkdirAll(repoRoot, 0750) // #nosec G301 -- test directory permissions
|
|
|
|
// Create multiple hidden config files - first one should win
|
|
testutil.WriteTestFile(t, filepath.Join(repoRoot, ".ghreadme.yaml"), `
|
|
theme: minimal
|
|
output_format: json
|
|
`)
|
|
|
|
configDir := filepath.Join(repoRoot, ".config")
|
|
_ = os.MkdirAll(configDir, 0750) // #nosec G301 -- test directory permissions
|
|
testutil.WriteTestFile(t, filepath.Join(configDir, "ghreadme.yaml"), `
|
|
theme: professional
|
|
quiet: true
|
|
`)
|
|
|
|
githubDir := filepath.Join(repoRoot, ".github")
|
|
_ = os.MkdirAll(githubDir, 0750) // #nosec G301 -- test directory permissions
|
|
testutil.WriteTestFile(t, filepath.Join(githubDir, "ghreadme.yaml"), `
|
|
theme: github
|
|
verbose: true
|
|
`)
|
|
|
|
return "", repoRoot, ""
|
|
},
|
|
checkFunc: func(_ *testing.T, config *AppConfig) {
|
|
// Should use the first found config (.ghreadme.yaml has priority)
|
|
testutil.AssertEqual(t, "minimal", config.Theme)
|
|
testutil.AssertEqual(t, "json", config.OutputFormat)
|
|
},
|
|
},
|
|
{
|
|
name: "selective source loading",
|
|
setupFunc: func(_ *testing.T, _ string) (string, string, string) {
|
|
// This test uses a loader with specific sources enabled
|
|
return "", "", ""
|
|
},
|
|
checkFunc: func(_ *testing.T, _ *AppConfig) {
|
|
// This will be tested with a custom loader
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir, cleanup := testutil.TempDir(t)
|
|
defer cleanup()
|
|
|
|
// Set HOME to temp directory for fallback
|
|
originalHome := os.Getenv("HOME")
|
|
_ = os.Setenv("HOME", tmpDir)
|
|
defer func() {
|
|
if originalHome != "" {
|
|
_ = os.Setenv("HOME", originalHome)
|
|
} else {
|
|
_ = os.Unsetenv("HOME")
|
|
}
|
|
}()
|
|
|
|
configFile, repoRoot, actionDir := tt.setupFunc(t, tmpDir)
|
|
|
|
// Special handling for selective source loading test
|
|
var loader *ConfigurationLoader
|
|
if tt.name == "selective source loading" {
|
|
// Create loader with only defaults and global sources
|
|
loader = NewConfigurationLoaderWithOptions(ConfigurationOptions{
|
|
EnabledSources: []ConfigurationSource{SourceDefaults, SourceGlobal},
|
|
})
|
|
} else {
|
|
loader = NewConfigurationLoader()
|
|
}
|
|
|
|
config, err := loader.LoadConfiguration(configFile, repoRoot, actionDir)
|
|
|
|
if tt.expectError {
|
|
testutil.AssertError(t, err)
|
|
return
|
|
}
|
|
|
|
testutil.AssertNoError(t, err)
|
|
|
|
if tt.checkFunc != nil {
|
|
tt.checkFunc(t, config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigurationLoader_LoadGlobalConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupFunc func(t *testing.T, tempDir string) string
|
|
expectError bool
|
|
checkFunc func(t *testing.T, config *AppConfig)
|
|
}{
|
|
{
|
|
name: "valid global config",
|
|
setupFunc: func(t *testing.T, tempDir string) string {
|
|
configPath := filepath.Join(tempDir, "config.yaml")
|
|
testutil.WriteTestFile(t, configPath, `
|
|
theme: professional
|
|
output_format: html
|
|
github_token: test-token
|
|
verbose: true
|
|
`)
|
|
return configPath
|
|
},
|
|
checkFunc: func(_ *testing.T, config *AppConfig) {
|
|
testutil.AssertEqual(t, "professional", config.Theme)
|
|
testutil.AssertEqual(t, "html", config.OutputFormat)
|
|
testutil.AssertEqual(t, "test-token", config.GitHubToken)
|
|
testutil.AssertEqual(t, true, config.Verbose)
|
|
},
|
|
},
|
|
{
|
|
name: "nonexistent config file",
|
|
setupFunc: func(_ *testing.T, tempDir string) string {
|
|
return filepath.Join(tempDir, "nonexistent.yaml")
|
|
},
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "invalid YAML",
|
|
setupFunc: func(t *testing.T, tempDir string) string {
|
|
configPath := filepath.Join(tempDir, "invalid.yaml")
|
|
testutil.WriteTestFile(t, configPath, "invalid: yaml: content: [")
|
|
return configPath
|
|
},
|
|
expectError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir, cleanup := testutil.TempDir(t)
|
|
defer cleanup()
|
|
|
|
// Set HOME to temp directory
|
|
originalHome := os.Getenv("HOME")
|
|
_ = os.Setenv("HOME", tmpDir)
|
|
defer func() {
|
|
if originalHome != "" {
|
|
_ = os.Setenv("HOME", originalHome)
|
|
} else {
|
|
_ = os.Unsetenv("HOME")
|
|
}
|
|
}()
|
|
|
|
configFile := tt.setupFunc(t, tmpDir)
|
|
|
|
loader := NewConfigurationLoader()
|
|
config, err := loader.LoadGlobalConfig(configFile)
|
|
|
|
if tt.expectError {
|
|
testutil.AssertError(t, err)
|
|
return
|
|
}
|
|
|
|
testutil.AssertNoError(t, err)
|
|
|
|
if tt.checkFunc != nil {
|
|
tt.checkFunc(t, config)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigurationLoader_ValidateConfiguration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *AppConfig
|
|
expectError bool
|
|
errorMsg string
|
|
}{
|
|
{
|
|
name: "nil config",
|
|
config: nil,
|
|
expectError: true,
|
|
errorMsg: "configuration cannot be nil",
|
|
},
|
|
{
|
|
name: "valid config",
|
|
config: &AppConfig{
|
|
Theme: "default",
|
|
OutputFormat: "md",
|
|
OutputDir: ".",
|
|
Verbose: false,
|
|
Quiet: false,
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "invalid output format",
|
|
config: &AppConfig{
|
|
Theme: "default",
|
|
OutputFormat: "invalid",
|
|
OutputDir: ".",
|
|
},
|
|
expectError: true,
|
|
errorMsg: "invalid output format",
|
|
},
|
|
{
|
|
name: "empty output directory",
|
|
config: &AppConfig{
|
|
Theme: "default",
|
|
OutputFormat: "md",
|
|
OutputDir: "",
|
|
},
|
|
expectError: true,
|
|
errorMsg: "output directory cannot be empty",
|
|
},
|
|
{
|
|
name: "verbose and quiet both true",
|
|
config: &AppConfig{
|
|
Theme: "default",
|
|
OutputFormat: "md",
|
|
OutputDir: ".",
|
|
Verbose: true,
|
|
Quiet: true,
|
|
},
|
|
expectError: true,
|
|
errorMsg: "verbose and quiet flags are mutually exclusive",
|
|
},
|
|
{
|
|
name: "invalid theme",
|
|
config: &AppConfig{
|
|
Theme: "nonexistent",
|
|
OutputFormat: "md",
|
|
OutputDir: ".",
|
|
},
|
|
expectError: true,
|
|
errorMsg: "invalid theme",
|
|
},
|
|
{
|
|
name: "valid built-in themes",
|
|
config: &AppConfig{
|
|
Theme: "github",
|
|
OutputFormat: "html",
|
|
OutputDir: "docs",
|
|
},
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
loader := NewConfigurationLoader()
|
|
err := loader.ValidateConfiguration(tt.config)
|
|
|
|
if tt.expectError {
|
|
testutil.AssertError(t, err)
|
|
if tt.errorMsg != "" {
|
|
testutil.AssertStringContains(t, err.Error(), tt.errorMsg)
|
|
}
|
|
} else {
|
|
testutil.AssertNoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigurationLoader_SourceManagement(t *testing.T) {
|
|
loader := NewConfigurationLoader()
|
|
|
|
// Test initial state
|
|
sources := loader.GetConfigurationSources()
|
|
if len(sources) != 6 { // All except CLI flags
|
|
t.Errorf("expected 6 enabled sources, got %d", len(sources))
|
|
}
|
|
|
|
// Test disabling a source
|
|
loader.DisableSource(SourceGlobal)
|
|
if loader.sources[SourceGlobal] {
|
|
t.Error("expected SourceGlobal to be disabled")
|
|
}
|
|
|
|
// Test enabling a source
|
|
loader.EnableSource(SourceCLIFlags)
|
|
if !loader.sources[SourceCLIFlags] {
|
|
t.Error("expected SourceCLIFlags to be enabled")
|
|
}
|
|
|
|
// Test updated sources list
|
|
sources = loader.GetConfigurationSources()
|
|
expectedCount := 6 // 5 original + CLI flags - Global
|
|
if len(sources) != expectedCount {
|
|
t.Errorf("expected %d enabled sources, got %d", expectedCount, len(sources))
|
|
}
|
|
}
|
|
|
|
func TestConfigurationSource_String(t *testing.T) {
|
|
tests := []struct {
|
|
source ConfigurationSource
|
|
expected string
|
|
}{
|
|
{SourceDefaults, "defaults"},
|
|
{SourceGlobal, "global"},
|
|
{SourceRepoOverride, "repo-override"},
|
|
{SourceRepoConfig, "repo-config"},
|
|
{SourceActionConfig, "action-config"},
|
|
{SourceEnvironment, "environment"},
|
|
{SourceCLIFlags, "cli-flags"},
|
|
{ConfigurationSource(999), "unknown"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := tt.source.String()
|
|
if result != tt.expected {
|
|
t.Errorf("source %d String() = %s, expected %s", int(tt.source), result, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConfigurationLoader_EnvironmentOverrides(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupFunc func(t *testing.T) func()
|
|
expectedToken string
|
|
}{
|
|
{
|
|
name: "GH_README_GITHUB_TOKEN priority",
|
|
setupFunc: func(_ *testing.T) func() {
|
|
_ = os.Setenv("GH_README_GITHUB_TOKEN", "priority-token")
|
|
_ = os.Setenv("GITHUB_TOKEN", "fallback-token")
|
|
return func() {
|
|
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
|
_ = os.Unsetenv("GITHUB_TOKEN")
|
|
}
|
|
},
|
|
expectedToken: "priority-token",
|
|
},
|
|
{
|
|
name: "GITHUB_TOKEN fallback",
|
|
setupFunc: func(_ *testing.T) func() {
|
|
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
|
_ = os.Setenv("GITHUB_TOKEN", "fallback-token")
|
|
return func() {
|
|
_ = os.Unsetenv("GITHUB_TOKEN")
|
|
}
|
|
},
|
|
expectedToken: "fallback-token",
|
|
},
|
|
{
|
|
name: "no environment variables",
|
|
setupFunc: func(_ *testing.T) func() {
|
|
_ = os.Unsetenv("GH_README_GITHUB_TOKEN")
|
|
_ = os.Unsetenv("GITHUB_TOKEN")
|
|
return func() {}
|
|
},
|
|
expectedToken: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cleanup := tt.setupFunc(t)
|
|
defer cleanup()
|
|
|
|
tmpDir, tmpCleanup := testutil.TempDir(t)
|
|
defer tmpCleanup()
|
|
|
|
loader := NewConfigurationLoader()
|
|
config, err := loader.LoadConfiguration("", tmpDir, "")
|
|
testutil.AssertNoError(t, err)
|
|
|
|
testutil.AssertEqual(t, tt.expectedToken, config.GitHubToken)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConfigurationLoader_RepoOverrides(t *testing.T) {
|
|
tmpDir, cleanup := testutil.TempDir(t)
|
|
defer cleanup()
|
|
|
|
// Create a mock git repository structure for testing
|
|
repoRoot := filepath.Join(tmpDir, "test-repo")
|
|
_ = os.MkdirAll(repoRoot, 0750) // #nosec G301 -- test directory permissions
|
|
|
|
// Create global config with repo overrides
|
|
globalConfigDir := filepath.Join(tmpDir, ".config", "gh-action-readme")
|
|
_ = os.MkdirAll(globalConfigDir, 0750) // #nosec G301 -- test directory permissions
|
|
globalConfigPath := filepath.Join(globalConfigDir, "config.yaml")
|
|
globalConfigContent := "theme: default\n"
|
|
globalConfigContent += "output_format: md\n"
|
|
globalConfigContent += "repo_overrides:\n"
|
|
globalConfigContent += " test-repo:\n"
|
|
globalConfigContent += " theme: github\n"
|
|
globalConfigContent += " output_format: html\n"
|
|
globalConfigContent += " verbose: true\n"
|
|
testutil.WriteTestFile(t, globalConfigPath, globalConfigContent)
|
|
|
|
// Set environment for XDG compliance
|
|
originalHome := os.Getenv("HOME")
|
|
_ = os.Setenv("HOME", tmpDir)
|
|
defer func() {
|
|
if originalHome != "" {
|
|
_ = os.Setenv("HOME", originalHome)
|
|
} else {
|
|
_ = os.Unsetenv("HOME")
|
|
}
|
|
}()
|
|
|
|
loader := NewConfigurationLoader()
|
|
config, err := loader.LoadConfiguration(globalConfigPath, repoRoot, "")
|
|
testutil.AssertNoError(t, err)
|
|
|
|
// Note: Since we don't have actual git repository detection in this test,
|
|
// repo overrides won't be applied. This test validates the structure works.
|
|
testutil.AssertEqual(t, "default", config.Theme)
|
|
testutil.AssertEqual(t, "md", config.OutputFormat)
|
|
}
|
|
|
|
// TestConfigurationLoader_ApplyRepoOverrides tests repo-specific overrides.
|
|
func TestConfigurationLoader_ApplyRepoOverrides(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
config *AppConfig
|
|
expectedTheme string
|
|
expectedFormat string
|
|
}{
|
|
{
|
|
name: "no repo overrides configured",
|
|
config: &AppConfig{
|
|
Theme: "default",
|
|
OutputFormat: "md",
|
|
RepoOverrides: nil,
|
|
},
|
|
expectedTheme: "default",
|
|
expectedFormat: "md",
|
|
},
|
|
{
|
|
name: "empty repo overrides map",
|
|
config: &AppConfig{
|
|
Theme: "default",
|
|
OutputFormat: "md",
|
|
RepoOverrides: map[string]AppConfig{},
|
|
},
|
|
expectedTheme: "default",
|
|
expectedFormat: "md",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir, cleanup := testutil.TempDir(t)
|
|
defer cleanup()
|
|
|
|
loader := NewConfigurationLoader()
|
|
loader.applyRepoOverrides(tt.config, tmpDir)
|
|
testutil.AssertEqual(t, tt.expectedTheme, tt.config.Theme)
|
|
testutil.AssertEqual(t, tt.expectedFormat, tt.config.OutputFormat)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigurationLoader_LoadActionConfig tests action-specific configuration loading.
|
|
func TestConfigurationLoader_LoadActionConfig(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
setupFunc func(t *testing.T, tmpDir string) string
|
|
expectError bool
|
|
expectedVals map[string]string
|
|
}{
|
|
{
|
|
name: "no action directory provided",
|
|
setupFunc: func(_ *testing.T, _ string) string {
|
|
return ""
|
|
},
|
|
expectError: false,
|
|
expectedVals: map[string]string{},
|
|
},
|
|
{
|
|
name: "action directory with config file",
|
|
setupFunc: func(t *testing.T, tmpDir string) string {
|
|
actionDir := filepath.Join(tmpDir, "action")
|
|
_ = os.MkdirAll(actionDir, 0750) // #nosec G301 -- test directory permissions
|
|
|
|
configPath := filepath.Join(actionDir, "config.yaml")
|
|
testutil.WriteTestFile(t, configPath, `
|
|
theme: minimal
|
|
output_format: json
|
|
verbose: true
|
|
`)
|
|
return actionDir
|
|
},
|
|
expectError: false,
|
|
expectedVals: map[string]string{
|
|
"theme": "minimal",
|
|
"output_format": "json",
|
|
},
|
|
},
|
|
{
|
|
name: "action directory with malformed config file",
|
|
setupFunc: func(t *testing.T, tmpDir string) string {
|
|
actionDir := filepath.Join(tmpDir, "action")
|
|
_ = os.MkdirAll(actionDir, 0750) // #nosec G301 -- test directory permissions
|
|
|
|
configPath := filepath.Join(actionDir, "config.yaml")
|
|
testutil.WriteTestFile(t, configPath, "invalid yaml content:\n - broken [")
|
|
return actionDir
|
|
},
|
|
expectError: false, // Function may handle YAML errors gracefully
|
|
expectedVals: map[string]string{},
|
|
},
|
|
{
|
|
name: "action directory without config file",
|
|
setupFunc: func(_ *testing.T, tmpDir string) string {
|
|
actionDir := filepath.Join(tmpDir, "action")
|
|
_ = os.MkdirAll(actionDir, 0750) // #nosec G301 -- test directory permissions
|
|
return actionDir
|
|
},
|
|
expectError: false,
|
|
expectedVals: map[string]string{},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpDir, cleanup := testutil.TempDir(t)
|
|
defer cleanup()
|
|
|
|
actionDir := tt.setupFunc(t, tmpDir)
|
|
|
|
loader := NewConfigurationLoader()
|
|
config, err := loader.loadActionConfig(actionDir)
|
|
|
|
if tt.expectError {
|
|
testutil.AssertError(t, err)
|
|
} else {
|
|
testutil.AssertNoError(t, err)
|
|
|
|
// Check expected values if no error
|
|
if config != nil {
|
|
for key, expected := range tt.expectedVals {
|
|
switch key {
|
|
case "theme":
|
|
testutil.AssertEqual(t, expected, config.Theme)
|
|
case "output_format":
|
|
testutil.AssertEqual(t, expected, config.OutputFormat)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestConfigurationLoader_ValidateTheme tests theme validation edge cases.
|
|
func TestConfigurationLoader_ValidateTheme(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
theme string
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "valid built-in theme",
|
|
theme: "github",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "valid default theme",
|
|
theme: "default",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "empty theme returns error",
|
|
theme: "",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "invalid theme",
|
|
theme: "nonexistent-theme",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "case sensitive theme",
|
|
theme: "GitHub",
|
|
expectError: true,
|
|
},
|
|
{
|
|
name: "custom theme path",
|
|
theme: "/custom/theme/path.tmpl",
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "relative theme path",
|
|
theme: "custom/theme.tmpl",
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
loader := NewConfigurationLoader()
|
|
err := loader.validateTheme(tt.theme)
|
|
|
|
if tt.expectError {
|
|
testutil.AssertError(t, err)
|
|
} else {
|
|
testutil.AssertNoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|