diff --git a/pkg/config/config.go b/pkg/config/config.go index dcf4315..17d6425 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,9 +1,11 @@ package config import ( + "errors" "fmt" "net/url" "os" + "slices" "time" "github.com/aykhans/dodo/pkg/types" @@ -29,7 +31,10 @@ var Defaults = struct { SkipVerify: false, } -var SupportedProxySchemes = []string{"http", "socks5", "socks5h"} +var ( + ValidProxySchemes = []string{"http", "socks5", "socks5h"} + ValidRequestURLSchemes = []string{"http", "https"} +) type IParser interface { Parse() (*Config, error) @@ -120,12 +125,73 @@ func (config *Config) SetDefaults() { } } +// Validate validates the config fields. +// It can return the following errors: +// - types.FieldValidationErrors +func (config Config) Validate() error { + validationErrors := make([]types.FieldValidationError, 0) + + if config.Method == nil { + validationErrors = append(validationErrors, types.NewFieldValidationError("Method", "", errors.New("method is required"))) + } + + if config.URL == nil { + validationErrors = append(validationErrors, types.NewFieldValidationError("URL", "", errors.New("URL is required"))) + } else if !slices.Contains(ValidRequestURLSchemes, config.URL.Scheme) { + validationErrors = append(validationErrors, types.NewFieldValidationError("URL", config.URL.String(), fmt.Errorf("URL scheme must be one of: %v", ValidRequestURLSchemes))) + } + + if config.DodosCount == nil { + validationErrors = append(validationErrors, types.NewFieldValidationError("Dodos Count", "", errors.New("dodos count is required"))) + } else if *config.DodosCount == 0 { + validationErrors = append(validationErrors, types.NewFieldValidationError("Dodos Count", "0", errors.New("dodos count must be greater than 0"))) + } + + switch { + case config.RequestCount == nil && config.Duration == nil: + validationErrors = append(validationErrors, types.NewFieldValidationError("Request Count / Duration", "", errors.New("either request count or duration must be specified"))) + case (config.RequestCount != nil && config.Duration != nil) && (*config.RequestCount == 0 && *config.Duration == 0): + validationErrors = append(validationErrors, types.NewFieldValidationError("Request Count / Duration", "0", errors.New("both request count and duration cannot be zero"))) + case config.RequestCount != nil && config.Duration == nil && *config.RequestCount == 0: + validationErrors = append(validationErrors, types.NewFieldValidationError("Request Count", "0", errors.New("request count must be greater than 0"))) + case config.RequestCount == nil && config.Duration != nil && *config.Duration == 0: + validationErrors = append(validationErrors, types.NewFieldValidationError("Duration", "0", errors.New("duration must be greater than 0"))) + } + + if config.Yes == nil { + validationErrors = append(validationErrors, types.NewFieldValidationError("Yes", "", errors.New("yes field is required"))) + } + + if config.SkipVerify == nil { + validationErrors = append(validationErrors, types.NewFieldValidationError("Skip Verify", "", errors.New("skip verify field is required"))) + } + + for i, proxy := range config.Proxies { + if !slices.Contains(ValidProxySchemes, proxy.Scheme) { + validationErrors = append( + validationErrors, + types.NewFieldValidationError( + fmt.Sprintf("Proxy[%d]", i), + proxy.String(), + fmt.Errorf("proxy scheme must be one of: %v", ValidProxySchemes), + ), + ) + } + } + + if len(validationErrors) > 0 { + return types.NewFieldValidationErrors(validationErrors) + } + + return nil +} + func ReadAllConfigs() *Config { envParser := NewConfigENVParser("DODO") envConfig, err := envParser.Parse() _ = utils.HandleErrorOrDie(err, utils.OnCustomError(func(err types.FieldParseErrors) error { - printValidationErrors("ENV", err.Errors...) + printParseErrors("ENV", err.Errors...) fmt.Println() os.Exit(1) return nil @@ -148,7 +214,7 @@ func ReadAllConfigs() *Config { utils.OnCustomError(func(err types.FieldParseErrors) error { cliParser.PrintHelp() fmt.Println() - printValidationErrors("CLI", err.Errors...) + printParseErrors("CLI", err.Errors...) os.Exit(1) return nil }), @@ -169,7 +235,7 @@ func ReadAllConfigs() *Config { return nil }), utils.OnCustomError(func(err types.FieldParseErrors) error { - printValidationErrors(fmt.Sprintf("CONFIG FILE '%s'", configFile.Path()), err.Errors...) + printParseErrors(fmt.Sprintf("CONFIG FILE '%s'", configFile.Path()), err.Errors...) os.Exit(1) return nil }), @@ -178,6 +244,23 @@ func ReadAllConfigs() *Config { envConfig.Merge(fileConfig) } + envConfig.SetDefaults() + + err = envConfig.Validate() + _ = utils.HandleErrorOrDie(err, + utils.OnCustomError(func(err types.FieldValidationErrors) error { + for _, fieldErr := range err.Errors { + if fieldErr.Value == "" { + utils.PrintErr(text.FgYellow, "[VALIDATION] Field '%s': %v", fieldErr.Field, fieldErr.Err) + } else { + utils.PrintErr(text.FgYellow, "[VALIDATION] Field '%s' (%s): %v", fieldErr.Field, fieldErr.Value, fieldErr.Err) + } + } + os.Exit(1) + return nil + }), + ) + return envConfig } @@ -210,7 +293,7 @@ func parseConfigFile(configFile types.ConfigFile, maxDepth int) (*Config, error) return fileConfig, nil } -func printValidationErrors(parserName string, errors ...types.FieldParseError) { +func printParseErrors(parserName string, errors ...types.FieldParseError) { for _, fieldErr := range errors { if fieldErr.Value == "" { utils.PrintErr(text.FgYellow, "[%s] Field '%s': %v", parserName, fieldErr.Field, fieldErr.Err) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 83f6947..4899d1f 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -553,8 +553,8 @@ func TestParseConfigFile(t *testing.T) { }) } -func TestPrintValidationErrors(t *testing.T) { - t.Run("printValidationErrors with empty value", func(t *testing.T) { +func TestPrintParseErrors(t *testing.T) { + t.Run("printParseErrors with empty value", func(t *testing.T) { // This function prints to stdout, so we can't easily test its output // But we can test that it doesn't panic errors := []types.FieldParseError{ @@ -567,11 +567,11 @@ func TestPrintValidationErrors(t *testing.T) { // Should not panic assert.NotPanics(t, func() { - printValidationErrors("TEST", errors...) + printParseErrors("TEST", errors...) }) }) - t.Run("printValidationErrors with value", func(t *testing.T) { + t.Run("printParseErrors with value", func(t *testing.T) { errors := []types.FieldParseError{ { Field: "test_field", @@ -582,11 +582,11 @@ func TestPrintValidationErrors(t *testing.T) { // Should not panic assert.NotPanics(t, func() { - printValidationErrors("TEST", errors...) + printParseErrors("TEST", errors...) }) }) - t.Run("printValidationErrors with multiple errors", func(t *testing.T) { + t.Run("printParseErrors with multiple errors", func(t *testing.T) { errors := []types.FieldParseError{ { Field: "field1", @@ -602,7 +602,345 @@ func TestPrintValidationErrors(t *testing.T) { // Should not panic assert.NotPanics(t, func() { - printValidationErrors("TEST", errors...) + printParseErrors("TEST", errors...) }) }) } + +func TestValidate(t *testing.T) { + t.Run("Valid config returns no error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + timeout := 30 * time.Second + duration := 1 * time.Minute + + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + Timeout: &timeout, + DodosCount: utils.ToPtr(uint(5)), + RequestCount: utils.ToPtr(uint(100)), + Duration: &duration, + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + assert.NoError(t, err) + }) + + t.Run("Missing Method returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + config := Config{ + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "method is required") + }) + + t.Run("Missing URL returns validation error", func(t *testing.T) { + config := Config{ + Method: utils.ToPtr("GET"), + DodosCount: utils.ToPtr(uint(5)), + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "URL is required") + }) + + t.Run("Invalid URL scheme returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("ftp://example.com") + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "URL scheme must be one of") + }) + + t.Run("Missing DodosCount returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "dodos count is required") + }) + + t.Run("Zero DodosCount returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(0)), + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "dodos count must be greater than 0") + }) + + t.Run("Missing both RequestCount and Duration returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "either request count or duration must be specified") + }) + + t.Run("Both RequestCount and Duration zero returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + duration := time.Duration(0) + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + RequestCount: utils.ToPtr(uint(0)), + Duration: &duration, + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "both request count and duration cannot be zero") + }) + + t.Run("Zero RequestCount only returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + RequestCount: utils.ToPtr(uint(0)), + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "request count must be greater than 0") + }) + + t.Run("Zero Duration only returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + duration := time.Duration(0) + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + Duration: &duration, + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "duration must be greater than 0") + }) + + t.Run("Missing Yes returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + RequestCount: utils.ToPtr(uint(100)), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "yes field is required") + }) + + t.Run("Missing SkipVerify returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + RequestCount: utils.ToPtr(uint(100)), + Yes: utils.ToPtr(true), + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "skip verify field is required") + }) + + t.Run("Invalid proxy scheme returns validation error", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + proxyURL, _ := url.Parse("ftp://proxy.example.com:8080") + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + RequestCount: utils.ToPtr(uint(100)), + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + Proxies: types.Proxies{types.Proxy(*proxyURL)}, + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Contains(t, err.Error(), "proxy scheme must be one of") + assert.Contains(t, err.Error(), "Proxy[0]") + }) + + t.Run("Multiple invalid proxies return validation errors", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + proxyURL1, _ := url.Parse("ftp://proxy1.example.com:8080") + proxyURL2, _ := url.Parse("ldap://proxy2.example.com:389") + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + RequestCount: utils.ToPtr(uint(100)), + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + Proxies: types.Proxies{types.Proxy(*proxyURL1), types.Proxy(*proxyURL2)}, + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Len(t, validationErr.Errors, 2) + assert.Contains(t, err.Error(), "Proxy[0]") + assert.Contains(t, err.Error(), "Proxy[1]") + }) + + t.Run("Valid proxy schemes pass validation", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + proxyURL1, _ := url.Parse("http://proxy1.example.com:8080") + proxyURL2, _ := url.Parse("socks5://proxy2.example.com:1080") + proxyURL3, _ := url.Parse("socks5h://proxy3.example.com:1080") + config := Config{ + Method: utils.ToPtr("GET"), + URL: testURL, + DodosCount: utils.ToPtr(uint(5)), + RequestCount: utils.ToPtr(uint(100)), + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + Proxies: types.Proxies{types.Proxy(*proxyURL1), types.Proxy(*proxyURL2), types.Proxy(*proxyURL3)}, + } + + err := config.Validate() + assert.NoError(t, err) + }) + + t.Run("Multiple validation errors are collected", func(t *testing.T) { + config := Config{ + // Missing Method, URL, DodosCount, Yes, SkipVerify + // Missing both RequestCount and Duration + } + + err := config.Validate() + require.Error(t, err) + + var validationErr types.FieldValidationErrors + require.ErrorAs(t, err, &validationErr) + assert.Len(t, validationErr.Errors, 6) // All required fields missing + assert.Contains(t, err.Error(), "method is required") + assert.Contains(t, err.Error(), "URL is required") + assert.Contains(t, err.Error(), "dodos count is required") + assert.Contains(t, err.Error(), "either request count or duration must be specified") + assert.Contains(t, err.Error(), "yes field is required") + assert.Contains(t, err.Error(), "skip verify field is required") + }) + + t.Run("Valid config with Duration only passes validation", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + duration := 30 * time.Second + config := Config{ + Method: utils.ToPtr("POST"), + URL: testURL, + DodosCount: utils.ToPtr(uint(10)), + Duration: &duration, + Yes: utils.ToPtr(false), + SkipVerify: utils.ToPtr(true), + } + + err := config.Validate() + assert.NoError(t, err) + }) + + t.Run("Valid config with RequestCount only passes validation", func(t *testing.T) { + testURL, _ := url.Parse("https://example.com") + config := Config{ + Method: utils.ToPtr("PUT"), + URL: testURL, + DodosCount: utils.ToPtr(uint(3)), + RequestCount: utils.ToPtr(uint(50)), + Yes: utils.ToPtr(true), + SkipVerify: utils.ToPtr(false), + } + + err := config.Validate() + assert.NoError(t, err) + }) +} diff --git a/pkg/types/errors.go b/pkg/types/errors.go index 66be7ce..700dd8e 100644 --- a/pkg/types/errors.go +++ b/pkg/types/errors.go @@ -63,6 +63,52 @@ func (e FieldParseErrors) Error() string { return errorString } +type FieldValidationError struct { + Field string + Value string + Err error +} + +func NewFieldValidationError(field string, value string, err error) FieldValidationError { + if err == nil { + err = ErrNoError + } + return FieldValidationError{field, value, err} +} + +func (e FieldValidationError) Error() string { + return fmt.Sprintf("Field '%s' validation failed: %v", e.Field, e.Err) +} + +func (e FieldValidationError) Unwrap() error { + return e.Err +} + +type FieldValidationErrors struct { + Errors []FieldValidationError +} + +func NewFieldValidationErrors(fieldValidationErrors []FieldValidationError) FieldValidationErrors { + return FieldValidationErrors{fieldValidationErrors} +} + +func (e FieldValidationErrors) Error() string { + if len(e.Errors) == 0 { + return "No field validation errors" + } + if len(e.Errors) == 1 { + return e.Errors[0].Error() + } + + errorString := "" + for _, err := range e.Errors { + errorString += err.Error() + "\n" + } + errorString, _ = strings.CutSuffix(errorString, "\n") + + return errorString +} + type UnmarshalError struct { error error } diff --git a/pkg/types/errors_test.go b/pkg/types/errors_test.go index 2b9c257..dab5aa8 100644 --- a/pkg/types/errors_test.go +++ b/pkg/types/errors_test.go @@ -252,6 +252,118 @@ func TestNewUnmarshalError(t *testing.T) { }) } +func TestFieldValidationError_Error(t *testing.T) { + t.Run("Error returns formatted message", func(t *testing.T) { + originalErr := errors.New("invalid value") + fieldErr := NewFieldValidationError("username", "testuser", originalErr) + + expected := "Field 'username' validation failed: invalid value" + assert.Equal(t, expected, fieldErr.Error()) + }) + + t.Run("Error with empty field name", func(t *testing.T) { + originalErr := errors.New("test error") + fieldErr := NewFieldValidationError("", "somevalue", originalErr) + + expected := "Field '' validation failed: test error" + assert.Equal(t, expected, fieldErr.Error()) + }) + + t.Run("Error with nil underlying error", func(t *testing.T) { + fieldErr := NewFieldValidationError("field", "value123", nil) + + expected := "Field 'field' validation failed: no error (internal)" + assert.Equal(t, expected, fieldErr.Error()) + }) +} + +func TestFieldValidationError_Unwrap(t *testing.T) { + t.Run("Unwrap returns original error", func(t *testing.T) { + originalErr := errors.New("original error") + fieldErr := NewFieldValidationError("field", "value", originalErr) + + assert.Equal(t, originalErr, fieldErr.Unwrap()) + }) + + t.Run("Unwrap with nil error", func(t *testing.T) { + fieldErr := NewFieldValidationError("field", "value", nil) + + assert.Equal(t, ErrNoError, fieldErr.Unwrap()) + }) +} + +func TestNewFieldValidationError(t *testing.T) { + t.Run("Creates FieldValidationError with correct values", func(t *testing.T) { + originalErr := errors.New("test error") + fieldErr := NewFieldValidationError("testField", "testValue", originalErr) + + assert.Equal(t, "testField", fieldErr.Field) + assert.Equal(t, "testValue", fieldErr.Value) + assert.Equal(t, originalErr, fieldErr.Err) + }) + + t.Run("Creates FieldValidationError with ErrNoError when nil passed", func(t *testing.T) { + fieldErr := NewFieldValidationError("testField", "testValue", nil) + + assert.Equal(t, "testField", fieldErr.Field) + assert.Equal(t, "testValue", fieldErr.Value) + assert.Equal(t, ErrNoError, fieldErr.Err) + }) +} + +func TestFieldValidationErrors_Error(t *testing.T) { + t.Run("Error with no errors returns default message", func(t *testing.T) { + fieldErrors := NewFieldValidationErrors([]FieldValidationError{}) + + assert.Equal(t, "No field validation errors", fieldErrors.Error()) + }) + + t.Run("Error with single error returns single error message", func(t *testing.T) { + fieldErr := NewFieldValidationError("field1", "value1", errors.New("error1")) + fieldErrors := NewFieldValidationErrors([]FieldValidationError{fieldErr}) + + expected := "Field 'field1' validation failed: error1" + assert.Equal(t, expected, fieldErrors.Error()) + }) + + t.Run("Error with multiple errors returns concatenated messages", func(t *testing.T) { + fieldErr1 := NewFieldValidationError("field1", "value1", errors.New("error1")) + fieldErr2 := NewFieldValidationError("field2", "value2", errors.New("error2")) + fieldErr3 := NewFieldValidationError("field3", "value3", errors.New("error3")) + fieldErrors := NewFieldValidationErrors([]FieldValidationError{fieldErr1, fieldErr2, fieldErr3}) + + expected := "Field 'field1' validation failed: error1\nField 'field2' validation failed: error2\nField 'field3' validation failed: error3" + assert.Equal(t, expected, fieldErrors.Error()) + }) + + t.Run("Error with two errors", func(t *testing.T) { + fieldErr1 := NewFieldValidationError("username", "john", errors.New("too short")) + fieldErr2 := NewFieldValidationError("email", "invalid", errors.New("invalid format")) + fieldErrors := NewFieldValidationErrors([]FieldValidationError{fieldErr1, fieldErr2}) + + expected := "Field 'username' validation failed: too short\nField 'email' validation failed: invalid format" + assert.Equal(t, expected, fieldErrors.Error()) + }) +} + +func TestNewFieldValidationErrors(t *testing.T) { + t.Run("Creates FieldValidationErrors with correct values", func(t *testing.T) { + fieldErr1 := NewFieldValidationError("field1", "value1", errors.New("error1")) + fieldErr2 := NewFieldValidationError("field2", "value2", errors.New("error2")) + fieldErrors := NewFieldValidationErrors([]FieldValidationError{fieldErr1, fieldErr2}) + + assert.Len(t, fieldErrors.Errors, 2) + assert.Equal(t, fieldErr1, fieldErrors.Errors[0]) + assert.Equal(t, fieldErr2, fieldErrors.Errors[1]) + }) + + t.Run("Creates FieldValidationErrors with empty slice", func(t *testing.T) { + fieldErrors := NewFieldValidationErrors([]FieldValidationError{}) + + assert.Empty(t, fieldErrors.Errors) + }) +} + func TestErrorImplementsErrorInterface(t *testing.T) { t.Run("FieldParseError implements error interface", func(t *testing.T) { var err error = NewFieldParseError("field", "value", errors.New("test")) @@ -277,4 +389,14 @@ func TestErrorImplementsErrorInterface(t *testing.T) { var err error = NewUnmarshalError(errors.New("test")) assert.Error(t, err) }) + + t.Run("FieldValidationError implements error interface", func(t *testing.T) { + var err error = NewFieldValidationError("field", "value", errors.New("test")) + assert.Error(t, err) + }) + + t.Run("FieldValidationErrors implements error interface", func(t *testing.T) { + var err error = NewFieldValidationErrors([]FieldValidationError{}) + assert.Error(t, err) + }) }