Files
gibidify/config/validation_test.go
Ismo Vuorinen 95b7ef6dd3 chore: modernize workflows, security scanning, and linting configuration (#50)
* build: update Go 1.25, CI workflows, and build tooling

- Upgrade to Go 1.25
- Add benchmark targets to Makefile
- Implement parallel gosec execution
- Lock tool versions for reproducibility
- Add shellcheck directives to scripts
- Update CI workflows with improved caching

* refactor: migrate from golangci-lint to revive

- Replace golangci-lint with revive for linting
- Configure comprehensive revive rules
- Fix all EditorConfig violations
- Add yamllint and yamlfmt support
- Remove deprecated .golangci.yml

* refactor: rename utils to shared and deduplicate code

- Rename utils package to shared
- Add shared constants package
- Deduplicate constants across packages
- Address CodeRabbit review feedback

* fix: resolve SonarQube issues and add safety guards

- Fix all 73 SonarQube OPEN issues
- Add nil guards for resourceMonitor, backpressure, metricsCollector
- Implement io.Closer for headerFileReader
- Propagate errors from processing helpers
- Add metrics and templates packages
- Improve error handling across codebase

* test: improve test infrastructure and coverage

- Add benchmarks for cli, fileproc, metrics
- Improve test coverage for cli, fileproc, config
- Refactor tests with helper functions
- Add shared test constants
- Fix test function naming conventions
- Reduce cognitive complexity in benchmark tests

* docs: update documentation and configuration examples

- Update CLAUDE.md with current project state
- Refresh README with new features
- Add usage and configuration examples
- Add SonarQube project configuration
- Consolidate config.example.yaml

* fix: resolve shellcheck warnings in scripts

- Use ./*.go instead of *.go to prevent dash-prefixed filenames
  from being interpreted as options (SC2035)
- Remove unreachable return statement after exit (SC2317)
- Remove obsolete gibidiutils/ directory reference

* chore(deps): upgrade go dependencies

* chore(lint): megalinter fixes

* fix: improve test coverage and fix file descriptor leaks

- Add defer r.Close() to fix pipe file descriptor leaks in benchmark tests
- Refactor TestProcessorConfigureFileTypes with helper functions and assertions
- Refactor TestProcessorLogFinalStats with output capture and keyword verification
- Use shared constants instead of literal strings (TestFilePNG, FormatMarkdown, etc.)
- Reduce cognitive complexity by extracting helper functions

* fix: align test comments with function names

Remove underscores from test comments to match actual function names:
- benchmark/benchmark_test.go (2 fixes)
- fileproc/filetypes_config_test.go (4 fixes)
- fileproc/filetypes_registry_test.go (6 fixes)
- fileproc/processor_test.go (6 fixes)
- fileproc/resource_monitor_types_test.go (4 fixes)
- fileproc/writer_test.go (3 fixes)

* fix: various test improvements and bug fixes

- Remove duplicate maxCacheSize check in filetypes_registry_test.go
- Shorten long comment in processor_test.go to stay under 120 chars
- Remove flaky time.Sleep in collector_test.go, use >= 0 assertion
- Close pipe reader in benchmark_test.go to fix file descriptor leak
- Use ContinueOnError in flags_test.go to match ResetFlags behavior
- Add nil check for p.ui in processor_workers.go before UpdateProgress
- Fix resource_monitor_validation_test.go by setting hardMemoryLimitBytes directly

* chore(yaml): add missing document start markers

Add --- document start to YAML files to satisfy yamllint:
- .github/workflows/codeql.yml
- .github/workflows/build-test-publish.yml
- .github/workflows/security.yml
- .github/actions/setup/action.yml

* fix: guard nil resourceMonitor and fix test deadlock

- Guard resourceMonitor before CreateFileProcessingContext call
- Add ui.UpdateProgress on emergency stop and path error returns
- Fix potential deadlock in TestProcessFile using wg.Go with defer close
2025-12-10 19:07:11 +02:00

259 lines
6.2 KiB
Go

package config_test
import (
"errors"
"strings"
"testing"
"github.com/spf13/viper"
"github.com/ivuorinen/gibidify/config"
"github.com/ivuorinen/gibidify/shared"
)
// TestValidateConfig tests the configuration validation functionality.
func TestValidateConfig(t *testing.T) {
tests := []struct {
name string
config map[string]any
wantErr bool
errContains string
}{
{
name: "valid default config",
config: map[string]any{
"fileSizeLimit": shared.ConfigFileSizeLimitDefault,
"ignoreDirectories": []string{"node_modules", ".git"},
},
wantErr: false,
},
{
name: "file size limit too small",
config: map[string]any{
"fileSizeLimit": shared.ConfigFileSizeLimitMin - 1,
},
wantErr: true,
errContains: "fileSizeLimit",
},
{
name: "file size limit too large",
config: map[string]any{
"fileSizeLimit": shared.ConfigFileSizeLimitMax + 1,
},
wantErr: true,
errContains: "fileSizeLimit",
},
{
name: "empty ignore directory",
config: map[string]any{
"ignoreDirectories": []string{"node_modules", "", ".git"},
},
wantErr: true,
errContains: "ignoreDirectories",
},
{
name: "ignore directory with path separator",
config: map[string]any{
"ignoreDirectories": []string{"node_modules", "src/build", ".git"},
},
wantErr: true,
errContains: "path separator",
},
{
name: "invalid supported format",
config: map[string]any{
"supportedFormats": []string{"json", "xml", "yaml"},
},
wantErr: true,
errContains: "not a valid format",
},
{
name: "invalid max concurrency",
config: map[string]any{
"maxConcurrency": 0,
},
wantErr: true,
errContains: "maxConcurrency",
},
{
name: "valid comprehensive config",
config: map[string]any{
"fileSizeLimit": shared.ConfigFileSizeLimitDefault,
"ignoreDirectories": []string{"node_modules", ".git", ".vscode"},
"supportedFormats": []string{"json", "yaml", "markdown"},
"maxConcurrency": 8,
"filePatterns": []string{"*.go", "*.js", "*.py"},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
// Reset viper for each test
viper.Reset()
// Set test configuration
for key, value := range tt.config {
viper.Set(key, value)
}
// Set defaults for missing values without touching disk
config.SetDefaultConfig()
err := config.ValidateConfig()
if tt.wantErr {
validateExpectedError(t, err, tt.errContains)
} else if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
},
)
}
}
// TestIsValidFormat tests the IsValidFormat function.
func TestIsValidFormat(t *testing.T) {
tests := []struct {
format string
valid bool
}{
{"json", true},
{"yaml", true},
{"markdown", true},
{"JSON", true},
{"xml", false},
{"txt", false},
{"", false},
{" json ", true},
}
for _, tt := range tests {
result := config.IsValidFormat(tt.format)
if result != tt.valid {
t.Errorf("IsValidFormat(%q) = %v, want %v", tt.format, result, tt.valid)
}
}
}
// TestValidateFileSize tests the ValidateFileSize function.
func TestValidateFileSize(t *testing.T) {
viper.Reset()
viper.Set("fileSizeLimit", shared.ConfigFileSizeLimitDefault)
tests := []struct {
name string
size int64
wantErr bool
}{
{"size within limit", shared.ConfigFileSizeLimitDefault - 1, false},
{"size at limit", shared.ConfigFileSizeLimitDefault, false},
{"size exceeds limit", shared.ConfigFileSizeLimitDefault + 1, true},
{"zero size", 0, false},
}
for _, tt := range tests {
err := config.ValidateFileSize(tt.size)
if (err != nil) != tt.wantErr {
t.Errorf("%s: ValidateFileSize(%d) error = %v, wantErr %v", tt.name, tt.size, err, tt.wantErr)
}
}
}
// TestValidateOutputFormat tests the ValidateOutputFormat function.
func TestValidateOutputFormat(t *testing.T) {
tests := []struct {
format string
wantErr bool
}{
{"json", false},
{"yaml", false},
{"markdown", false},
{"xml", true},
{"txt", true},
{"", true},
}
for _, tt := range tests {
err := config.ValidateOutputFormat(tt.format)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateOutputFormat(%q) error = %v, wantErr %v", tt.format, err, tt.wantErr)
}
}
}
// TestValidateConcurrency tests the ValidateConcurrency function.
func TestValidateConcurrency(t *testing.T) {
tests := []struct {
name string
concurrency int
maxConcurrency int
setMax bool
wantErr bool
}{
{"valid concurrency", 4, 0, false, false},
{"minimum concurrency", 1, 0, false, false},
{"zero concurrency", 0, 0, false, true},
{"negative concurrency", -1, 0, false, true},
{"concurrency within max", 4, 8, true, false},
{"concurrency exceeds max", 16, 8, true, true},
}
for _, tt := range tests {
viper.Reset()
if tt.setMax {
viper.Set("maxConcurrency", tt.maxConcurrency)
}
err := config.ValidateConcurrency(tt.concurrency)
if (err != nil) != tt.wantErr {
t.Errorf("%s: ValidateConcurrency(%d) error = %v, wantErr %v", tt.name, tt.concurrency, err, tt.wantErr)
}
}
}
// validateExpectedError validates that an error occurred and matches expectations.
func validateExpectedError(t *testing.T, err error, errContains string) {
t.Helper()
if err == nil {
t.Error(shared.TestMsgExpectedError)
return
}
if errContains != "" && !strings.Contains(err.Error(), errContains) {
t.Errorf("Expected error to contain %q, got %q", errContains, err.Error())
}
// Check that it's a structured error
var structErr *shared.StructuredError
if !errorAs(err, &structErr) {
t.Errorf("Expected structured error, got %T", err)
return
}
if structErr.Type != shared.ErrorTypeConfiguration {
t.Errorf("Expected error type %v, got %v", shared.ErrorTypeConfiguration, structErr.Type)
}
if structErr.Code != shared.CodeConfigValidation {
t.Errorf("Expected error code %v, got %v", shared.CodeConfigValidation, structErr.Code)
}
}
func errorAs(err error, target any) bool {
if err == nil {
return false
}
structErr := &shared.StructuredError{}
if errors.As(err, &structErr) {
if ptr, ok := target.(**shared.StructuredError); ok {
*ptr = structErr
return true
}
}
return false
}