Files
gh-action-readme/internal/configuration_loader_test.go
Ismo Vuorinen 4f12c4d3dd feat(lint): add many linters, make all the tests run fast! (#23)
* chore(lint): added nlreturn, run linting

* chore(lint): replace some fmt.Sprintf calls

* chore(lint): replace fmt.Sprintf with strconv

* chore(lint): add goconst, use http lib for status codes, and methods

* chore(lint): use errors lib, errCodes from internal/errors

* chore(lint): dupl, thelper and usetesting

* chore(lint): fmt.Errorf %v to %w, more linters

* chore(lint): paralleltest, where possible

* perf(test): optimize test performance by 78%

- Implement shared binary building with package-level cache to eliminate redundant builds
- Add strategic parallelization to 15+ tests while preserving environment variable isolation
- Implement thread-safe fixture caching with RWMutex to reduce I/O operations
- Remove unnecessary working directory changes by leveraging embedded templates
- Add embedded template system with go:embed directive for reliable template resolution
- Fix linting issues: rename sharedBinaryError to errSharedBinary, add nolint directive

Performance improvements:
- Total test execution time: 12+ seconds → 2.7 seconds (78% faster)
- Binary build overhead: 14+ separate builds → 1 shared build (93% reduction)
- Parallel execution: Limited → 15+ concurrent tests (60-70% better CPU usage)
- I/O operations: 66+ fixture reads → cached with sync.RWMutex (50% reduction)

All tests maintain 100% success rate and coverage while running nearly 4x faster.
2025-08-06 15:28:09 +03:00

765 lines
20 KiB
Go

package internal
import (
"os"
"path/filepath"
"testing"
"github.com/ivuorinen/gh-action-readme/testutil"
)
func TestNewConfigurationLoader(t *testing.T) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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(t *testing.T, tempDir string) (string, string, string) {
t.Helper()
// Set environment variables
t.Setenv("GH_README_GITHUB_TOKEN", "env-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
t.Setenv("HOME", tmpDir)
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 {
t.Helper()
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 {
t.Helper()
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
t.Setenv("HOME", tmpDir)
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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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 := testutil.GetGitHubTokenHierarchyTests()
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
t.Setenv("HOME", tmpDir)
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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
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 {
t.Helper()
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 {
t.Helper()
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) {
t.Parallel()
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) {
t.Parallel()
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) {
t.Parallel()
loader := NewConfigurationLoader()
err := loader.validateTheme(tt.theme)
if tt.expectError {
testutil.AssertError(t, err)
} else {
testutil.AssertNoError(t, err)
}
})
}
}