mirror of
https://github.com/aykhans/dodo.git
synced 2025-09-10 04:50:47 +00:00
Compare commits
3 Commits
c3292dee5f
...
8b8f32d58f
Author | SHA1 | Date | |
---|---|---|---|
8b8f32d58f | |||
896bb3ad2d | |||
5cc13cfe7e |
@@ -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:
|
||||
|
@@ -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
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
@@ -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,
|
||||
)
|
||||
}
|
||||
|
@@ -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
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
|
@@ -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,
|
@@ -1,4 +1,4 @@
|
||||
package parser
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/url"
|
184
pkg/config/file.go
Normal file
184
pkg/config/file.go
Normal 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
1060
pkg/config/file_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
||||
package parser
|
||||
|
||||
import "github.com/aykhans/dodo/pkg/config"
|
||||
|
||||
type IParser interface {
|
||||
Parse() (*config.Config, error)
|
||||
}
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
})
|
||||
}
|
||||
|
Reference in New Issue
Block a user