package errors import ( "context" "errors" "fmt" "io" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Custom error types for testing type CustomError struct { Code int Message string } func (e CustomError) Error() string { return fmt.Sprintf("custom error %d: %s", e.Code, e.Message) } type ValidationError struct { Field string Value string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation failed for field %s with value %s", e.Field, e.Value) } // Sentinel errors for testing var ( ErrSentinel1 = errors.New("sentinel error 1") ErrSentinel2 = errors.New("sentinel error 2") ) func TestHandleError(t *testing.T) { t.Run("HandleError with nil error", func(t *testing.T) { handled, result := HandleError(nil) assert.True(t, handled) assert.NoError(t, result) }) t.Run("HandleError with sentinel error match", func(t *testing.T) { err := io.EOF handled, result := HandleError(err, OnSentinelError(io.EOF, func(e error) error { return errors.New("handled EOF") }), ) assert.True(t, handled) assert.EqualError(t, result, "handled EOF") }) t.Run("HandleError with wrapped sentinel error", func(t *testing.T) { wrappedErr := fmt.Errorf("wrapped: %w", io.EOF) handled, result := HandleError(wrappedErr, OnSentinelError(io.EOF, func(e error) error { return errors.New("handled wrapped EOF") }), ) assert.True(t, handled) assert.EqualError(t, result, "handled wrapped EOF") }) t.Run("HandleError with custom error type match", func(t *testing.T) { err := &CustomError{Code: 404, Message: "not found"} handled, result := HandleError(err, OnCustomError(func(e *CustomError) error { return fmt.Errorf("handled custom error with code %d", e.Code) }), ) assert.True(t, handled) assert.EqualError(t, result, "handled custom error with code 404") }) t.Run("HandleError with wrapped custom error", func(t *testing.T) { customErr := &CustomError{Code: 500, Message: "internal error"} wrappedErr := fmt.Errorf("wrapped: %w", customErr) handled, result := HandleError(wrappedErr, OnCustomError(func(e *CustomError) error { return fmt.Errorf("handled wrapped custom error: %s", e.Message) }), ) assert.True(t, handled) assert.EqualError(t, result, "handled wrapped custom error: internal error") }) t.Run("HandleError with no matching handler", func(t *testing.T) { err := errors.New("unhandled error") handled, _ := HandleError(err, OnSentinelError(io.EOF, func(e error) error { return nil }), OnCustomError(func(e CustomError) error { return nil }), ) assert.False(t, handled) }) t.Run("HandleError with multiple matchers first match wins", func(t *testing.T) { err := io.EOF handled, result := HandleError(err, OnSentinelError(io.EOF, func(e error) error { return errors.New("first handler") }), OnSentinelError(io.EOF, func(e error) error { return errors.New("second handler") }), ) assert.True(t, handled) assert.EqualError(t, result, "first handler") }) t.Run("HandleError with handler returning nil", func(t *testing.T) { err := io.EOF handled, result := HandleError(err, OnSentinelError(io.EOF, func(e error) error { return nil }), ) assert.True(t, handled) assert.NoError(t, result) }) t.Run("HandleError with multiple error types", func(t *testing.T) { customErr := &CustomError{Code: 400, Message: "bad request"} validationErr := &ValidationError{Field: "email", Value: "invalid"} // Test CustomError handling handled1, result1 := HandleError(customErr, OnCustomError(func(e *CustomError) error { return fmt.Errorf("custom: %d", e.Code) }), OnCustomError(func(e *ValidationError) error { return fmt.Errorf("validation: %s", e.Field) }), ) assert.True(t, handled1) require.EqualError(t, result1, "custom: 400") // Test ValidationError handling handled2, result2 := HandleError(validationErr, OnCustomError(func(e *CustomError) error { return fmt.Errorf("custom: %d", e.Code) }), OnCustomError(func(e *ValidationError) error { return fmt.Errorf("validation: %s", e.Field) }), ) assert.True(t, handled2) assert.EqualError(t, result2, "validation: email") }) t.Run("HandleError with context errors", func(t *testing.T) { // Test context.Canceled handled1, result1 := HandleError(context.Canceled, OnSentinelError(context.Canceled, func(e error) error { return errors.New("operation canceled") }), ) assert.True(t, handled1) require.EqualError(t, result1, "operation canceled") // Test context.DeadlineExceeded handled2, result2 := HandleError(context.DeadlineExceeded, OnSentinelError(context.DeadlineExceeded, func(e error) error { return errors.New("deadline exceeded") }), ) assert.True(t, handled2) assert.EqualError(t, result2, "deadline exceeded") }) t.Run("HandleError preserves original error in handler", func(t *testing.T) { originalErr := &CustomError{Code: 403, Message: "forbidden"} var capturedErr error handled, _ := HandleError(originalErr, OnCustomError(func(e *CustomError) error { capturedErr = e return nil }), ) assert.True(t, handled) assert.Equal(t, originalErr, capturedErr) }) } func TestHandleErrorOrDie(t *testing.T) { t.Run("HandleErrorOrDie with nil error", func(t *testing.T) { result := HandleErrorOrDie(nil) assert.NoError(t, result) }) t.Run("HandleErrorOrDie with matched error", func(t *testing.T) { err := io.EOF result := HandleErrorOrDie(err, OnSentinelError(io.EOF, func(e error) error { return errors.New("handled EOF in die") }), ) assert.EqualError(t, result, "handled EOF in die") }) t.Run("HandleErrorOrDie panics on unmatched error", func(t *testing.T) { err := errors.New("unmatched error") assert.Panics(t, func() { HandleErrorOrDie(err, OnSentinelError(io.EOF, func(e error) error { return nil }), ) }) }) t.Run("HandleErrorOrDie with custom error panic", func(t *testing.T) { customErr := &CustomError{Code: 500, Message: "server error"} assert.Panics(t, func() { HandleErrorOrDie(customErr, OnCustomError(func(e *ValidationError) error { return nil }), ) }) }) t.Run("HandleErrorOrDie with multiple matchers", func(t *testing.T) { validationErr := &ValidationError{Field: "username", Value: ""} result := HandleErrorOrDie(validationErr, OnSentinelError(io.EOF, func(e error) error { return errors.New("EOF handler") }), OnCustomError(func(e *CustomError) error { return errors.New("custom handler") }), OnCustomError(func(e *ValidationError) error { return fmt.Errorf("validation handler: field=%s", e.Field) }), ) assert.EqualError(t, result, "validation handler: field=username") }) } func TestHandleErrorOrDefault(t *testing.T) { t.Run("HandleErrorOrDefault with nil error", func(t *testing.T) { defaultCalled := false result := HandleErrorOrDefault( nil, func(e error) error { defaultCalled = true return errors.New("default handler") }, ) require.NoError(t, result) assert.False(t, defaultCalled) }) t.Run("HandleErrorOrDefault with matched error", func(t *testing.T) { err := io.EOF defaultCalled := false result := HandleErrorOrDefault( err, func(e error) error { defaultCalled = true return errors.New("default handler") }, OnSentinelError(io.EOF, func(e error) error { return errors.New("handled EOF") }), ) require.EqualError(t, result, "handled EOF") assert.False(t, defaultCalled) }) t.Run("HandleErrorOrDefault with unmatched error calls default", func(t *testing.T) { err := errors.New("unmatched error") defaultCalled := false var capturedErr error result := HandleErrorOrDefault( err, func(e error) error { defaultCalled = true capturedErr = e return fmt.Errorf("default: %w", e) }, OnSentinelError(io.EOF, func(e error) error { return errors.New("EOF handler") }), ) assert.True(t, defaultCalled) assert.Equal(t, err, capturedErr) assert.EqualError(t, result, "default: unmatched error") }) t.Run("HandleErrorOrDefault with custom error match", func(t *testing.T) { customErr := &CustomError{Code: 404, Message: "not found"} defaultCalled := false result := HandleErrorOrDefault( customErr, func(e error) error { defaultCalled = true return errors.New("default handler") }, OnCustomError(func(e *CustomError) error { return fmt.Errorf("custom handler: code=%d", e.Code) }), ) assert.False(t, defaultCalled) assert.EqualError(t, result, "custom handler: code=404") }) t.Run("HandleErrorOrDefault with custom error no match", func(t *testing.T) { customErr := &CustomError{Code: 500, Message: "server error"} defaultCalled := false result := HandleErrorOrDefault( customErr, func(e error) error { defaultCalled = true return fmt.Errorf("default for custom: %v", e) }, OnCustomError(func(e *ValidationError) error { return errors.New("validation handler") }), ) assert.True(t, defaultCalled) assert.EqualError(t, result, "default for custom: custom error 500: server error") }) t.Run("HandleErrorOrDefault with multiple matchers", func(t *testing.T) { validationErr := &ValidationError{Field: "email", Value: "invalid"} defaultCalled := false result := HandleErrorOrDefault( validationErr, func(e error) error { defaultCalled = true return errors.New("default handler") }, OnSentinelError(io.EOF, func(e error) error { return errors.New("EOF handler") }), OnCustomError(func(e *CustomError) error { return errors.New("custom handler") }), OnCustomError(func(e *ValidationError) error { return fmt.Errorf("validation handler: field=%s", e.Field) }), ) assert.False(t, defaultCalled) assert.EqualError(t, result, "validation handler: field=email") }) t.Run("HandleErrorOrDefault default returns nil", func(t *testing.T) { err := errors.New("some error") defaultCalled := false result := HandleErrorOrDefault( err, func(e error) error { defaultCalled = true return nil // Default handler suppresses error }, OnSentinelError(io.EOF, func(e error) error { return errors.New("EOF handler") }), ) assert.True(t, defaultCalled) assert.NoError(t, result) }) t.Run("HandleErrorOrDefault with wrapped error", func(t *testing.T) { wrappedErr := fmt.Errorf("wrapped: %w", io.EOF) defaultCalled := false result := HandleErrorOrDefault( wrappedErr, func(e error) error { defaultCalled = true return errors.New("default handler") }, OnSentinelError(io.EOF, func(e error) error { return errors.New("handled wrapped EOF") }), ) assert.False(t, defaultCalled) assert.EqualError(t, result, "handled wrapped EOF") }) t.Run("HandleErrorOrDefault with context errors", func(t *testing.T) { // Test matched context.Canceled defaultCalled := false result1 := HandleErrorOrDefault( context.Canceled, func(e error) error { defaultCalled = true return errors.New("default handler") }, OnSentinelError(context.Canceled, func(e error) error { return errors.New("operation canceled") }), ) assert.False(t, defaultCalled) require.EqualError(t, result1, "operation canceled") // Test unmatched context.DeadlineExceeded defaultCalled = false result2 := HandleErrorOrDefault( context.DeadlineExceeded, func(e error) error { defaultCalled = true return errors.New("default: deadline exceeded") }, OnSentinelError(context.Canceled, func(e error) error { return errors.New("operation canceled") }), ) assert.True(t, defaultCalled) assert.EqualError(t, result2, "default: deadline exceeded") }) t.Run("HandleErrorOrDefault preserves error in default handler", func(t *testing.T) { originalErr := &CustomError{Code: 403, Message: "forbidden"} var capturedErr error result := HandleErrorOrDefault( originalErr, func(e error) error { capturedErr = e return fmt.Errorf("default handled: %w", e) }, OnCustomError(func(e *ValidationError) error { return errors.New("validation handler") }), ) assert.Equal(t, originalErr, capturedErr) assert.EqualError(t, result, "default handled: custom error 403: forbidden") }) t.Run("HandleErrorOrDefault default handler can access error details", func(t *testing.T) { customErr := &CustomError{Code: 500, Message: "internal error"} wrappedErr := fmt.Errorf("operation failed: %w", customErr) result := HandleErrorOrDefault( wrappedErr, func(e error) error { // Default handler can still use errors.As to extract details var ce *CustomError if errors.As(e, &ce) { return fmt.Errorf("default: found code %d", ce.Code) } return errors.New("default: unknown error") }, OnSentinelError(io.EOF, func(e error) error { return errors.New("EOF handler") }), ) assert.EqualError(t, result, "default: found code 500") }) t.Run("HandleErrorOrDefault with nil default handler and no match", func(t *testing.T) { err := errors.New("unmatched error") result := HandleErrorOrDefault( err, nil, // nil default handler OnSentinelError(io.EOF, func(e error) error { return errors.New("EOF handler") }), ) assert.NoError(t, result) // Should return nil for unmatched errors }) t.Run("HandleErrorOrDefault with nil default handler and match", func(t *testing.T) { err := io.EOF result := HandleErrorOrDefault( err, nil, // nil default handler (should not be called) OnSentinelError(io.EOF, func(e error) error { return errors.New("EOF handled") }), ) assert.EqualError(t, result, "EOF handled") // Should use the matcher, not default }) t.Run("HandleErrorOrDefault with nil default handler and nil error", func(t *testing.T) { result := HandleErrorOrDefault( nil, nil, // nil default handler OnSentinelError(io.EOF, func(e error) error { return errors.New("EOF handler") }), ) assert.NoError(t, result) // nil error should return nil }) t.Run("HandleErrorOrDefault with nil default handler suppresses custom errors", func(t *testing.T) { customErr := &CustomError{Code: 404, Message: "not found"} result := HandleErrorOrDefault( customErr, nil, // nil default handler suppresses unmatched errors OnCustomError(func(e *ValidationError) error { return errors.New("validation handler") }), ) assert.NoError(t, result) // Should return nil since no matcher matches }) } func TestOnSentinelError(t *testing.T) { t.Run("OnSentinelError creates proper matcher", func(t *testing.T) { handler := func(e error) error { return e } matcher := OnSentinelError(io.EOF, handler) assert.Equal(t, io.EOF, matcher.ErrorType) assert.True(t, matcher.IsSentinel) assert.NotNil(t, matcher.Handler) }) t.Run("OnSentinelError with custom sentinel", func(t *testing.T) { customSentinel := errors.New("custom sentinel") callCount := 0 matcher := OnSentinelError(customSentinel, func(e error) error { callCount++ return errors.New("handled custom sentinel") }) // Test that it matches the sentinel handled, result := HandleError(customSentinel, matcher) assert.True(t, handled) require.EqualError(t, result, "handled custom sentinel") assert.Equal(t, 1, callCount) // Test that it matches wrapped sentinel wrappedErr := fmt.Errorf("wrapped: %w", customSentinel) handled, result = HandleError(wrappedErr, matcher) assert.True(t, handled) require.EqualError(t, result, "handled custom sentinel") assert.Equal(t, 2, callCount) }) } func TestOnCustomError(t *testing.T) { t.Run("OnCustomError creates proper matcher", func(t *testing.T) { matcher := OnCustomError(func(e *CustomError) error { return fmt.Errorf("handled: %d", e.Code) }) assert.False(t, matcher.IsSentinel) assert.NotNil(t, matcher.Handler) // Test the handler works err := &CustomError{Code: 200, Message: "ok"} result := matcher.Handler(err) assert.EqualError(t, result, "handled: 200") }) t.Run("OnCustomError with different error types", func(t *testing.T) { // Create matchers for different types customMatcher := OnCustomError(func(e *CustomError) error { return fmt.Errorf("custom error: code=%d", e.Code) }) validationMatcher := OnCustomError(func(e *ValidationError) error { return fmt.Errorf("validation error: field=%s", e.Field) }) // Test with CustomError customErr := &CustomError{Code: 404, Message: "not found"} handled, result := HandleError(customErr, customMatcher, validationMatcher) assert.True(t, handled) require.EqualError(t, result, "custom error: code=404") // Test with ValidationError validationErr := &ValidationError{Field: "age", Value: "-1"} handled, result = HandleError(validationErr, customMatcher, validationMatcher) assert.True(t, handled) assert.EqualError(t, result, "validation error: field=age") }) t.Run("OnCustomError handler receives correct type", func(t *testing.T) { var receivedErr *CustomError matcher := OnCustomError(func(e *CustomError) error { receivedErr = e return nil }) originalErr := &CustomError{Code: 301, Message: "redirect"} handled, _ := HandleError(originalErr, matcher) assert.True(t, handled) require.NotNil(t, receivedErr) assert.Equal(t, 301, receivedErr.Code) assert.Equal(t, "redirect", receivedErr.Message) }) } func TestErrorMatcherEdgeCases(t *testing.T) { t.Run("Invalid sentinel error type in matcher", func(t *testing.T) { // Create a matcher with invalid ErrorType for sentinel matcher := ErrorMatcher{ ErrorType: "not an error", // Invalid type Handler: func(e error) error { return e }, IsSentinel: true, } err := errors.New("test error") handled, _ := HandleError(err, matcher) assert.False(t, handled) }) t.Run("Handler that panics", func(t *testing.T) { matcher := OnSentinelError(io.EOF, func(e error) error { panic("handler panic") }) assert.Panics(t, func() { HandleError(io.EOF, matcher) }) }) t.Run("Complex error chain", func(t *testing.T) { // Create a complex error chain baseErr := &CustomError{Code: 500, Message: "base"} wrapped1 := fmt.Errorf("layer1: %w", baseErr) wrapped2 := fmt.Errorf("layer2: %w", wrapped1) wrapped3 := fmt.Errorf("layer3: %w", wrapped2) handled, result := HandleError(wrapped3, OnCustomError(func(e *CustomError) error { return fmt.Errorf("found custom error at code %d", e.Code) }), ) assert.True(t, handled) assert.EqualError(t, result, "found custom error at code 500") }) }