mirror of
https://github.com/aykhans/dodo.git
synced 2025-09-08 12:00:47 +00:00
add 'Validate' method to the 'Config'
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/aykhans/dodo/pkg/types"
|
"github.com/aykhans/dodo/pkg/types"
|
||||||
@@ -29,7 +31,10 @@ var Defaults = struct {
|
|||||||
SkipVerify: false,
|
SkipVerify: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
var SupportedProxySchemes = []string{"http", "socks5", "socks5h"}
|
var (
|
||||||
|
ValidProxySchemes = []string{"http", "socks5", "socks5h"}
|
||||||
|
ValidRequestURLSchemes = []string{"http", "https"}
|
||||||
|
)
|
||||||
|
|
||||||
type IParser interface {
|
type IParser interface {
|
||||||
Parse() (*Config, error)
|
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 {
|
func ReadAllConfigs() *Config {
|
||||||
envParser := NewConfigENVParser("DODO")
|
envParser := NewConfigENVParser("DODO")
|
||||||
envConfig, err := envParser.Parse()
|
envConfig, err := envParser.Parse()
|
||||||
_ = utils.HandleErrorOrDie(err,
|
_ = utils.HandleErrorOrDie(err,
|
||||||
utils.OnCustomError(func(err types.FieldParseErrors) error {
|
utils.OnCustomError(func(err types.FieldParseErrors) error {
|
||||||
printValidationErrors("ENV", err.Errors...)
|
printParseErrors("ENV", err.Errors...)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return nil
|
return nil
|
||||||
@@ -148,7 +214,7 @@ func ReadAllConfigs() *Config {
|
|||||||
utils.OnCustomError(func(err types.FieldParseErrors) error {
|
utils.OnCustomError(func(err types.FieldParseErrors) error {
|
||||||
cliParser.PrintHelp()
|
cliParser.PrintHelp()
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
printValidationErrors("CLI", err.Errors...)
|
printParseErrors("CLI", err.Errors...)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
@@ -169,7 +235,7 @@ func ReadAllConfigs() *Config {
|
|||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
utils.OnCustomError(func(err types.FieldParseErrors) error {
|
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)
|
os.Exit(1)
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
@@ -178,6 +244,23 @@ func ReadAllConfigs() *Config {
|
|||||||
envConfig.Merge(fileConfig)
|
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
|
return envConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +293,7 @@ func parseConfigFile(configFile types.ConfigFile, maxDepth int) (*Config, error)
|
|||||||
return fileConfig, nil
|
return fileConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printValidationErrors(parserName string, errors ...types.FieldParseError) {
|
func printParseErrors(parserName string, errors ...types.FieldParseError) {
|
||||||
for _, fieldErr := range errors {
|
for _, fieldErr := range errors {
|
||||||
if fieldErr.Value == "" {
|
if fieldErr.Value == "" {
|
||||||
utils.PrintErr(text.FgYellow, "[%s] Field '%s': %v", parserName, fieldErr.Field, fieldErr.Err)
|
utils.PrintErr(text.FgYellow, "[%s] Field '%s': %v", parserName, fieldErr.Field, fieldErr.Err)
|
||||||
|
@@ -553,8 +553,8 @@ func TestParseConfigFile(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintValidationErrors(t *testing.T) {
|
func TestPrintParseErrors(t *testing.T) {
|
||||||
t.Run("printValidationErrors with empty value", func(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
|
// This function prints to stdout, so we can't easily test its output
|
||||||
// But we can test that it doesn't panic
|
// But we can test that it doesn't panic
|
||||||
errors := []types.FieldParseError{
|
errors := []types.FieldParseError{
|
||||||
@@ -567,11 +567,11 @@ func TestPrintValidationErrors(t *testing.T) {
|
|||||||
|
|
||||||
// Should not panic
|
// Should not panic
|
||||||
assert.NotPanics(t, func() {
|
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{
|
errors := []types.FieldParseError{
|
||||||
{
|
{
|
||||||
Field: "test_field",
|
Field: "test_field",
|
||||||
@@ -582,11 +582,11 @@ func TestPrintValidationErrors(t *testing.T) {
|
|||||||
|
|
||||||
// Should not panic
|
// Should not panic
|
||||||
assert.NotPanics(t, func() {
|
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{
|
errors := []types.FieldParseError{
|
||||||
{
|
{
|
||||||
Field: "field1",
|
Field: "field1",
|
||||||
@@ -602,7 +602,345 @@ func TestPrintValidationErrors(t *testing.T) {
|
|||||||
|
|
||||||
// Should not panic
|
// Should not panic
|
||||||
assert.NotPanics(t, func() {
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@@ -63,6 +63,52 @@ func (e FieldParseErrors) Error() string {
|
|||||||
return errorString
|
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 {
|
type UnmarshalError struct {
|
||||||
error error
|
error error
|
||||||
}
|
}
|
||||||
|
@@ -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) {
|
func TestErrorImplementsErrorInterface(t *testing.T) {
|
||||||
t.Run("FieldParseError implements error interface", func(t *testing.T) {
|
t.Run("FieldParseError implements error interface", func(t *testing.T) {
|
||||||
var err error = NewFieldParseError("field", "value", errors.New("test"))
|
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"))
|
var err error = NewUnmarshalError(errors.New("test"))
|
||||||
assert.Error(t, err)
|
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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user