3 Commits

Author SHA1 Message Date
8b8f32d58f add 'Validate' method to the 'Config' 2025-09-07 01:59:23 +04:00
896bb3ad2d refactor error types 2025-09-07 01:02:05 +04:00
5cc13cfe7e add config file parser 2025-09-06 23:39:10 +04:00
15 changed files with 2128 additions and 112 deletions

View File

@@ -36,7 +36,6 @@ linters:
- embeddedstructfieldcheck
- errchkjson
- errorlint
- exhaustive
- exptostd
- fatcontext
- forcetypeassert
@@ -66,7 +65,6 @@ linters:
- nilerr
- nilnesserr
- nilnil
- noctx
- nonamedreturns
- nosprintfhostport
- perfsprint
@@ -99,6 +97,7 @@ linters:
varnamelen:
ignore-decls:
- i int
- w http.ResponseWriter
exclusions:
rules:

View File

@@ -1,61 +1,11 @@
package main
import (
"fmt"
"os"
"github.com/aykhans/dodo/pkg/config/parser"
"github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/aykhans/dodo/pkg/config"
"github.com/k0kubun/pp/v3"
)
func main() {
envParser := parser.NewConfigENVParser("DODO")
envConfig, err := envParser.Parse()
_ = utils.HandleErrorOrDie(err,
utils.OnCustomError(func(err types.FieldParseErrors) error {
printValidationErrors("ENV", err.Errors...)
fmt.Println()
os.Exit(1)
return nil
}),
)
cliParser := parser.NewConfigCLIParser(os.Args)
cliConf, err := cliParser.Parse()
_ = utils.HandleErrorOrDie(err,
utils.OnSentinelError(types.ErrCLINoArgs, func(err error) error {
cliParser.PrintHelp()
utils.PrintErrAndExit(text.FgRed, 1, "\nNo arguments provided.")
return nil
}),
utils.OnCustomError(func(err types.CLIUnexpectedArgsError) error {
cliParser.PrintHelp()
utils.PrintErrAndExit(text.FgRed, 1, "\nUnexpected CLI arguments provided: %v", err.Args)
return nil
}),
utils.OnCustomError(func(err types.FieldParseErrors) error {
cliParser.PrintHelp()
fmt.Println()
printValidationErrors("CLI", err.Errors...)
fmt.Println()
os.Exit(1)
return nil
}),
)
envConfig.Merge(cliConf)
pp.Println(cliConf) //nolint
pp.Println(envConfig) //nolint
}
func printValidationErrors(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)
}
utils.PrintErr(text.FgYellow, "[%s] Field '%s' (%s): %v", parserName, fieldErr.Field, fieldErr.Value, fieldErr.Err)
}
cfg := config.ReadAllConfigs()
pp.Println(cfg) //nolint
}

1
go.mod
View File

