Files
gh-action-readme/internal/config_test.go

1442 lines
39 KiB
Go

package internal
import (
"net/http"
"path/filepath"
"testing"
"github.com/google/go-github/v74/github"
"github.com/ivuorinen/gh-action-readme/appconstants"
"github.com/ivuorinen/gh-action-readme/testutil"
)
func TestInitConfig(t *testing.T) {
tests := []struct {
name string
configFile string
setupFunc func(t *testing.T, tempDir string)
expectError bool
expected *AppConfig
}{
{
name: "default config when no file exists",
configFile: "",
setupFunc: nil,
expected: &AppConfig{
Theme: testutil.TestThemeDefault,
OutputFormat: "md",
OutputDir: ".",
Template: testutil.TestTemplateWithPrefix,
Schema: "schemas/schema.json",
Verbose: false,
Quiet: false,
GitHubToken: "",
},
},
{
name: "custom config file",
configFile: testutil.TestFileCustomConfig,
setupFunc: func(t *testing.T, tempDir string) {
t.Helper()
testutil.WriteFileInDir(
t,
tempDir,
testutil.TestFileCustomConfig,
testutil.MustReadFixture(testutil.TestFixtureProfessionalConfig),
)
},
expected: &AppConfig{
Theme: testutil.TestThemeProfessional,
OutputFormat: "html",
OutputDir: "docs",
Template: "custom-template.tmpl",
Schema: "custom-schema.json",
Verbose: true,
Quiet: false,
GitHubToken: "test-token-from-config",
},
},
{
name: "invalid config file",
configFile: testutil.TestPathConfigYML,
setupFunc: func(t *testing.T, tempDir string) {
t.Helper()
testutil.WriteFileInDir(t, tempDir, testutil.TestPathConfigYML, "invalid: yaml: content: [")
},
expectError: true,
},
{
name: "nonexistent config file",
configFile: "nonexistent.yml",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, cleanup := testutil.SetupTestEnvironment(t)
defer cleanup()
if tt.setupFunc != nil {
tt.setupFunc(t, tmpDir)
}
// Set config file path if specified
configPath := ""
if tt.configFile != "" {
configPath = filepath.Join(tmpDir, tt.configFile)
}
config, err := InitConfig(configPath)
if tt.expectError {
testutil.AssertError(t, err)
return
}
testutil.AssertNoError(t, err)
// Verify config values
if tt.expected != nil {
testutil.AssertEqual(t, tt.expected.Theme, config.Theme)
testutil.AssertEqual(t, tt.expected.OutputFormat, config.OutputFormat)
testutil.AssertEqual(t, tt.expected.OutputDir, config.OutputDir)
testutil.AssertEqual(t, tt.expected.Template, config.Template)
testutil.AssertEqual(t, tt.expected.Schema, config.Schema)
testutil.AssertEqual(t, tt.expected.Verbose, config.Verbose)
testutil.AssertEqual(t, tt.expected.Quiet, config.Quiet)
testutil.AssertEqual(t, tt.expected.GitHubToken, config.GitHubToken)
}
})
}
}
func TestLoadConfiguration(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T, tempDir string) (configFile, repoRoot, currentDir string)
expectError bool
checkFunc func(t *testing.T, config *AppConfig)
}{
{
name: "multi-level config hierarchy",
setupFunc: func(t *testing.T, tempDir string) (string, string, string) {
t.Helper()
// Clear environment variables to ensure config file values are used
t.Setenv(appconstants.EnvGitHubTokenStandard, "")
t.Setenv(appconstants.EnvGitHubToken, "")
// Create global config
globalConfigDir := filepath.Join(tempDir, testutil.TestDirDotConfig, testutil.TestBinaryName)
globalConfigPath := WriteConfigFixture(t, globalConfigDir, testutil.TestConfigGlobalDefault)
// Create repo root with repo-specific config
repoRoot := filepath.Join(tempDir, "repo")
testutil.WriteFileInDir(t, repoRoot, testutil.TestFileGHReadmeYAML,
string(testutil.MustReadFixture(testutil.TestConfigRepoSimple)))
// Create current directory with action-specific config
currentDir := filepath.Join(repoRoot, "action")
WriteConfigFixture(t, currentDir, testutil.TestConfigActionSimple)
return globalConfigPath, repoRoot, currentDir
},
checkFunc: func(t *testing.T, config *AppConfig) {
t.Helper()
// Should have action-level overrides
testutil.AssertEqual(t, testutil.TestThemeProfessional, config.Theme)
testutil.AssertEqual(t, "output", config.OutputDir)
// Should inherit from repo level
testutil.AssertEqual(t, "html", config.OutputFormat)
// Should inherit GitHub token from global config
testutil.AssertEqual(t, testutil.TestTokenStd, 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")
t.Setenv("GITHUB_TOKEN", "fallback-token")
// Create config file
testutil.WriteFileInDir(t, tempDir, testutil.TestPathConfigYML, `
theme: minimal
github_token: config-token
`)
configPath := filepath.Join(tempDir, testutil.TestPathConfigYML)
return configPath, tempDir, tempDir
},
checkFunc: func(t *testing.T, config *AppConfig) {
t.Helper()
// Environment variable should override config file
testutil.AssertEqual(t, "env-token", config.GitHubToken)
testutil.AssertEqual(t, testutil.TestThemeMinimal, config.Theme)
},
},
{
name: "XDG compliance",
setupFunc: func(t *testing.T, tempDir string) (string, string, string) {
t.Helper()
// Set XDG environment variables
xdgConfigHome := filepath.Join(tempDir, "xdg-config")
t.Setenv("XDG_CONFIG_HOME", xdgConfigHome)
// Create XDG-compliant config
configDir := filepath.Join(xdgConfigHome, testutil.TestBinaryName)
configPath := WriteConfigFixture(t, configDir, testutil.TestConfigGitHubVerbose)
return configPath, tempDir, tempDir
},
checkFunc: func(t *testing.T, config *AppConfig) {
t.Helper()
testutil.AssertEqual(t, testutil.TestThemeGitHub, config.Theme)
testutil.AssertEqual(t, true, config.Verbose)
},
},
{
name: "hidden config file discovery",
setupFunc: func(t *testing.T, tempDir string) (string, string, string) {
t.Helper()
repoRoot := filepath.Join(tempDir, "repo")
// Create multiple hidden config files
testutil.WriteFileInDir(t, repoRoot, testutil.TestFileGHReadmeYAML,
string(testutil.MustReadFixture(testutil.TestConfigMinimalTheme)))
configDir := filepath.Join(repoRoot, testutil.TestDirDotConfig)
testutil.WriteFileInDir(t, configDir, "ghreadme.yaml",
string(testutil.MustReadFixture(testutil.TestConfigProfessionalQuiet)))
githubDir := filepath.Join(repoRoot, ".github")
testutil.WriteFileInDir(t, githubDir, "ghreadme.yaml",
string(testutil.MustReadFixture(testutil.TestConfigGitHubVerbose)))
return "", repoRoot, repoRoot
},
checkFunc: func(t *testing.T, config *AppConfig) {
t.Helper()
// Should use the first found config (.ghreadme.yaml has priority)
testutil.AssertEqual(t, testutil.TestThemeMinimal, config.Theme)
testutil.AssertEqual(t, "json", config.OutputFormat)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, cleanup := testutil.SetupTestEnvironment(t)
defer cleanup()
configFile, repoRoot, currentDir := tt.setupFunc(t, tmpDir)
config, err := LoadConfiguration(configFile, repoRoot, currentDir)
if tt.expectError {
testutil.AssertError(t, err)
return
}
testutil.AssertNoError(t, err)
if tt.checkFunc != nil {
tt.checkFunc(t, config)
}
})
}
}
func TestGetConfigPath(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T, tempDir string)
contains string
}{
{
name: "XDG_CONFIG_HOME set",
setupFunc: func(t *testing.T, tempDir string) {
t.Helper()
t.Setenv("XDG_CONFIG_HOME", tempDir)
t.Setenv("HOME", "")
},
contains: testutil.TestBinaryName,
},
{
name: "HOME fallback",
setupFunc: func(t *testing.T, tempDir string) {
t.Helper()
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("HOME", tempDir)
},
contains: testutil.TestDirDotConfig,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
tt.setupFunc(t, tmpDir)
path, err := GetConfigPath()
testutil.AssertNoError(t, err)
if !filepath.IsAbs(path) {
t.Errorf("expected absolute path, got: %s", path)
}
testutil.AssertStringContains(t, path, tt.contains)
})
}
}
func TestWriteDefaultConfig(t *testing.T) {
_, cleanup := testutil.SetupTestEnvironment(t)
defer cleanup()
err := WriteDefaultConfig()
testutil.AssertNoError(t, err)
// Check that config file was created
configPath, _ := GetConfigPath()
t.Logf("Expected config path: %s", configPath)
testutil.AssertFileExists(t, configPath)
// Verify config file content
config, err := InitConfig(configPath)
testutil.AssertNoError(t, err)
// Should have default values
testutil.AssertEqual(t, testutil.TestThemeDefault, config.Theme)
testutil.AssertEqual(t, "md", config.OutputFormat)
testutil.AssertEqual(t, ".", config.OutputDir)
}
func TestResolveThemeTemplate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
theme string
expectError bool
shouldExist bool
expectedPath string
}{
{
name: "default theme",
theme: testutil.TestThemeDefault,
expectError: false,
shouldExist: true,
expectedPath: testutil.TestTemplateWithPrefix,
},
{
name: "github theme",
theme: testutil.TestThemeGitHub,
expectError: false,
shouldExist: true,
expectedPath: "templates/themes/github/readme.tmpl",
},
{
name: "gitlab theme",
theme: testutil.TestThemeGitLab,
expectError: false,
shouldExist: true,
expectedPath: "templates/themes/gitlab/readme.tmpl",
},
{
name: "minimal theme",
theme: testutil.TestThemeMinimal,
expectError: false,
shouldExist: true,
expectedPath: "templates/themes/minimal/readme.tmpl",
},
{
name: "professional theme",
theme: testutil.TestThemeProfessional,
expectError: false,
shouldExist: true,
expectedPath: "templates/themes/professional/readme.tmpl",
},
{
name: testutil.TestCaseNameUnknownTheme,
theme: "nonexistent",
expectError: true,
},
{
name: testutil.TestCaseNameEmptyTheme,
theme: "",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
path := resolveThemeTemplate(tt.theme)
if tt.expectError {
if path != "" {
t.Errorf("expected empty path on error, got: %s", path)
}
return
}
if path == "" {
t.Error("expected non-empty path")
}
if tt.expectedPath != "" {
testutil.AssertStringContains(t, path, tt.expectedPath)
}
// Note: We can't check file existence here because template files
// might not be present in the test environment
})
}
}
func TestConfigTokenHierarchy(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()
// Use default config
config, err := LoadConfiguration("", tmpDir, tmpDir)
testutil.AssertNoError(t, err)
testutil.AssertEqual(t, tt.ExpectedToken, config.GitHubToken)
})
}
}
func TestConfigMerging(t *testing.T) {
tmpDir, cleanup := testutil.TempDir(t)
defer cleanup()
// Test config merging by creating config files and seeing the result
globalConfigDir := filepath.Join(tmpDir, testutil.TestDirDotConfig, testutil.TestBinaryName)
WriteConfigFixture(t, globalConfigDir, testutil.TestConfigGlobalBaseToken)
repoRoot := filepath.Join(tmpDir, "repo")
testutil.WriteFileInDir(t, repoRoot, testutil.TestFileGHReadmeYAML,
string(testutil.MustReadFixture(testutil.TestConfigRepoVerbose)))
// Set HOME and XDG_CONFIG_HOME to temp directory
testutil.SetupConfigEnvironment(t, tmpDir)
// Use the specific config file path instead of relying on XDG discovery
configPath := filepath.Join(
tmpDir,
testutil.TestDirDotConfig,
testutil.TestBinaryName,
testutil.TestFileConfigYAML,
)
config, err := LoadConfiguration(configPath, repoRoot, repoRoot)
testutil.AssertNoError(t, err)
// Should have merged values
testutil.AssertEqual(t, testutil.TestThemeGitHub, config.Theme) // from repo config
testutil.AssertEqual(t, "html", config.OutputFormat) // from repo config
testutil.AssertEqual(t, true, config.Verbose) // from repo config
testutil.AssertEqual(t, "base-token", config.GitHubToken) // from global config
testutil.AssertEqual(t, "schemas/schema.json", config.Schema) // default value
}
// TestGetGitHubToken tests GitHub token resolution with different priority levels.
func TestGetGitHubToken(t *testing.T) {
tests := []struct {
name string
toolEnvToken string
stdEnvToken string
configToken string
expectedToken string
}{
{
name: "tool-specific env var has highest priority",
toolEnvToken: "tool-token",
stdEnvToken: testutil.TestTokenStd,
configToken: testutil.TestTokenConfig,
expectedToken: "tool-token",
},
{
name: "standard env var when tool env not set",
toolEnvToken: "",
stdEnvToken: testutil.TestTokenStd,
configToken: testutil.TestTokenConfig,
expectedToken: testutil.TestTokenStd,
},
{
name: "config token when env vars not set",
toolEnvToken: "",
stdEnvToken: "",
configToken: testutil.TestTokenConfig,
expectedToken: testutil.TestTokenConfig,
},
{
name: "empty string when nothing set",
toolEnvToken: "",
stdEnvToken: "",
configToken: "",
expectedToken: "",
},
{
name: "empty env var does not override config",
toolEnvToken: "",
stdEnvToken: "",
configToken: testutil.TestTokenConfig,
expectedToken: testutil.TestTokenConfig,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Set up environment
if tt.toolEnvToken != "" {
t.Setenv(appconstants.EnvGitHubToken, tt.toolEnvToken)
} else {
t.Setenv(appconstants.EnvGitHubToken, "")
}
if tt.stdEnvToken != "" {
t.Setenv(appconstants.EnvGitHubTokenStandard, tt.stdEnvToken)
} else {
t.Setenv(appconstants.EnvGitHubTokenStandard, "")
}
config := &AppConfig{GitHubToken: tt.configToken}
result := GetGitHubToken(config)
testutil.AssertEqual(t, tt.expectedToken, result)
})
}
}
// TestMergeMapFields tests the merging of map fields in configuration.
func TestMergeMapFields(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dst *AppConfig
src *AppConfig
expected *AppConfig
}{
createMapMergeTest(
"merge permissions into empty dst",
nil,
map[string]string{"read": "read", "write": "write"},
map[string]string{"read": "read", "write": "write"},
true, // isPermissions
),
createMapMergeTest(
"merge permissions into existing dst",
map[string]string{"read": "existing"},
map[string]string{"read": "new", "write": "write"},
map[string]string{"read": "new", "write": "write"},
true, // isPermissions
),
createMapMergeTest(
"merge variables into empty dst",
nil,
map[string]string{"VAR1": "value1", "VAR2": "value2"},
map[string]string{"VAR1": "value1", "VAR2": "value2"},
false, // isPermissions
),
createMapMergeTest(
"merge variables into existing dst",
map[string]string{"VAR1": "existing"},
map[string]string{"VAR1": "new", "VAR2": "value2"},
map[string]string{"VAR1": "new", "VAR2": "value2"},
false, // isPermissions
),
{
name: "merge both permissions and variables",
dst: &AppConfig{
Permissions: map[string]string{"read": "existing"},
},
src: &AppConfig{
Permissions: map[string]string{"write": "write"},
Variables: map[string]string{"VAR1": "value1"},
},
expected: &AppConfig{
Permissions: map[string]string{"read": "existing", "write": "write"},
Variables: map[string]string{"VAR1": "value1"},
},
},
{
name: "empty src does not affect dst",
dst: &AppConfig{
Permissions: map[string]string{"read": "read"},
Variables: map[string]string{"VAR1": "value1"},
},
src: &AppConfig{},
expected: &AppConfig{
Permissions: map[string]string{"read": "read"},
Variables: map[string]string{"VAR1": "value1"},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Deep copy dst to avoid modifying test data
dst := &AppConfig{}
if tt.dst.Permissions != nil {
dst.Permissions = make(map[string]string)
for k, v := range tt.dst.Permissions {
dst.Permissions[k] = v
}
}
if tt.dst.Variables != nil {
dst.Variables = make(map[string]string)
for k, v := range tt.dst.Variables {
dst.Variables[k] = v
}
}
mergeMapFields(dst, tt.src)
testutil.AssertEqual(t, tt.expected.Permissions, dst.Permissions)
testutil.AssertEqual(t, tt.expected.Variables, dst.Variables)
})
}
}
// TestMergeSliceFields tests the merging of slice fields in configuration.
func TestMergeSliceFields(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dst *AppConfig
src *AppConfig
expected []string
}{
{
name: "merge runsOn into empty dst",
dst: &AppConfig{},
src: &AppConfig{RunsOn: []string{testutil.RunnerUbuntuLatest, testutil.RunnerWindowsLatest}},
expected: []string{testutil.RunnerUbuntuLatest, testutil.RunnerWindowsLatest},
},
{
name: "merge runsOn replaces existing dst",
dst: &AppConfig{RunsOn: []string{"macos-latest"}},
src: &AppConfig{RunsOn: []string{testutil.RunnerUbuntuLatest, testutil.RunnerWindowsLatest}},
expected: []string{testutil.RunnerUbuntuLatest, testutil.RunnerWindowsLatest},
},
{
name: "empty src does not affect dst",
dst: &AppConfig{RunsOn: []string{testutil.RunnerUbuntuLatest}},
src: &AppConfig{},
expected: []string{testutil.RunnerUbuntuLatest},
},
{
name: "empty src slice does not affect dst",
dst: &AppConfig{RunsOn: []string{testutil.RunnerUbuntuLatest}},
src: &AppConfig{RunsOn: []string{}},
expected: []string{testutil.RunnerUbuntuLatest},
},
{
name: "single item slice",
dst: &AppConfig{},
src: &AppConfig{RunsOn: []string{"self-hosted"}},
expected: []string{"self-hosted"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mergeSliceFields(tt.dst, tt.src)
// Compare slices manually since they can't be compared directly
if len(tt.expected) != len(tt.dst.RunsOn) {
t.Errorf("expected slice length %d, got %d", len(tt.expected), len(tt.dst.RunsOn))
return
}
for i, expected := range tt.expected {
if i >= len(tt.dst.RunsOn) || tt.dst.RunsOn[i] != expected {
t.Errorf("expected %v, got %v", tt.expected, tt.dst.RunsOn)
return
}
}
})
}
}
// assertBooleanConfigFields is a helper that checks all boolean fields in AppConfig.
func assertBooleanConfigFields(t *testing.T, got, want *AppConfig) {
t.Helper()
fields := []struct {
name string
gotVal bool
wantVal bool
}{
{"AnalyzeDependencies", got.AnalyzeDependencies, want.AnalyzeDependencies},
{"ShowSecurityInfo", got.ShowSecurityInfo, want.ShowSecurityInfo},
{"Verbose", got.Verbose, want.Verbose},
{"Quiet", got.Quiet, want.Quiet},
{"UseDefaultBranch", got.UseDefaultBranch, want.UseDefaultBranch},
}
for _, field := range fields {
if field.gotVal != field.wantVal {
t.Errorf("%s = %v, want %v", field.name, field.gotVal, field.wantVal)
}
}
}
// TestMergeBooleanFields tests merging boolean configuration fields.
func TestMergeBooleanFields(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dst *AppConfig
src *AppConfig
want *AppConfig
}{
createBoolFieldMergeTest(
"merge all true values",
boolFields{false, false, false, false, false},
boolFields{true, true, true, true, true},
boolFields{true, true, true, true, true},
),
createBoolFieldMergeTest(
"merge only some true values",
boolFields{false, true, false, true, false},
boolFields{true, false, true, false, false},
boolFields{true, true, true, true, false},
),
createBoolFieldMergeTest(
"merge with all source false",
boolFields{true, true, true, true, true},
boolFields{false, false, false, false, false},
boolFields{true, true, true, true, true},
),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mergeBooleanFields(tt.dst, tt.src)
assertBooleanConfigFields(t, tt.dst, tt.want)
})
}
}
// TestMergeSecurityFields tests merging security-sensitive configuration fields.
func TestMergeSecurityFields(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dst *AppConfig
src *AppConfig
allowTokens bool
want *AppConfig
}{
createTokenMergeTest(
"allow tokens - merge token",
"",
"ghp_test_token",
"ghp_test_token",
true,
),
createTokenMergeTest(
"disallow tokens - do not merge token",
"",
"ghp_test_token",
"",
false,
),
createTokenMergeTest(
"allow tokens - do not overwrite with empty",
"ghp_existing_token",
"",
"ghp_existing_token",
true,
),
createTokenMergeTest(
"allow tokens - overwrite existing token",
"ghp_old_token",
"ghp_new_token",
"ghp_new_token",
true,
),
{
name: "allow tokens - merge repo overrides into nil dst",
dst: &AppConfig{
RepoOverrides: nil,
},
src: &AppConfig{
RepoOverrides: map[string]AppConfig{
testutil.OrgRepo: {Organization: testutil.OrgName, Repository: testutil.RepoName},
},
},
allowTokens: true,
want: &AppConfig{
RepoOverrides: map[string]AppConfig{
testutil.OrgRepo: {Organization: testutil.OrgName, Repository: testutil.RepoName},
},
},
},
{
name: "allow tokens - merge repo overrides into existing dst",
dst: &AppConfig{
RepoOverrides: map[string]AppConfig{
testutil.ExistingRepo: {Organization: testutil.ExistingOrgName, Repository: testutil.RepoName},
},
},
src: &AppConfig{
RepoOverrides: map[string]AppConfig{
testutil.NewRepo: {Organization: testutil.NewOrgName, Repository: testutil.RepoName},
},
},
allowTokens: true,
want: &AppConfig{
RepoOverrides: map[string]AppConfig{
testutil.ExistingRepo: {Organization: testutil.ExistingOrgName, Repository: testutil.RepoName},
testutil.NewRepo: {Organization: testutil.NewOrgName, Repository: testutil.RepoName},
},
},
},
{
name: "disallow tokens - do not merge repo overrides",
dst: &AppConfig{
RepoOverrides: nil,
},
src: &AppConfig{
RepoOverrides: map[string]AppConfig{
testutil.OrgRepo: {Organization: testutil.OrgName, Repository: testutil.RepoName},
},
},
allowTokens: false,
want: &AppConfig{
RepoOverrides: nil,
},
},
{
name: "allow tokens - empty source repo overrides",
dst: &AppConfig{
RepoOverrides: map[string]AppConfig{
testutil.ExistingRepo: {Organization: testutil.ExistingOrgName, Repository: testutil.RepoName},
},
},
src: &AppConfig{
RepoOverrides: map[string]AppConfig{},
},
allowTokens: true,
want: &AppConfig{
RepoOverrides: map[string]AppConfig{
testutil.ExistingRepo: {Organization: testutil.ExistingOrgName, Repository: testutil.RepoName},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
mergeSecurityFields(tt.dst, tt.src, tt.allowTokens)
if tt.dst.GitHubToken != tt.want.GitHubToken {
t.Errorf("GitHubToken = %q, want %q",
tt.dst.GitHubToken, tt.want.GitHubToken)
}
assertRepoOverrides(t, tt.dst.RepoOverrides, tt.want.RepoOverrides)
})
}
}
// assertRepoOverrides validates that RepoOverrides match expectations.
func assertRepoOverrides(t *testing.T, got, want map[string]AppConfig) {
t.Helper()
if want == nil {
if got != nil {
t.Errorf("RepoOverrides = %v, want nil", got)
}
return
}
if got == nil {
t.Error("RepoOverrides is nil, want non-nil")
return
}
for key, wantVal := range want {
gotVal, exists := got[key]
if !exists {
t.Errorf("RepoOverrides missing key %q", key)
} else if gotVal.Organization != wantVal.Organization ||
gotVal.Repository != wantVal.Repository {
t.Errorf("RepoOverrides[%q] = %+v, want %+v",
key, gotVal, wantVal)
}
}
if len(got) != len(want) {
t.Errorf("RepoOverrides length = %d, want %d", len(got), len(want))
}
}
// assertGitHubClientValid checks that a GitHub client is properly initialized.
func assertGitHubClientValid(t *testing.T, client *GitHubClient, expectedToken string) {
t.Helper()
if client == nil {
t.Error("expected non-nil client")
return
}
if client.Client == nil {
t.Error("expected non-nil GitHub client")
}
if client.Token != expectedToken {
t.Errorf("expected token %q, got %q", expectedToken, client.Token)
}
}
// TestNewGitHubClient_EdgeCases tests GitHub client initialization edge cases.
func TestNewGitHubClientEdgeCases(t *testing.T) {
t.Parallel()
tests := []struct {
name string
token string
expectError bool
description string
}{
{
name: "valid classic GitHub token",
token: "ghp_1234567890abcdefghijklmnopqrstuvwxyzABCD",
expectError: false,
description: "Should create client with valid classic token",
},
{ // #nosec G101 -- test token, not a real credential
name: "valid fine-grained PAT",
token: "github_pat_11AAAAAA0AAAAaAaaAaaaAaa_AaAAaAAaAAAaAAAAAaAAaAAaAaAAaAAAAaAAAAAAAAaAAaAAaAaaAA",
expectError: false,
description: "Should create client with fine-grained token",
},
{
name: "empty token",
token: "",
expectError: false,
description: "Should create client without authentication",
},
{
name: "short token",
token: "ghp_short",
expectError: false,
description: "Should create client even with unusual token format",
},
{
name: "token with special characters",
token: "test-token_123",
expectError: false,
description: "Should handle tokens with various characters",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
client, err := NewGitHubClient(tt.token)
if tt.expectError {
testutil.AssertError(t, err)
return
}
testutil.AssertNoError(t, err)
assertGitHubClientValid(t, client, tt.token)
})
}
}
// TestValidateGitHubClientCreation tests raw GitHub client creation validation.
// This test demonstrates the use of the assertGitHubClient helper for
// validating github.Client instances with different configurations.
func TestValidateGitHubClientCreation(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setupFunc func(t *testing.T) (*github.Client, error)
expectError bool
description string
}{
{
name: "successful client creation with nil transport",
setupFunc: func(t *testing.T) (*github.Client, error) {
t.Helper()
// Valid client creation - github.NewClient handles nil gracefully
return github.NewClient(nil), nil
},
expectError: false,
description: "Should create valid GitHub client with default transport",
},
{
name: "successful client creation with custom HTTP client",
setupFunc: func(t *testing.T) (*github.Client, error) {
t.Helper()
// Create client with custom HTTP client for testing
mockHTTPClient := &http.Client{
Transport: &testutil.MockTransport{},
}
return github.NewClient(mockHTTPClient), nil
},
expectError: false,
description: "Should create valid GitHub client with custom transport",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
client, err := tt.setupFunc(t)
// Use the assertGitHubClient helper to validate the result
assertGitHubClient(t, client, err, tt.expectError)
})
}
}
// runTemplatePathTest runs a template path test with setup and validation.
func runTemplatePathTest(
t *testing.T,
setupFunc func(*testing.T) (string, func()),
checkFunc func(*testing.T, string),
) {
t.Helper()
templatePath, cleanup := setupFunc(t)
defer cleanup()
result := resolveTemplatePath(templatePath)
if checkFunc != nil {
checkFunc(t, result)
}
}
// TestResolveTemplatePath_EdgeCases tests template path resolution edge cases.
func TestResolveTemplatePathEdgeCases(t *testing.T) {
// Note: Cannot use t.Parallel() because one subtest uses t.Chdir()
tests := []struct {
name string
setupFunc func(t *testing.T) (templatePath string, cleanup func())
checkFunc func(t *testing.T, result string)
description string
}{
{
name: "absolute path - return as-is",
setupFunc: func(t *testing.T) (string, func()) {
t.Helper()
tmpDir, cleanup := testutil.TempDir(t)
testutil.WriteFileInDir(t, tmpDir, "template.tmpl", "test template")
absPath := filepath.Join(tmpDir, "template.tmpl")
return absPath, cleanup
},
checkFunc: func(t *testing.T, result string) {
t.Helper()
if !filepath.IsAbs(result) {
t.Errorf("expected absolute path, got: %s", result)
}
},
description: "Absolute paths should be returned unchanged",
},
{
name: "embedded template - available",
setupFunc: func(t *testing.T) (string, func()) {
t.Helper()
// Use a path we know is embedded
return testutil.TestTemplateReadme, func() { /* No cleanup needed for embedded templates */ }
},
checkFunc: func(t *testing.T, result string) {
t.Helper()
if result != testutil.TestTemplateReadme {
t.Errorf("expected %q, got: %s", testutil.TestTemplateReadme, result)
}
},
description: "Embedded templates should return original path",
},
{
name: "embedded template with templates/ prefix",
setupFunc: func(t *testing.T) (string, func()) {
t.Helper()
return testutil.TestTemplateWithPrefix, func() { /* No cleanup needed for embedded templates */ }
},
checkFunc: func(t *testing.T, result string) {
t.Helper()
if result != testutil.TestTemplateWithPrefix {
t.Errorf("expected %q, got: %s", testutil.TestTemplateWithPrefix, result)
}
},
description: "Embedded templates with prefix should return original path",
},
{
name: "filesystem template - exists in current dir",
setupFunc: func(t *testing.T) (string, func()) {
t.Helper()
tmpDir, cleanup := testutil.TempDir(t)
// Create template in current directory
templateName := "custom-template.tmpl"
testutil.WriteFileInDir(t, tmpDir, templateName, "custom template")
// Change to tmpDir
t.Chdir(tmpDir)
return templateName, cleanup
},
checkFunc: func(t *testing.T, result string) {
t.Helper()
if result == "" {
t.Error(testutil.TestMsgExpectedNonEmpty)
}
},
description: "Templates in current directory should be found",
},
{
name: "non-existent template - fallback to original path",
setupFunc: func(t *testing.T) (string, func()) {
t.Helper()
return "nonexistent-template.tmpl", func() { /* No cleanup needed for non-existent template test */ }
},
checkFunc: func(t *testing.T, result string) {
t.Helper()
if result != "nonexistent-template.tmpl" {
t.Errorf("expected original path, got: %s", result)
}
},
description: "Non-existent templates should return original path",
},
{
name: testutil.TestCaseNameEmptyPath,
setupFunc: func(t *testing.T) (string, func()) {
t.Helper()
return "", func() { /* No cleanup needed for empty path test */ }
},
checkFunc: func(t *testing.T, _ string) {
t.Helper()
// Empty path may return binary directory or empty string
// depending on whether GetBinaryDir succeeds
// Just verify it doesn't crash
},
description: "Empty path should not crash",
},
{
name: "relative path with subdirectory",
setupFunc: func(t *testing.T) (string, func()) {
t.Helper()
return "themes/github/readme.tmpl", func() { /* No cleanup needed for relative path test */ }
},
checkFunc: func(t *testing.T, result string) {
t.Helper()
// Should return the path (either embedded or fallback)
if result == "" {
t.Error(testutil.TestMsgExpectedNonEmpty)
}
},
description: "Relative paths with subdirectories should be resolved",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Note: Cannot use t.Parallel() because one subtest uses t.Chdir()
runTemplatePathTest(t, tt.setupFunc, tt.checkFunc)
})
}
}
// TestDetectRepositoryName_EdgeCases tests repository name detection edge cases.
func TestDetectRepositoryNameEdgeCases(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setupFunc func(t *testing.T) string
expectedResult string
description string
}{
{
name: "empty repo root",
setupFunc: func(t *testing.T) string {
t.Helper()
return ""
},
expectedResult: "",
description: "Empty repo root should return empty string",
},
{
name: "non-existent directory",
setupFunc: func(t *testing.T) string {
t.Helper()
return "/nonexistent/path/to/repo"
},
expectedResult: "",
description: "Non-existent directory should return empty string",
},
{
name: "directory without git",
setupFunc: func(t *testing.T) string {
t.Helper()
tmpDir, _ := testutil.TempDir(t)
return tmpDir
},
expectedResult: "",
description: "Directory without .git should return empty string",
},
createGitRemoteTestCase(
"valid git repository with GitHub remote",
`[remote "origin"]
url = https://github.com/testorg/testrepo.git
fetch = +refs/heads/*:refs/remotes/origin/*
`,
"testorg/testrepo",
"Valid GitHub repo should return org/repo",
),
createGitRemoteTestCase(
"git repository with SSH remote",
`[remote "origin"]
url = git@github.com:sshorg/sshrepo.git
fetch = +refs/heads/*:refs/remotes/origin/*
`,
"sshorg/sshrepo",
"SSH remote should be parsed correctly",
),
createGitRemoteTestCase(
"git repository without remote",
"", // No config content
"",
"Repository without remote should return empty string",
),
createGitRemoteTestCase(
"git repository with non-GitHub remote",
`[remote "origin"]
url = https://gitlab.com/glorg/glrepo.git
fetch = +refs/heads/*:refs/remotes/origin/*
`,
"",
"Non-GitHub remote should return empty string",
),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
repoRoot := tt.setupFunc(t)
result := DetectRepositoryName(repoRoot)
if result != tt.expectedResult {
t.Errorf("DetectRepositoryName() = %q, want %q (test: %s)",
result, tt.expectedResult, tt.description)
}
})
}
}
// TestLoadConfiguration_EdgeCases tests configuration loading edge cases.
func TestLoadConfigurationEdgeCases(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T) (configFile, repoRoot, currentDir string)
expectError bool
checkFunc func(t *testing.T, config *AppConfig)
description string
}{
{
name: "empty config file path with defaults",
setupFunc: func(t *testing.T) (string, string, string) {
t.Helper()
tmpDir, _ := testutil.TempDir(t)
testutil.SetupConfigEnvironment(t, tmpDir)
return "", tmpDir, tmpDir
},
expectError: false,
checkFunc: func(t *testing.T, config *AppConfig) {
t.Helper()
if config == nil {
t.Fatal(testutil.TestMsgExpectedNonNilConfig)
}
// Should have default values
if config.Theme == "" {
t.Error("expected non-empty theme (default)")
}
},
description: "Empty config file should load defaults",
},
{
name: "all paths empty",
setupFunc: func(t *testing.T) (string, string, string) {
t.Helper()
tmpDir, _ := testutil.TempDir(t)
t.Setenv("HOME", tmpDir)
return "", "", ""
},
expectError: false,
checkFunc: func(t *testing.T, config *AppConfig) {
t.Helper()
if config == nil {
t.Fatal(testutil.TestMsgExpectedNonNilConfig)
}
},
description: "All empty paths should still return config",
},
{
name: "config file with minimal values",
setupFunc: func(t *testing.T) (string, string, string) {
t.Helper()
tmpDir, _ := testutil.TempDir(t)
testutil.WriteFileInDir(t, tmpDir, testutil.TestFileConfigYAML, "theme: minimal\n")
configPath := filepath.Join(tmpDir, testutil.TestFileConfigYAML)
return configPath, tmpDir, tmpDir
},
expectError: false,
checkFunc: func(t *testing.T, config *AppConfig) {
t.Helper()
testutil.AssertEqual(t, testutil.TestThemeMinimal, config.Theme)
},
description: "Minimal config should merge with defaults",
},
{
name: "invalid config file path",
setupFunc: func(t *testing.T) (string, string, string) {
t.Helper()
tmpDir, _ := testutil.TempDir(t)
return filepath.Join(tmpDir, "nonexistent.yaml"), tmpDir, tmpDir
},
expectError: true,
description: "Invalid config file path should error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configFile, repoRoot, currentDir := tt.setupFunc(t)
config, err := LoadConfiguration(configFile, repoRoot, currentDir)
if tt.expectError {
testutil.AssertError(t, err)
} else {
testutil.AssertNoError(t, err)
if tt.checkFunc != nil {
tt.checkFunc(t, config)
}
}
})
}
}
// TestInitConfig_EdgeCases tests config initialization edge cases.
func TestInitConfigEdgeCases(t *testing.T) {
tests := []struct {
name string
setupFunc func(t *testing.T) string
expectError bool
checkFunc func(t *testing.T, config *AppConfig)
description string
}{
{
name: "empty config file path - use default",
setupFunc: func(t *testing.T) string {
t.Helper()
tmpDir, _ := testutil.TempDir(t)
testutil.SetupConfigEnvironment(t, tmpDir)
return ""
},
expectError: false,
checkFunc: func(t *testing.T, config *AppConfig) {
t.Helper()
if config == nil {
t.Fatal(testutil.TestMsgExpectedNonNilConfig)
}
// Should have default values
testutil.AssertEqual(t, testutil.TestThemeDefault, config.Theme)
},
description: "Empty path should use default config",
},
{
name: "config file with empty values",
setupFunc: func(t *testing.T) string {
t.Helper()
tmpDir, _ := testutil.TempDir(t)
testutil.WriteFileInDir(t, tmpDir, "empty.yaml", "---\n")
configPath := filepath.Join(tmpDir, "empty.yaml")
return configPath
},
expectError: false,
checkFunc: func(t *testing.T, config *AppConfig) {
t.Helper()
// Should still have default values filled in
if config.Theme == "" {
t.Error("expected non-empty theme from defaults")
}
},
description: "Empty config should be filled with defaults",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configPath := tt.setupFunc(t)
config, err := InitConfig(configPath)
if tt.expectError {
testutil.AssertError(t, err)
} else {
testutil.AssertNoError(t, err)
if tt.checkFunc != nil {
tt.checkFunc(t, config)
}
}
})
}
}