Files
gibidify/cli/errors_test.go
Ismo Vuorinen 3f65b813bd feat: update go to 1.25, add permissions and envs (#49)
* chore(ci): update go to 1.25, add permissions and envs
* fix(ci): update pr-lint.yml
* chore: update go, fix linting
* fix: tests and linting
* fix(lint): lint fixes, renovate should now pass
* fix: updates, security upgrades
* chore: workflow updates, lint
* fix: more lint, checkmake, and other fixes
* fix: more lint, convert scripts to POSIX compliant
* fix: simplify codeql workflow
* tests: increase test coverage, fix found issues
* fix(lint): editorconfig checking, add to linters
* fix(lint): shellcheck, add to linters
* fix(lint): apply cr comment suggestions
* fix(ci): remove step-security/harden-runner
* fix(lint): remove duplication, apply cr fixes
* fix(ci): tests in CI/CD pipeline
* chore(lint): deduplication of strings
* fix(lint): apply cr comment suggestions
* fix(ci): actionlint
* fix(lint): apply cr comment suggestions
* chore: lint, add deps management
2025-10-10 12:14:42 +03:00

964 lines
20 KiB
Go

package cli
import (
"bytes"
"errors"
"strings"
"testing"
"github.com/fatih/color"
"github.com/stretchr/testify/assert"
"github.com/ivuorinen/gibidify/gibidiutils"
)
func TestNewErrorFormatter(t *testing.T) {
ui := &UIManager{
output: &bytes.Buffer{},
}
ef := NewErrorFormatter(ui)
assert.NotNil(t, ef)
assert.Equal(t, ui, ef.ui)
}
func TestFormatError(t *testing.T) {
tests := []struct {
name string
err error
expectedOutput []string
notExpected []string
}{
{
name: "nil error",
err: nil,
expectedOutput: []string{},
},
{
name: "structured error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeFileSystem,
gibidiutils.CodeFSNotFound,
testErrFileNotFound,
"/test/file.txt",
map[string]interface{}{"size": 1024},
),
expectedOutput: []string{
gibidiutils.IconError + testErrorSuffix,
"FileSystem",
testErrFileNotFound,
"/test/file.txt",
"NOT_FOUND",
},
},
{
name: "generic error",
err: errors.New("something went wrong"),
expectedOutput: []string{gibidiutils.IconError + testErrorSuffix, "something went wrong"},
},
{
name: "wrapped structured error",
err: gibidiutils.WrapError(
errors.New("inner error"),
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationRequired,
"validation failed",
),
expectedOutput: []string{
gibidiutils.IconError + testErrorSuffix,
"validation failed",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
prev := color.NoColor
color.NoColor = true
t.Cleanup(func() { color.NoColor = prev })
ef := NewErrorFormatter(ui)
ef.FormatError(tt.err)
output := buf.String()
for _, expected := range tt.expectedOutput {
assert.Contains(t, output, expected)
}
for _, notExpected := range tt.notExpected {
assert.NotContains(t, output, notExpected)
}
})
}
}
func TestFormatStructuredError(t *testing.T) {
tests := []struct {
name string
err *gibidiutils.StructuredError
expectedOutput []string
}{
{
name: "filesystem error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeFileSystem,
gibidiutils.CodeFSPermission,
testErrPermissionDenied,
"/etc/shadow",
nil,
),
expectedOutput: []string{
"FileSystem",
testErrPermissionDenied,
"/etc/shadow",
"PERMISSION_DENIED",
testSuggestionsHeader,
},
},
{
name: "validation error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationFormat,
testErrInvalidFormat,
"",
map[string]interface{}{"format": "xml"},
),
expectedOutput: []string{
"Validation",
testErrInvalidFormat,
"FORMAT",
testSuggestionsHeader,
},
},
{
name: "processing error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeProcessing,
gibidiutils.CodeProcessingFileRead,
"failed to read file",
"large.bin",
nil,
),
expectedOutput: []string{
"Processing",
"failed to read file",
"large.bin",
"FILE_READ",
testSuggestionsHeader,
},
},
{
name: "IO error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeIO,
gibidiutils.CodeIOFileWrite,
"disk full",
"/output/result.txt",
nil,
),
expectedOutput: []string{
"IO",
"disk full",
"/output/result.txt",
"FILE_WRITE",
testSuggestionsHeader,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
prev := color.NoColor
color.NoColor = true
t.Cleanup(func() { color.NoColor = prev })
ef := &ErrorFormatter{ui: ui}
ef.formatStructuredError(tt.err)
output := buf.String()
for _, expected := range tt.expectedOutput {
assert.Contains(t, output, expected)
}
})
}
}
func TestFormatGenericError(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
prev := color.NoColor
color.NoColor = true
t.Cleanup(func() { color.NoColor = prev })
ef := &ErrorFormatter{ui: ui}
ef.formatGenericError(errors.New("generic error message"))
output := buf.String()
assert.Contains(t, output, gibidiutils.IconError+testErrorSuffix)
assert.Contains(t, output, "generic error message")
}
func TestProvideSuggestions(t *testing.T) {
tests := []struct {
name string
err *gibidiutils.StructuredError
expectedSugges []string
}{
{
name: "filesystem permission error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeFileSystem,
gibidiutils.CodeFSPermission,
testErrPermissionDenied,
"/root/file",
nil,
),
expectedSugges: []string{
testSuggestCheckPerms,
testSuggestVerifyPath,
},
},
{
name: "filesystem not found error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeFileSystem,
gibidiutils.CodeFSNotFound,
testErrFileNotFound,
"/missing/file",
nil,
),
expectedSugges: []string{
"Check if the file/directory exists: /missing/file",
},
},
{
name: "validation format error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationFormat,
"unsupported format",
"",
nil,
),
expectedSugges: []string{
testSuggestFormat,
testSuggestFormatEx,
},
},
{
name: "validation path error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationPath,
"invalid path",
"../../etc",
nil,
),
expectedSugges: []string{
testSuggestCheckArgs,
testSuggestHelp,
},
},
{
name: "processing file read error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeProcessing,
gibidiutils.CodeProcessingFileRead,
"read error",
"corrupted.dat",
nil,
),
expectedSugges: []string{
"Check file permissions",
"Verify the file is not corrupted",
},
},
{
name: "IO file write error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeIO,
gibidiutils.CodeIOFileWrite,
"write failed",
"/output.txt",
nil,
),
expectedSugges: []string{
testSuggestCheckPerms,
testSuggestDiskSpace,
},
},
{
name: "unknown error type",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeUnknown,
"UNKNOWN",
"unknown error",
"",
nil,
),
expectedSugges: []string{
testSuggestCheckArgs,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
prev := color.NoColor
color.NoColor = true
t.Cleanup(func() { color.NoColor = prev })
ef := &ErrorFormatter{ui: ui}
ef.provideSuggestions(tt.err)
output := buf.String()
for _, suggestion := range tt.expectedSugges {
assert.Contains(t, output, suggestion)
}
})
}
}
func TestProvideFileSystemSuggestions(t *testing.T) {
tests := []struct {
name string
err *gibidiutils.StructuredError
expectedSugges []string
}{
{
name: testErrPermissionDenied,
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeFileSystem,
gibidiutils.CodeFSPermission,
testErrPermissionDenied,
"/root/secret",
nil,
),
expectedSugges: []string{
testSuggestCheckPerms,
testSuggestVerifyPath,
},
},
{
name: "path resolution error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeFileSystem,
gibidiutils.CodeFSPathResolution,
"path error",
"../../../etc",
nil,
),
expectedSugges: []string{
"Use an absolute path instead of relative",
},
},
{
name: testErrFileNotFound,
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeFileSystem,
gibidiutils.CodeFSNotFound,
"not found",
"/missing.txt",
nil,
),
expectedSugges: []string{
"Check if the file/directory exists: /missing.txt",
},
},
{
name: "default filesystem error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeFileSystem,
"OTHER_FS_ERROR",
testErrOther,
"/some/path",
nil,
),
expectedSugges: []string{
testSuggestCheckPerms,
testSuggestVerifyPath,
"Path: /some/path",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
ef := &ErrorFormatter{ui: ui}
ef.provideFileSystemSuggestions(tt.err)
output := buf.String()
for _, suggestion := range tt.expectedSugges {
assert.Contains(t, output, suggestion)
}
})
}
}
func TestProvideValidationSuggestions(t *testing.T) {
tests := []struct {
name string
err *gibidiutils.StructuredError
expectedSugges []string
}{
{
name: "format validation",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationFormat,
testErrInvalidFormat,
"",
nil,
),
expectedSugges: []string{
testSuggestFormat,
testSuggestFormatEx,
},
},
{
name: "path validation",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationPath,
"invalid path",
"",
nil,
),
expectedSugges: []string{
testSuggestCheckArgs,
testSuggestHelp,
},
},
{
name: "size validation",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationSize,
"size error",
"",
nil,
),
expectedSugges: []string{
"Increase file size limit in config.yaml",
"Use smaller files or exclude large files",
},
},
{
name: "required validation",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationRequired,
"required",
"",
nil,
),
expectedSugges: []string{
testSuggestCheckArgs,
testSuggestHelp,
},
},
{
name: "default validation",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
"OTHER_VALIDATION",
"other",
"",
nil,
),
expectedSugges: []string{
testSuggestCheckArgs,
testSuggestHelp,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
ef := &ErrorFormatter{ui: ui}
ef.provideValidationSuggestions(tt.err)
output := buf.String()
for _, suggestion := range tt.expectedSugges {
assert.Contains(t, output, suggestion)
}
})
}
}
func TestProvideProcessingSuggestions(t *testing.T) {
tests := []struct {
name string
err *gibidiutils.StructuredError
expectedSugges []string
}{
{
name: "file read error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeProcessing,
gibidiutils.CodeProcessingFileRead,
"read error",
"",
nil,
),
expectedSugges: []string{
"Check file permissions",
"Verify the file is not corrupted",
},
},
{
name: "collection error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeProcessing,
gibidiutils.CodeProcessingCollection,
"collection error",
"",
nil,
),
expectedSugges: []string{
"Check if the source directory exists and is readable",
"Verify directory permissions",
},
},
{
name: testErrEncoding,
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeProcessing,
gibidiutils.CodeProcessingEncode,
testErrEncoding,
"",
nil,
),
expectedSugges: []string{
"Try reducing concurrency: -concurrency 1",
"Check available system resources",
},
},
{
name: "default processing",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeProcessing,
"OTHER",
testErrOther,
"",
nil,
),
expectedSugges: []string{
"Try reducing concurrency: -concurrency 1",
"Check available system resources",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
ef := &ErrorFormatter{ui: ui}
ef.provideProcessingSuggestions(tt.err)
output := buf.String()
for _, suggestion := range tt.expectedSugges {
assert.Contains(t, output, suggestion)
}
})
}
}
func TestProvideIOSuggestions(t *testing.T) {
tests := []struct {
name string
err *gibidiutils.StructuredError
expectedSugges []string
}{
{
name: "file create error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeIO,
gibidiutils.CodeIOFileCreate,
"create error",
"",
nil,
),
expectedSugges: []string{
"Check if the destination directory exists",
"Verify write permissions for the output file",
"Ensure sufficient disk space",
},
},
{
name: "file write error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeIO,
gibidiutils.CodeIOFileWrite,
"write error",
"",
nil,
),
expectedSugges: []string{
testSuggestCheckPerms,
testSuggestDiskSpace,
},
},
{
name: testErrEncoding,
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeIO,
gibidiutils.CodeIOEncoding,
testErrEncoding,
"",
nil,
),
expectedSugges: []string{
testSuggestCheckPerms,
testSuggestDiskSpace,
},
},
{
name: "default IO error",
err: gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeIO,
"OTHER",
testErrOther,
"",
nil,
),
expectedSugges: []string{
testSuggestCheckPerms,
testSuggestDiskSpace,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
ef := &ErrorFormatter{ui: ui}
ef.provideIOSuggestions(tt.err)
output := buf.String()
for _, suggestion := range tt.expectedSugges {
assert.Contains(t, output, suggestion)
}
})
}
}
func TestProvideGenericSuggestions(t *testing.T) {
tests := []struct {
name string
err error
expectedSugges []string
}{
{
name: "permission error",
err: errors.New("permission denied accessing file"),
expectedSugges: []string{
testSuggestCheckPerms,
"Try running with appropriate privileges",
},
},
{
name: "not found error",
err: errors.New("no such file or directory"),
expectedSugges: []string{
"Verify the file/directory path is correct",
"Check if the file exists",
},
},
{
name: "memory error",
err: errors.New("out of memory"),
expectedSugges: []string{
testSuggestCheckArgs,
testSuggestHelp,
testSuggestReduceConcur,
},
},
{
name: "timeout error",
err: errors.New("operation timed out"),
expectedSugges: []string{
testSuggestCheckArgs,
testSuggestHelp,
testSuggestReduceConcur,
},
},
{
name: "connection error",
err: errors.New("connection refused"),
expectedSugges: []string{
testSuggestCheckArgs,
testSuggestHelp,
testSuggestReduceConcur,
},
},
{
name: "default error",
err: errors.New("unknown error occurred"),
expectedSugges: []string{
testSuggestCheckArgs,
testSuggestHelp,
testSuggestReduceConcur,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
ef := &ErrorFormatter{ui: ui}
ef.provideGenericSuggestions(tt.err)
output := buf.String()
for _, suggestion := range tt.expectedSugges {
assert.Contains(t, output, suggestion)
}
})
}
}
func TestMissingSourceError(t *testing.T) {
err := &MissingSourceError{}
assert.Equal(t, "source directory is required", err.Error())
}
func TestNewMissingSourceErrorType(t *testing.T) {
err := NewMissingSourceError()
assert.NotNil(t, err)
assert.Equal(t, "source directory is required", err.Error())
var msErr *MissingSourceError
ok := errors.As(err, &msErr)
assert.True(t, ok)
assert.NotNil(t, msErr)
}
// Test error formatting with colors enabled
func TestFormatErrorWithColors(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: true,
output: buf,
}
prev := color.NoColor
color.NoColor = false
t.Cleanup(func() { color.NoColor = prev })
ef := NewErrorFormatter(ui)
err := gibidiutils.NewStructuredError(
gibidiutils.ErrorTypeValidation,
gibidiutils.CodeValidationFormat,
testErrInvalidFormat,
"",
nil,
)
ef.FormatError(err)
output := buf.String()
// When colors are enabled, some output may go directly to stdout
// Check for suggestions that are captured in the buffer
assert.Contains(t, output, testSuggestFormat)
assert.Contains(t, output, testSuggestFormatEx)
}
// Test wrapped error handling
func TestFormatWrappedError(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
ef := NewErrorFormatter(ui)
innerErr := errors.New("inner error")
wrappedErr := gibidiutils.WrapError(
innerErr,
gibidiutils.ErrorTypeProcessing,
gibidiutils.CodeProcessingFileRead,
"wrapper message",
)
ef.FormatError(wrappedErr)
output := buf.String()
assert.Contains(t, output, "wrapper message")
}
// Test all suggestion paths get called
func TestSuggestionPathCoverage(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
ef := &ErrorFormatter{ui: ui}
// Test all error types
errorTypes := []gibidiutils.ErrorType{
gibidiutils.ErrorTypeFileSystem,
gibidiutils.ErrorTypeValidation,
gibidiutils.ErrorTypeProcessing,
gibidiutils.ErrorTypeIO,
gibidiutils.ErrorTypeConfiguration,
gibidiutils.ErrorTypeUnknown,
}
for _, errType := range errorTypes {
t.Run(errType.String(), func(t *testing.T) {
buf.Reset()
err := gibidiutils.NewStructuredError(
errType,
"TEST_CODE",
"test error",
"/test/path",
nil,
)
ef.provideSuggestions(err)
output := buf.String()
// Should have some suggestion output
assert.NotEmpty(t, output)
})
}
}
// Test suggestion helper functions with various inputs
func TestSuggestHelpers(t *testing.T) {
tests := []struct {
name string
testFunc func(*ErrorFormatter)
}{
{
name: "suggestFileAccess",
testFunc: func(ef *ErrorFormatter) {
ef.suggestFileAccess("/root/file")
},
},
{
name: "suggestPathResolution",
testFunc: func(ef *ErrorFormatter) {
ef.suggestPathResolution("../../../etc")
},
},
{
name: "suggestFileNotFound",
testFunc: func(ef *ErrorFormatter) {
ef.suggestFileNotFound("/missing")
},
},
{
name: "suggestFileSystemGeneral",
testFunc: func(ef *ErrorFormatter) {
ef.suggestFileSystemGeneral("/path")
},
},
{
name: "provideDefaultSuggestions",
testFunc: func(ef *ErrorFormatter) {
ef.provideDefaultSuggestions()
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
ef := &ErrorFormatter{ui: ui}
tt.testFunc(ef)
output := buf.String()
// Each should produce some output
assert.NotEmpty(t, output)
// Should contain bullet point
assert.Contains(t, output, gibidiutils.IconBullet)
})
}
}
// Test edge cases in error message analysis
func TestGenericSuggestionsEdgeCases(t *testing.T) {
tests := []struct {
name string
err error
}{
{"empty message", errors.New("")},
{"very long message", errors.New(strings.Repeat("error ", 100))},
{"special characters", errors.New("error!@#$%^&*()")},
{"newlines", errors.New("error\nwith\nnewlines")},
{"unicode", errors.New("error with 中文 characters")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := &bytes.Buffer{}
ui := &UIManager{
enableColors: false,
output: buf,
}
ef := &ErrorFormatter{ui: ui}
// Should not panic
ef.provideGenericSuggestions(tt.err)
output := buf.String()
// Should have some output
assert.NotEmpty(t, output)
})
}
}