@@ -6,6 +6,7 @@ require (
github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/k0kubun/pp/v3 v3.5.0
github.com/stretchr/testify v1.10.0
go.yaml.in/yaml/v4 v4.0.0-rc.2
)
require (

2
go.sum
View File

@@ -17,6 +17,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@@ -1,4 +1,4 @@
package parser
package config
import (
"flag"
@@ -7,7 +7,6 @@ import (
"strings"
"time"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils"
)
@@ -81,13 +80,13 @@ func (arg *stringSliceArg) Set(value string) error {
// - types.ErrCLINoArgs
// - types.CLIUnexpectedArgsError
// - types.FieldParseErrors
func (parser ConfigCLIParser) Parse() (*config.Config, error) {
func (parser ConfigCLIParser) Parse() (*Config, error) {
flagSet := flag.NewFlagSet("dodo", flag.ExitOnError)
flagSet.Usage = func() { parser.PrintHelp() }
var (
config = &config.Config{}
config = &Config{}
configFiles = stringSliceArg{}
yes bool
skipVerify bool
@@ -175,7 +174,7 @@ func (parser ConfigCLIParser) Parse() (*config.Config, error) {
utils.OnCustomError(func(err types.RemoteConfigFileParseError) error {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
types.NewFieldParseError(
fmt.Sprintf("config-file[%d]", i),
configFile,
fmt.Errorf("parse error: %w", err),
@@ -199,7 +198,7 @@ func (parser ConfigCLIParser) Parse() (*config.Config, error) {
case "url", "u":
urlParsed, err := url.Parse(urlInput)
if err != nil {
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("url", urlInput, err))
fieldParseErrors = append(fieldParseErrors, types.NewFieldParseError("url", urlInput, err))
} else {
config.URL = urlParsed
}
@@ -225,7 +224,7 @@ func (parser ConfigCLIParser) Parse() (*config.Config, error) {
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(fmt.Sprintf("proxy[%d]", i), proxy, err),
types.NewFieldParseError(fmt.Sprintf("proxy[%d]", i), proxy, err),
)
}
}
@@ -242,11 +241,11 @@ func (parser ConfigCLIParser) Parse() (*config.Config, error) {
func (parser ConfigCLIParser) PrintHelp() {
fmt.Printf(
cliUsageText+"\n",
config.Defaults.Yes,
config.Defaults.DodosCount,
config.Defaults.RequestTimeout,
config.Defaults.Method,
config.Defaults.SkipVerify,
Defaults.Yes,
Defaults.DodosCount,
Defaults.RequestTimeout,
Defaults.Method,
Defaults.SkipVerify,
)
}

View File

@@ -1,4 +1,4 @@
package parser
package config
import (
"bytes"
@@ -8,7 +8,6 @@ import (
"testing"
"time"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -479,7 +478,7 @@ func TestConfigCLIParser_PrintHelp(t *testing.T) {
assert.Contains(t, output, "-f, -config-file")
// Verify default values are included
assert.Contains(t, output, config.Defaults.Method)
assert.Contains(t, output, Defaults.Method)
assert.Contains(t, output, "1") // DodosCount default
assert.Contains(t, output, "10s") // RequestTimeout default
assert.Contains(t, output, "false") // Yes default

View File

@@ -1,11 +1,16 @@
package config
import (
"errors"
"fmt"
"net/url"
"os"
"slices"
"time"
"github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils"
"github.com/jedib0t/go-pretty/v6/text"
)
const VERSION string = "1.0.0"
@@ -26,7 +31,14 @@ var Defaults = struct {
SkipVerify: false,
}
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
var (
ValidProxySchemes = []string{"http", "socks5", "socks5h"}
ValidRequestURLSchemes = []string{"http", "https"}
)
type IParser interface {
Parse() (*Config, error)
}
type Config struct {
Files []types.ConfigFile
@@ -112,3 +124,180 @@ func (config *Config) SetDefaults() {
config.Headers = append(config.Headers, types.Header{Key: "User-Agent", Value: []string{Defaults.UserAgent}})
}
}
// 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 {
printParseErrors("ENV", err.Errors...)
fmt.Println()
os.Exit(1)
return nil
}),
)
cliParser := NewConfigCLIParser(os.Args)
cliConf, err := cliParser.Parse()
_ = utils.HandleErrorOrDie(err,
utils.OnSentinelError(types.ErrCLINoArgs, func(err error) error {
cliParser.PrintHelp()
utils.PrintErrAndExit(text.FgYellow, 1, "\nNo arguments provided.")
return nil
}),
utils.OnCustomError(func(err types.CLIUnexpectedArgsError) error {
cliParser.PrintHelp()
utils.PrintErrAndExit(text.FgYellow, 1, "\nUnexpected CLI arguments provided: %v", err.Args)
return nil
}),
utils.OnCustomError(func(err types.FieldParseErrors) error {
cliParser.PrintHelp()
fmt.Println()
printParseErrors("CLI", err.Errors...)
os.Exit(1)
return nil
}),
)
envConfig.Merge(cliConf)
for _, configFile := range envConfig.Files {
fileConfig, err := parseConfigFile(configFile, 10)
_ = utils.HandleErrorOrDie(err,
utils.OnCustomError(func(err types.ConfigFileReadError) error {
cliParser.PrintHelp()
utils.PrintErrAndExit(text.FgYellow, 1, "\nFailed to read config file '%s': %v", configFile.Path(), err)
return nil
}),
utils.OnCustomError(func(err types.UnmarshalError) error {
utils.PrintErrAndExit(text.FgYellow, 1, "\nFailed to unmarshal config file '%s': %v", configFile.Path(), err)
return nil
}),
utils.OnCustomError(func(err types.FieldParseErrors) error {
printParseErrors(fmt.Sprintf("CONFIG FILE '%s'", configFile.Path()), err.Errors...)
os.Exit(1)
return nil
}),
)
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
}
// parseConfigFile recursively parses a config file and its nested files up to maxDepth levels.
// Returns the merged configuration or an error if parsing fails.
// It can return the following errors:
// - types.ConfigFileReadError
// - types.UnmarshalError
// - types.FieldParseErrors
func parseConfigFile(configFile types.ConfigFile, maxDepth int) (*Config, error) {
configFileParser := NewConfigFileParser(configFile)
fileConfig, err := configFileParser.Parse()
if err != nil {
return nil, err
}
if maxDepth <= 0 {
return fileConfig, nil
}
for _, c := range fileConfig.Files {
innerFileConfig, err := parseConfigFile(c, maxDepth-1)
if err != nil {
return nil, err
}
fileConfig.Merge(innerFileConfig)
}
return fileConfig, nil
}
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)
}
utils.PrintErr(text.FgYellow, "[%s] Field '%s' (%s): %v", parserName, fieldErr.Field, fieldErr.Value, fieldErr.Err)
}
}

View File

@@ -1,6 +1,7 @@
package config
import (
"errors"
"net/url"
"testing"
"time"
@@ -525,3 +526,421 @@ func TestMergeConfig_AppendBehavior(t *testing.T) {
assert.Len(t, config.Proxies, originalProxies, "Empty proxies should not change existing proxies")
})
}
func TestParseConfigFile(t *testing.T) {
t.Run("parseConfigFile with maxDepth 0", func(t *testing.T) {
// Create a mock config file
configFile, _ := types.ParseConfigFile("test.yaml")
// Since we can't actually test file reading without a real file,
// we'll test the function's behavior with maxDepth
config, err := parseConfigFile(*configFile, 0)
// The function will return an error because the file doesn't exist
require.Error(t, err)
assert.Nil(t, config)
})
t.Run("parseConfigFile returns ConfigFileReadError", func(t *testing.T) {
configFile, _ := types.ParseConfigFile("/nonexistent/file.yaml")
config, err := parseConfigFile(*configFile, 1)
require.Error(t, err)
assert.Nil(t, config)
// Check if error is of type ConfigFileReadError
var readErr types.ConfigFileReadError
assert.ErrorAs(t, err, &readErr)
})
}
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{
{
Field: "test_field",
Value: "",
Err: errors.New("test error"),
},
}
// Should not panic
assert.NotPanics(t, func() {
printParseErrors("TEST", errors...)
})
})
t.Run("printParseErrors with value", func(t *testing.T) {
errors := []types.FieldParseError{
{
Field: "test_field",
Value: "test_value",
Err: errors.New("test error"),
},
}
// Should not panic
assert.NotPanics(t, func() {
printParseErrors("TEST", errors...)
})
})
t.Run("printParseErrors with multiple errors", func(t *testing.T) {
errors := []types.FieldParseError{
{
Field: "field1",
Value: "value1",
Err: errors.New("error1"),
},
{
Field: "field2",
Value: "",
Err: errors.New("error2"),
},
}
// Should not panic
assert.NotPanics(t, func() {
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)
})
}

View File

@@ -1,4 +1,4 @@
package parser
package config
import (
"errors"
@@ -7,7 +7,6 @@ import (
"os"
"time"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils"
)
@@ -25,9 +24,9 @@ func NewConfigENVParser(envPrefix string) *ConfigENVParser {
// Parse parses env arguments into a Config object.
// It can return the following errors:
// - types.FieldParseErrors
func (parser ConfigENVParser) Parse() (*config.Config, error) {
func (parser ConfigENVParser) Parse() (*Config, error) {
var (
config = &config.Config{}
config = &Config{}
fieldParseErrors []types.FieldParseError
)
@@ -38,7 +37,7 @@ func (parser ConfigENVParser) Parse() (*config.Config, error) {
utils.OnCustomError(func(err types.RemoteConfigFileParseError) error {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
types.NewFieldParseError(
parser.getFullEnvName("CONFIG_FILE"),
configFile,
fmt.Errorf("parse error: %w", err),
@@ -58,7 +57,7 @@ func (parser ConfigENVParser) Parse() (*config.Config, error) {
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
types.NewFieldParseError(
parser.getFullEnvName("YES"),
yes,
errors.New("invalid value for boolean, expected 'true' or 'false'"),
@@ -74,7 +73,7 @@ func (parser ConfigENVParser) Parse() (*config.Config, error) {
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
types.NewFieldParseError(
parser.getFullEnvName("SKIP_VERIFY"),
skipVerify,
errors.New("invalid value for boolean, expected 'true' or 'false'"),
@@ -94,7 +93,7 @@ func (parser ConfigENVParser) Parse() (*config.Config, error) {
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(parser.getFullEnvName("URL"), urlEnv, err),
types.NewFieldParseError(parser.getFullEnvName("URL"), urlEnv, err),
)
} else {
config.URL = urlEnvParsed
@@ -106,7 +105,7 @@ func (parser ConfigENVParser) Parse() (*config.Config, error) {
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
types.NewFieldParseError(
parser.getFullEnvName("DODOS"),
dodos,
errors.New("invalid value for unsigned integer"),
@@ -122,7 +121,7 @@ func (parser ConfigENVParser) Parse() (*config.Config, error) {
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
types.NewFieldParseError(
parser.getFullEnvName("REQUESTS"),
requests,
errors.New("invalid value for unsigned integer"),
@@ -138,7 +137,7 @@ func (parser ConfigENVParser) Parse() (*config.Config, error) {
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
types.NewFieldParseError(
parser.getFullEnvName("DURATION"),
duration,
errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"),
@@ -154,7 +153,7 @@ func (parser ConfigENVParser) Parse() (*config.Config, error) {
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
types.NewFieldParseError(
parser.getFullEnvName("TIMEOUT"),
timeout,
errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"),
@@ -186,7 +185,7 @@ func (parser ConfigENVParser) Parse() (*config.Config, error) {
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
types.NewFieldParseError(
parser.getFullEnvName("PROXY"),
proxy,
err,

View File

@@ -1,4 +1,4 @@
package parser
package config
import (
"net/url"

184
pkg/config/file.go Normal file
View File

@@ -0,0 +1,184 @@
package config
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
"github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils"
"go.yaml.in/yaml/v4"
)
var _ IParser = ConfigFileParser{}
type ConfigFileParser struct {
configFile types.ConfigFile
}
func NewConfigFileParser(configFile types.ConfigFile) *ConfigFileParser {
return &ConfigFileParser{configFile}
}
// Parse parses config file arguments into a Config object.
// It can return the following errors:
// - types.ConfigFileReadError
// - types.UnmarshalError
// - types.FieldParseErrors
func (parser ConfigFileParser) Parse() (*Config, error) {
var err error
var configFileData []byte
switch parser.configFile.LocationType() {
case types.ConfigFileLocationLocal:
configFileData, err = os.ReadFile(parser.configFile.Path())
if err != nil {
return nil, types.NewConfigFileReadError(err)
}
case types.ConfigFileLocationRemote:
resp, err := http.Get(parser.configFile.Path())
if err != nil {
return nil, types.NewConfigFileReadError(err)
}
defer resp.Body.Close() //nolint:errcheck
if resp.StatusCode != http.StatusOK {
return nil, types.NewConfigFileReadError(errors.New("failed to retrieve remote config file: " + resp.Status))
}
configFileData, err = io.ReadAll(resp.Body)
if err != nil {
return nil, types.NewConfigFileReadError(err)
}
default:
panic("unhandled config file location type")
}
switch parser.configFile.Type() {
case types.ConfigFileTypeYAML, types.ConfigFileTypeUnknown:
return parser.ParseYAML(configFileData)
default:
panic("unhandled config file type")
}
}
type stringOrSliceField []string
func (ss *stringOrSliceField) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind {
case yaml.ScalarNode:
// Handle single string value
*ss = []string{node.Value}
return nil
case yaml.SequenceNode:
// Handle array of strings
var slice []string
if err := node.Decode(&slice); err != nil {
return err //nolint:wrapcheck
}
*ss = slice
return nil
default:
return fmt.Errorf("expected a string or a sequence of strings, but got %v", node.Kind)
}
}
type configYAML struct {
Files stringOrSliceField `yaml:"files"`
Method *string `yaml:"method"`
URL *string `yaml:"url"`
Timeout *time.Duration `yaml:"timeout"`
DodosCount *uint `yaml:"dodos"`
RequestCount *uint `yaml:"requests"`
Duration *time.Duration `yaml:"duration"`
Yes *bool `yaml:"yes"`
SkipVerify *bool `yaml:"skipVerify"`
Params stringOrSliceField `yaml:"params"`
Headers stringOrSliceField `yaml:"headers"`
Cookies stringOrSliceField `yaml:"cookies"`
Bodies stringOrSliceField `yaml:"body"`
Proxies stringOrSliceField `yaml:"proxy"`
}
// ParseYAML parses YAML config file arguments into a Config object.
// It can return the following errors:
// - types.UnmarshalError
// - types.FieldParseErrors
func (parser ConfigFileParser) ParseYAML(data []byte) (*Config, error) {
var (
config = &Config{}
parsedData = &configYAML{}
)
err := yaml.Unmarshal(data, &parsedData)
if err != nil {
return nil, types.NewUnmarshalError(err)
}
var fieldParseErrors []types.FieldParseError
config.Method = parsedData.Method
config.Timeout = parsedData.Timeout
config.DodosCount = parsedData.DodosCount
config.RequestCount = parsedData.RequestCount
config.Duration = parsedData.Duration
config.Yes = parsedData.Yes
config.SkipVerify = parsedData.SkipVerify
config.Params.Parse(parsedData.Params...)
config.Headers.Parse(parsedData.Headers...)
config.Cookies.Parse(parsedData.Cookies...)
config.Bodies.Parse(parsedData.Bodies...)
if len(parsedData.Files) > 0 {
for i, configFile := range parsedData.Files {
configFileParsed, err := types.ParseConfigFile(configFile)
_ = utils.HandleErrorOrDie(err,
utils.OnCustomError(func(err types.RemoteConfigFileParseError) error {
fieldParseErrors = append(
fieldParseErrors,
types.NewFieldParseError(
fmt.Sprintf("config-file[%d]", i),
configFile,
fmt.Errorf("parse error: %w", err),
),
)
return nil
}),
)
if err == nil {
config.Files = append(config.Files, *configFileParsed)
}
}
}
if parsedData.URL != nil {
urlParsed, err := url.Parse(*parsedData.URL)
if err != nil {
fieldParseErrors = append(fieldParseErrors, types.NewFieldParseError("url", *parsedData.URL, err))
} else {
config.URL = urlParsed
}
}
for i, proxy := range parsedData.Proxies {
err := config.Proxies.Parse(proxy)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
types.NewFieldParseError(fmt.Sprintf("proxy[%d]", i), proxy, err),
)
}
}
if len(fieldParseErrors) > 0 {
return nil, types.NewFieldParseErrors(fieldParseErrors)
}
return config, nil
}

1060
pkg/config/file_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +0,0 @@
package parser
import "github.com/aykhans/dodo/pkg/config"
type IParser interface {
Parse() (*config.Config, error)
}

View File

@@ -23,11 +23,11 @@ type FieldParseError struct {
Err error
}
func NewFieldParseError(field string, value string, err error) *FieldParseError {
func NewFieldParseError(field string, value string, err error) FieldParseError {
if err == nil {
err = ErrNoError
}
return &FieldParseError{field, value, err}
return FieldParseError{field, value, err}
}
func (e FieldParseError) Error() string {
@@ -63,6 +63,71 @@ 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
}
func NewUnmarshalError(err error) UnmarshalError {
if err == nil {
err = ErrNoError
}
return UnmarshalError{err}
}
func (e UnmarshalError) Error() string {
return "Unmarshal error: " + e.error.Error()
}
func (e UnmarshalError) Unwrap() error {
return e.error
}
// ======================================== CLI ========================================
type CLIUnexpectedArgsError struct {

View File

@@ -74,7 +74,7 @@ func TestFieldParseErrors_Error(t *testing.T) {
})
t.Run("Error with single error returns single error message", func(t *testing.T) {
fieldErr := *NewFieldParseError("field1", "value1", errors.New("error1"))
fieldErr := NewFieldParseError("field1", "value1", errors.New("error1"))
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr})
expected := "Field 'field1' parse failed: error1"
@@ -82,9 +82,9 @@ func TestFieldParseErrors_Error(t *testing.T) {
})
t.Run("Error with multiple errors returns concatenated messages", func(t *testing.T) {
fieldErr1 := *NewFieldParseError("field1", "value1", errors.New("error1"))
fieldErr2 := *NewFieldParseError("field2", "value2", errors.New("error2"))
fieldErr3 := *NewFieldParseError("field3", "value3", errors.New("error3"))
fieldErr1 := NewFieldParseError("field1", "value1", errors.New("error1"))
fieldErr2 := NewFieldParseError("field2", "value2", errors.New("error2"))
fieldErr3 := NewFieldParseError("field3", "value3", errors.New("error3"))
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2, fieldErr3})
expected := "Field 'field1' parse failed: error1\nField 'field2' parse failed: error2\nField 'field3' parse failed: error3"
@@ -92,8 +92,8 @@ func TestFieldParseErrors_Error(t *testing.T) {
})
t.Run("Error with two errors", func(t *testing.T) {
fieldErr1 := *NewFieldParseError("username", "john", errors.New("too short"))
fieldErr2 := *NewFieldParseError("email", "invalid", errors.New("invalid format"))
fieldErr1 := NewFieldParseError("username", "john", errors.New("too short"))
fieldErr2 := NewFieldParseError("email", "invalid", errors.New("invalid format"))
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2})
expected := "Field 'username' parse failed: too short\nField 'email' parse failed: invalid format"
@@ -103,8 +103,8 @@ func TestFieldParseErrors_Error(t *testing.T) {
func TestNewFieldParseErrors(t *testing.T) {
t.Run("Creates FieldParseErrors with correct values", func(t *testing.T) {
fieldErr1 := *NewFieldParseError("field1", "value1", errors.New("error1"))
fieldErr2 := *NewFieldParseError("field2", "value2", errors.New("error2"))
fieldErr1 := NewFieldParseError("field1", "value1", errors.New("error1"))
fieldErr2 := NewFieldParseError("field2", "value2", errors.New("error2"))
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2})
assert.Len(t, fieldErrors.Errors, 2)
@@ -205,20 +205,162 @@ func TestNewRemoteConfigFileParseError(t *testing.T) {
})
}
func TestErrorConstants(t *testing.T) {
t.Run("ErrNoError has correct message", func(t *testing.T) {
expected := "no error (internal)"
assert.Equal(t, expected, ErrNoError.Error())
func TestUnmarshalError_Error(t *testing.T) {
t.Run("Error returns formatted message", func(t *testing.T) {
originalErr := errors.New("yaml parsing failed")
err := NewUnmarshalError(originalErr)
expected := "Unmarshal error: yaml parsing failed"
assert.Equal(t, expected, err.Error())
})
t.Run("ErrCLINoArgs has correct message", func(t *testing.T) {
expected := "CLI expects arguments but received none"
assert.Equal(t, expected, ErrCLINoArgs.Error())
t.Run("Error with nil underlying error", func(t *testing.T) {
err := NewUnmarshalError(nil)
expected := "Unmarshal error: no error (internal)"
assert.Equal(t, expected, err.Error())
})
}
func TestUnmarshalError_Unwrap(t *testing.T) {
t.Run("Unwrap returns original error", func(t *testing.T) {
originalErr := errors.New("original error")
err := NewUnmarshalError(originalErr)
assert.Equal(t, originalErr, err.Unwrap())
})
t.Run("ErrCLIUnexpectedArgs has correct message", func(t *testing.T) {
expected := "CLI received unexpected arguments"
assert.Equal(t, expected, ErrCLIUnexpectedArgs.Error())
t.Run("Unwrap with nil error", func(t *testing.T) {
err := NewUnmarshalError(nil)
assert.Equal(t, ErrNoError, err.Unwrap())
})
}
func TestNewUnmarshalError(t *testing.T) {
t.Run("Creates UnmarshalError with correct values", func(t *testing.T) {
originalErr := errors.New("test error")
err := NewUnmarshalError(originalErr)
assert.Equal(t, originalErr, err.error)
})
t.Run("Creates UnmarshalError with ErrNoError when nil passed", func(t *testing.T) {
err := NewUnmarshalError(nil)
assert.Equal(t, ErrNoError, err.error)
})
}
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)
})
}
@@ -242,4 +384,19 @@ func TestErrorImplementsErrorInterface(t *testing.T) {
var err error = NewRemoteConfigFileParseError(errors.New("test"))
assert.Error(t, err)
})
t.Run("UnmarshalError implements error interface", func(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)
})
}