diff --git a/cmd/cli/main.go b/cmd/cli/main.go index f289669..eb7babb 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -4,16 +4,27 @@ import ( "fmt" "os" - "github.com/aykhans/dodo/pkg/config" + "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/k0kubun/pp/v3" ) func main() { - cliParser := config.NewConfigCLIParser(os.Args) - cfg, err := cliParser.Parse() + 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() @@ -35,7 +46,9 @@ func main() { }), ) - fmt.Println(cfg) + envConfig.Merge(cliConf) + pp.Println(cliConf) //nolint + pp.Println(envConfig) //nolint } func printValidationErrors(parserName string, errors ...types.FieldParseError) { diff --git a/go.mod b/go.mod index c74e483..8a24417 100644 --- a/go.mod +++ b/go.mod @@ -4,15 +4,18 @@ go 1.25 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 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 42dbb42..e9006c6 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/k0kubun/pp/v3 v3.5.0 h1:iYNlYA5HJAJvkD4ibuf9c8y6SHM0QFhaBuCqm1zHp0w= +github.com/k0kubun/pp/v3 v3.5.0/go.mod h1:5lzno5ZZeEeTV/Ky6vs3g6d1U3WarDrH8k240vMtGro= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -11,10 +17,11 @@ 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= +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= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/config/config.go b/pkg/config/config.go index 0d30dc6..8d4d7a8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -49,7 +49,7 @@ func NewConfig() *Config { return &Config{} } -func (config *Config) MergeConfig(newConfig *Config) { +func (config *Config) Merge(newConfig *Config) { config.Files = append(config.Files, newConfig.Files...) if newConfig.Method != nil { config.Method = newConfig.Method @@ -75,19 +75,19 @@ func (config *Config) MergeConfig(newConfig *Config) { if newConfig.SkipVerify != nil { config.SkipVerify = newConfig.SkipVerify } - if len(newConfig.Params) != 0 { + if len(newConfig.Params) != 0 { // TODO: append config.Params = newConfig.Params } - if len(newConfig.Headers) != 0 { + if len(newConfig.Headers) != 0 { // TODO: append config.Headers = newConfig.Headers } - if len(newConfig.Cookies) != 0 { + if len(newConfig.Cookies) != 0 { // TODO: append config.Cookies = newConfig.Cookies } - if len(newConfig.Bodies) != 0 { + if len(newConfig.Bodies) != 0 { // TODO: append config.Bodies = newConfig.Bodies } - if len(newConfig.Proxies) != 0 { + if len(newConfig.Proxies) != 0 { // TODO: append config.Proxies = newConfig.Proxies } } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index ff8a2d2..0ab0814 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -56,7 +56,7 @@ func TestMergeConfig(t *testing.T) { Proxies: types.Proxies{}, } - config.MergeConfig(newConfig) + config.Merge(newConfig) assert.Equal(t, "POST", *config.Method) assert.Equal(t, newURL, config.URL) @@ -93,7 +93,7 @@ func TestMergeConfig(t *testing.T) { DodosCount: utils.ToPtr(uint(10)), } - config.MergeConfig(newConfig) + config.Merge(newConfig) assert.Equal(t, "GET", *config.Method, "Method should remain unchanged") assert.Equal(t, newURL, config.URL, "URL should be updated") @@ -127,7 +127,7 @@ func TestMergeConfig(t *testing.T) { } originalConfigCopy := *config - config.MergeConfig(newConfig) + config.Merge(newConfig) assert.Equal(t, originalConfigCopy.Method, config.Method) assert.Equal(t, originalConfigCopy.URL, config.URL) @@ -157,7 +157,7 @@ func TestMergeConfig(t *testing.T) { Proxies: types.Proxies{}, } - config.MergeConfig(newConfig) + config.Merge(newConfig) assert.Equal(t, []types.ConfigFile{*configFile}, config.Files, "Empty Files should not override") assert.Equal(t, types.Params{{Key: "original", Value: []string{"value"}}}, config.Params, "Empty Params should not override") @@ -182,7 +182,7 @@ func TestMergeConfig(t *testing.T) { Method: utils.ToPtr("POST"), } - config.MergeConfig(newConfig) + config.Merge(newConfig) assert.Equal(t, "POST", *config.Method, "Method should be updated") assert.Equal(t, []types.ConfigFile{*configFile1, *configFile2}, config.Files, "Files should be appended") @@ -213,7 +213,7 @@ func TestMergeConfig(t *testing.T) { Proxies: types.Proxies{}, } - config.MergeConfig(newConfig) + config.Merge(newConfig) assert.Equal(t, "POST", *config.Method) assert.Equal(t, newURL, config.URL) diff --git a/pkg/config/parser/base.go b/pkg/config/parser/base.go new file mode 100644 index 0000000..4391f34 --- /dev/null +++ b/pkg/config/parser/base.go @@ -0,0 +1,7 @@ +package parser + +import "github.com/aykhans/dodo/pkg/config" + +type IParser interface { + Parse() (*config.Config, error) +} diff --git a/pkg/config/cli.go b/pkg/config/parser/cli.go similarity index 96% rename from pkg/config/cli.go rename to pkg/config/parser/cli.go index b18d0a6..2828eb7 100644 --- a/pkg/config/cli.go +++ b/pkg/config/parser/cli.go @@ -1,4 +1,4 @@ -package config +package parser import ( "errors" @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/aykhans/dodo/pkg/config" "github.com/aykhans/dodo/pkg/types" "github.com/aykhans/dodo/pkg/utils" ) @@ -52,6 +53,8 @@ Flags: -x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080") -skip-verify bool Skip SSL/TLS certificate verification (default %v)` +var _ IParser = ConfigCLIParser{} + type ConfigCLIParser struct { args []string } @@ -79,13 +82,13 @@ func (arg *stringSliceArg) Set(value string) error { // - types.ErrCLINoArgs // - types.CLIUnexpectedArgsError // - types.FieldParseErrors -func (parser *ConfigCLIParser) Parse() (*Config, error) { +func (parser ConfigCLIParser) Parse() (*config.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 @@ -259,14 +262,14 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) { return config, nil } -func (parser *ConfigCLIParser) PrintHelp() { +func (parser ConfigCLIParser) PrintHelp() { fmt.Printf( cliUsageText+"\n", - Defaults.Yes, - Defaults.DodosCount, - Defaults.RequestTimeout, - Defaults.Method, - Defaults.SkipVerify, + config.Defaults.Yes, + config.Defaults.DodosCount, + config.Defaults.RequestTimeout, + config.Defaults.Method, + config.Defaults.SkipVerify, ) } diff --git a/pkg/config/cli_test.go b/pkg/config/parser/cli_test.go similarity index 99% rename from pkg/config/cli_test.go rename to pkg/config/parser/cli_test.go index 4c6b149..d02b05e 100644 --- a/pkg/config/cli_test.go +++ b/pkg/config/parser/cli_test.go @@ -1,4 +1,4 @@ -package config +package parser import ( "bytes" @@ -8,6 +8,7 @@ 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" @@ -485,7 +486,7 @@ func TestConfigCLIParser_PrintHelp(t *testing.T) { assert.Contains(t, output, "-f, -config-file") // Verify default values are included - assert.Contains(t, output, Defaults.Method) + assert.Contains(t, output, config.Defaults.Method) assert.Contains(t, output, "1") // DodosCount default assert.Contains(t, output, "10s") // RequestTimeout default assert.Contains(t, output, "false") // Yes default diff --git a/pkg/config/parser/env.go b/pkg/config/parser/env.go new file mode 100644 index 0000000..bfc4271 --- /dev/null +++ b/pkg/config/parser/env.go @@ -0,0 +1,236 @@ +package parser + +import ( + "errors" + "fmt" + "net/url" + "os" + "time" + + "github.com/aykhans/dodo/pkg/config" + "github.com/aykhans/dodo/pkg/types" + "github.com/aykhans/dodo/pkg/utils" +) + +var _ IParser = ConfigENVParser{} + +type ConfigENVParser struct { + envPrefix string +} + +func NewConfigENVParser(envPrefix string) *ConfigENVParser { + return &ConfigENVParser{envPrefix} +} + +// Parse parses env arguments into a Config object. +// It can return the following errors: +// - types.FieldParseErrors +func (parser ConfigENVParser) Parse() (*config.Config, error) { + var ( + config = &config.Config{} + fieldParseErrors []types.FieldParseError + ) + + if configFile := parser.getEnv("CONFIG_FILE"); configFile != "" { + configFileParsed, err := types.ParseConfigFile(configFile) + + _ = utils.HandleErrorOrDie(err, + utils.OnSentinelError(types.ErrConfigFileExtensionNotFound, func(err error) error { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError( + parser.getFullEnvName("CONFIG_FILE"), + configFile, + errors.New("file extension not found"), + ), + ) + return nil + }), + utils.OnCustomError(func(err types.RemoteConfigFileParseError) error { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError( + parser.getFullEnvName("CONFIG_FILE"), + configFile, + fmt.Errorf("parse error: %w", err), + ), + ) + return nil + }), + utils.OnCustomError(func(err types.UnknownConfigFileTypeError) error { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError( + parser.getFullEnvName("CONFIG_FILE"), + configFile, + fmt.Errorf("file type '%s' not supported (supported types: %s)", err.Type, types.ConfigFileTypeYAML), + ), + ) + return nil + }), + ) + + if err == nil { + config.Files = append(config.Files, *configFileParsed) + } + } + + if yes := parser.getEnv("YES"); yes != "" { + yesParsed, err := utils.ParseString[bool](yes) + if err != nil { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError( + parser.getFullEnvName("YES"), + yes, + errors.New("invalid value for boolean, expected 'true' or 'false'"), + ), + ) + } else { + config.Yes = &yesParsed + } + } + + if skipVerify := parser.getEnv("SKIP_VERIFY"); skipVerify != "" { + skipVerifyParsed, err := utils.ParseString[bool](skipVerify) + if err != nil { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError( + parser.getFullEnvName("SKIP_VERIFY"), + skipVerify, + errors.New("invalid value for boolean, expected 'true' or 'false'"), + ), + ) + } else { + config.SkipVerify = &skipVerifyParsed + } + } + + if method := parser.getEnv("METHOD"); method != "" { + config.Method = &method + } + + if urlEnv := parser.getEnv("URL"); urlEnv != "" { + urlEnvParsed, err := url.Parse(urlEnv) + if err != nil { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError(parser.getFullEnvName("URL"), urlEnv, err), + ) + } else { + config.URL = urlEnvParsed + } + } + + if dodos := parser.getEnv("DODOS"); dodos != "" { + dodosParsed, err := utils.ParseString[uint](dodos) + if err != nil { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError( + parser.getFullEnvName("DODOS"), + dodos, + errors.New("invalid value for unsigned integer"), + ), + ) + } else { + config.DodosCount = &dodosParsed + } + } + + if requests := parser.getEnv("REQUESTS"); requests != "" { + requestsParsed, err := utils.ParseString[uint](requests) + if err != nil { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError( + parser.getFullEnvName("REQUESTS"), + requests, + errors.New("invalid value for unsigned integer"), + ), + ) + } else { + config.RequestCount = &requestsParsed + } + } + + if duration := parser.getEnv("DURATION"); duration != "" { + durationParsed, err := utils.ParseString[time.Duration](duration) + if err != nil { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError( + parser.getFullEnvName("DURATION"), + duration, + errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"), + ), + ) + } else { + config.Duration = &durationParsed + } + } + + if timeout := parser.getEnv("TIMEOUT"); timeout != "" { + timeoutParsed, err := utils.ParseString[time.Duration](timeout) + if err != nil { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError( + parser.getFullEnvName("TIMEOUT"), + timeout, + errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"), + ), + ) + } else { + config.Timeout = &timeoutParsed + } + } + + if param := parser.getEnv("PARAM"); param != "" { + config.Params.Parse(param) + } + + if header := parser.getEnv("HEADER"); header != "" { + config.Headers.Parse(header) + } + + if cookie := parser.getEnv("COOKIE"); cookie != "" { + config.Cookies.Parse(cookie) + } + + if body := parser.getEnv("BODY"); body != "" { + config.Bodies.Parse(body) + } + + if proxy := parser.getEnv("PROXY"); proxy != "" { + err := config.Proxies.Parse(proxy) + if err != nil { + fieldParseErrors = append( + fieldParseErrors, + *types.NewFieldParseError( + parser.getFullEnvName("PROXY"), + proxy, + err, + ), + ) + } + } + + if len(fieldParseErrors) > 0 { + return nil, types.NewFieldParseErrors(fieldParseErrors) + } + + return config, nil +} + +func (parser ConfigENVParser) getFullEnvName(envName string) string { + if parser.envPrefix == "" { + return envName + } + return parser.envPrefix + "_" + envName +} + +func (parser ConfigENVParser) getEnv(envName string) string { + return os.Getenv(parser.getFullEnvName(envName)) +} diff --git a/pkg/config/parser/env_test.go b/pkg/config/parser/env_test.go new file mode 100644 index 0000000..48d23fc --- /dev/null +++ b/pkg/config/parser/env_test.go @@ -0,0 +1,1166 @@ +package parser + +import ( + "net/url" + "testing" + "time" + + "github.com/aykhans/dodo/pkg/config" + "github.com/aykhans/dodo/pkg/types" + "github.com/aykhans/dodo/pkg/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewConfigENVParser(t *testing.T) { + t.Run("NewConfigENVParser with empty prefix", func(t *testing.T) { + parser := NewConfigENVParser("") + require.NotNil(t, parser) + assert.Empty(t, parser.envPrefix) + }) + + t.Run("NewConfigENVParser with prefix", func(t *testing.T) { + parser := NewConfigENVParser("DODO") + require.NotNil(t, parser) + assert.Equal(t, "DODO", parser.envPrefix) + }) +} + +func TestConfigENVParser_getFullEnvName(t *testing.T) { + t.Run("getFullEnvName with empty prefix", func(t *testing.T) { + parser := ConfigENVParser{envPrefix: ""} + assert.Equal(t, "URL", parser.getFullEnvName("URL")) + assert.Equal(t, "METHOD", parser.getFullEnvName("METHOD")) + }) + + t.Run("getFullEnvName with prefix", func(t *testing.T) { + parser := ConfigENVParser{envPrefix: "DODO"} + assert.Equal(t, "DODO_URL", parser.getFullEnvName("URL")) + assert.Equal(t, "DODO_METHOD", parser.getFullEnvName("METHOD")) + }) + + t.Run("getFullEnvName with complex prefix", func(t *testing.T) { + parser := ConfigENVParser{envPrefix: "MY_APP"} + assert.Equal(t, "MY_APP_CONFIG_FILE", parser.getFullEnvName("CONFIG_FILE")) + }) +} + +func TestConfigENVParser_getEnv(t *testing.T) { + t.Run("getEnv with empty prefix", func(t *testing.T) { + t.Setenv("TEST_ENV_VAR", "test_value") + + parser := ConfigENVParser{envPrefix: ""} + assert.Equal(t, "test_value", parser.getEnv("TEST_ENV_VAR")) + }) + + t.Run("getEnv with prefix", func(t *testing.T) { + t.Setenv("PREFIX_TEST_VAR", "prefixed_value") + + parser := ConfigENVParser{envPrefix: "PREFIX"} + assert.Equal(t, "prefixed_value", parser.getEnv("TEST_VAR")) + }) + + t.Run("getEnv for non-existent variable", func(t *testing.T) { + parser := ConfigENVParser{envPrefix: "PREFIX"} + assert.Empty(t, parser.getEnv("NON_EXISTENT")) + }) +} + +func TestConfigENVParser_Parse_ConfigFile(t *testing.T) { + t.Run("Parse with valid YAML config file", func(t *testing.T) { + t.Setenv("CONFIG_FILE", "/path/to/config.yaml") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Files, 1) + assert.Equal(t, "/path/to/config.yaml", config.Files[0].String()) + assert.Equal(t, types.ConfigFileTypeYAML, config.Files[0].Type()) + }) + + t.Run("Parse with config file without extension", func(t *testing.T) { + t.Setenv("CONFIG_FILE", "/path/to/config") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + assert.Len(t, fieldErr.Errors, 1) + assert.Equal(t, "CONFIG_FILE", fieldErr.Errors[0].Field) + assert.Equal(t, "/path/to/config", fieldErr.Errors[0].Value) + assert.Contains(t, fieldErr.Errors[0].Err.Error(), "file extension not found") + }) + + t.Run("Parse with unsupported config file type", func(t *testing.T) { + t.Setenv("CONFIG_FILE", "/path/to/config.json") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + assert.Len(t, fieldErr.Errors, 1) + assert.Equal(t, "CONFIG_FILE", fieldErr.Errors[0].Field) + assert.Equal(t, "/path/to/config.json", fieldErr.Errors[0].Value) + assert.Contains(t, fieldErr.Errors[0].Err.Error(), "file type") + assert.Contains(t, fieldErr.Errors[0].Err.Error(), "not supported") + }) + + t.Run("Parse with remote config file URL", func(t *testing.T) { + t.Setenv("CONFIG_FILE", "https://example.com/config.yaml") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Files, 1) + assert.Equal(t, "https://example.com/config.yaml", config.Files[0].String()) + assert.Equal(t, types.ConfigFileTypeYAML, config.Files[0].Type()) + assert.Equal(t, types.ConfigFileLocationRemote, config.Files[0].LocationType()) + }) + + t.Run("Parse with config file using prefix", func(t *testing.T) { + t.Setenv("DODO_CONFIG_FILE", "/path/to/config.yml") + + parser := NewConfigENVParser("DODO") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Files, 1) + }) +} + +func TestConfigENVParser_Parse_BooleanFields(t *testing.T) { + t.Run("Parse with YES=true", func(t *testing.T) { + t.Setenv("YES", "true") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Yes) + assert.True(t, *config.Yes) + }) + + t.Run("Parse with YES=false", func(t *testing.T) { + t.Setenv("YES", "false") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Yes) + assert.False(t, *config.Yes) + }) + + t.Run("Parse with invalid YES value", func(t *testing.T) { + t.Setenv("YES", "maybe") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + assert.Len(t, fieldErr.Errors, 1) + assert.Equal(t, "YES", fieldErr.Errors[0].Field) + assert.Equal(t, "maybe", fieldErr.Errors[0].Value) + assert.Contains(t, fieldErr.Errors[0].Err.Error(), "invalid value for boolean") + }) + + t.Run("Parse with SKIP_VERIFY=true", func(t *testing.T) { + t.Setenv("SKIP_VERIFY", "true") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.SkipVerify) + assert.True(t, *config.SkipVerify) + }) + + t.Run("Parse with invalid SKIP_VERIFY value", func(t *testing.T) { + t.Setenv("SKIP_VERIFY", "invalid") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + assert.Len(t, fieldErr.Errors, 1) + assert.Equal(t, "SKIP_VERIFY", fieldErr.Errors[0].Field) + assert.Equal(t, "invalid", fieldErr.Errors[0].Value) + assert.Contains(t, fieldErr.Errors[0].Err.Error(), "invalid value for boolean") + }) + + t.Run("Parse with numeric boolean values", func(t *testing.T) { + t.Setenv("YES", "1") + t.Setenv("SKIP_VERIFY", "0") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Yes) + assert.True(t, *config.Yes, "1 should be parsed as true") + require.NotNil(t, config.SkipVerify) + assert.False(t, *config.SkipVerify, "0 should be parsed as false") + }) + + t.Run("Parse boolean fields with prefix", func(t *testing.T) { + t.Setenv("APP_YES", "true") + t.Setenv("APP_SKIP_VERIFY", "false") + + parser := NewConfigENVParser("APP") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Yes) + assert.True(t, *config.Yes) + require.NotNil(t, config.SkipVerify) + assert.False(t, *config.SkipVerify) + }) +} + +func TestConfigENVParser_Parse_StringFields(t *testing.T) { + t.Run("Parse with METHOD", func(t *testing.T) { + t.Setenv("METHOD", "POST") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Method) + assert.Equal(t, "POST", *config.Method) + }) + + t.Run("Parse with empty METHOD", func(t *testing.T) { + t.Setenv("METHOD", "") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Nil(t, config.Method) + }) + + t.Run("Parse METHOD with prefix", func(t *testing.T) { + t.Setenv("TEST_METHOD", "DELETE") + + parser := NewConfigENVParser("TEST") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Method) + assert.Equal(t, "DELETE", *config.Method) + }) +} + +func TestConfigENVParser_Parse_URL(t *testing.T) { + t.Run("Parse with valid URL", func(t *testing.T) { + t.Setenv("URL", "https://api.example.com/v1/endpoint") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.URL) + assert.Equal(t, "https://api.example.com/v1/endpoint", config.URL.String()) + }) + + t.Run("Parse with invalid URL", func(t *testing.T) { + t.Setenv("URL", "://invalid-url") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + assert.Len(t, fieldErr.Errors, 1) + assert.Equal(t, "URL", fieldErr.Errors[0].Field) + assert.Equal(t, "://invalid-url", fieldErr.Errors[0].Value) + }) + + t.Run("Parse URL with query parameters", func(t *testing.T) { + t.Setenv("URL", "https://api.example.com?param1=value1¶m2=value2") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.URL) + + parsedURL, _ := url.Parse("https://api.example.com?param1=value1¶m2=value2") + assert.Equal(t, parsedURL, config.URL) + }) + + t.Run("Parse URL with prefix", func(t *testing.T) { + t.Setenv("MY_APP_URL", "https://example.com") + + parser := NewConfigENVParser("MY_APP") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.URL) + assert.Equal(t, "https://example.com", config.URL.String()) + }) +} + +func TestConfigENVParser_Parse_UintFields(t *testing.T) { + t.Run("Parse with DODOS", func(t *testing.T) { + t.Setenv("DODOS", "10") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.DodosCount) + assert.Equal(t, uint(10), *config.DodosCount) + }) + + t.Run("Parse with invalid DODOS value", func(t *testing.T) { + t.Setenv("DODOS", "-5") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + assert.Len(t, fieldErr.Errors, 1) + assert.Equal(t, "DODOS", fieldErr.Errors[0].Field) + assert.Equal(t, "-5", fieldErr.Errors[0].Value) + assert.Contains(t, fieldErr.Errors[0].Err.Error(), "invalid value for unsigned integer") + }) + + t.Run("Parse with REQUESTS", func(t *testing.T) { + t.Setenv("REQUESTS", "1000") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.RequestCount) + assert.Equal(t, uint(1000), *config.RequestCount) + }) + + t.Run("Parse with invalid REQUESTS value", func(t *testing.T) { + t.Setenv("REQUESTS", "not_a_number") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + assert.Len(t, fieldErr.Errors, 1) + assert.Equal(t, "REQUESTS", fieldErr.Errors[0].Field) + assert.Equal(t, "not_a_number", fieldErr.Errors[0].Value) + assert.Contains(t, fieldErr.Errors[0].Err.Error(), "invalid value for unsigned integer") + }) + + t.Run("Parse with zero values", func(t *testing.T) { + t.Setenv("DODOS", "0") + t.Setenv("REQUESTS", "0") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.DodosCount) + assert.Equal(t, uint(0), *config.DodosCount) + require.NotNil(t, config.RequestCount) + assert.Equal(t, uint(0), *config.RequestCount) + }) + + t.Run("Parse uint fields with prefix", func(t *testing.T) { + t.Setenv("TEST_DODOS", "5") + t.Setenv("TEST_REQUESTS", "500") + + parser := NewConfigENVParser("TEST") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.DodosCount) + assert.Equal(t, uint(5), *config.DodosCount) + require.NotNil(t, config.RequestCount) + assert.Equal(t, uint(500), *config.RequestCount) + }) +} + +func TestConfigENVParser_Parse_DurationFields(t *testing.T) { + t.Run("Parse with DURATION", func(t *testing.T) { + t.Setenv("DURATION", "5m") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Duration) + assert.Equal(t, 5*time.Minute, *config.Duration) + }) + + t.Run("Parse with complex DURATION", func(t *testing.T) { + t.Setenv("DURATION", "1h30m45s") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Duration) + expected := time.Hour + 30*time.Minute + 45*time.Second + assert.Equal(t, expected, *config.Duration) + }) + + t.Run("Parse with invalid DURATION", func(t *testing.T) { + t.Setenv("DURATION", "invalid") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + assert.Len(t, fieldErr.Errors, 1) + assert.Equal(t, "DURATION", fieldErr.Errors[0].Field) + assert.Equal(t, "invalid", fieldErr.Errors[0].Value) + assert.Contains(t, fieldErr.Errors[0].Err.Error(), "invalid value duration") + }) + + t.Run("Parse with TIMEOUT", func(t *testing.T) { + t.Setenv("TIMEOUT", "30s") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Timeout) + assert.Equal(t, 30*time.Second, *config.Timeout) + }) + + t.Run("Parse with invalid TIMEOUT", func(t *testing.T) { + t.Setenv("TIMEOUT", "30") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + assert.Len(t, fieldErr.Errors, 1) + assert.Equal(t, "TIMEOUT", fieldErr.Errors[0].Field) + assert.Equal(t, "30", fieldErr.Errors[0].Value) + assert.Contains(t, fieldErr.Errors[0].Err.Error(), "invalid value duration") + }) + + t.Run("Parse with zero durations", func(t *testing.T) { + t.Setenv("DURATION", "0s") + t.Setenv("TIMEOUT", "0s") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Duration) + assert.Equal(t, time.Duration(0), *config.Duration) + require.NotNil(t, config.Timeout) + assert.Equal(t, time.Duration(0), *config.Timeout) + }) + + t.Run("Parse duration fields with prefix", func(t *testing.T) { + t.Setenv("APP_DURATION", "10m") + t.Setenv("APP_TIMEOUT", "5s") + + parser := NewConfigENVParser("APP") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Duration) + assert.Equal(t, 10*time.Minute, *config.Duration) + require.NotNil(t, config.Timeout) + assert.Equal(t, 5*time.Second, *config.Timeout) + }) +} + +func TestConfigENVParser_Parse_CollectionFields(t *testing.T) { + t.Run("Parse with PARAM", func(t *testing.T) { + t.Setenv("PARAM", "key1=value1") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Params, 1) + assert.Equal(t, "key1", config.Params[0].Key) + assert.Equal(t, []string{"value1"}, config.Params[0].Value) + }) + + t.Run("Parse with HEADER", func(t *testing.T) { + t.Setenv("HEADER", "Content-Type: application/json") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Headers, 1) + assert.Equal(t, "Content-Type", config.Headers[0].Key) + assert.Equal(t, []string{"application/json"}, config.Headers[0].Value) + }) + + t.Run("Parse with COOKIE", func(t *testing.T) { + t.Setenv("COOKIE", "session=abc123") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Cookies, 1) + assert.Equal(t, "session", config.Cookies[0].Key) + assert.Equal(t, []string{"abc123"}, config.Cookies[0].Value) + }) + + t.Run("Parse with BODY", func(t *testing.T) { + t.Setenv("BODY", `{"data": "test"}`) + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Bodies, 1) + assert.Equal(t, types.Body(`{"data": "test"}`), config.Bodies[0]) //nolint:testifylint + }) + + t.Run("Parse collection fields with prefix", func(t *testing.T) { + t.Setenv("APP_PARAM", "api_key=secret") + t.Setenv("APP_HEADER", "Authorization: Bearer token") + t.Setenv("APP_COOKIE", "user=john") + t.Setenv("APP_BODY", "request body") + + parser := NewConfigENVParser("APP") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Params, 1) + assert.Equal(t, "api_key", config.Params[0].Key) + assert.Len(t, config.Headers, 1) + assert.Equal(t, "Authorization", config.Headers[0].Key) + assert.Len(t, config.Cookies, 1) + assert.Equal(t, "user", config.Cookies[0].Key) + assert.Len(t, config.Bodies, 1) + assert.Equal(t, types.Body("request body"), config.Bodies[0]) + }) +} + +func TestConfigENVParser_Parse_Proxy(t *testing.T) { + t.Run("Parse with valid PROXY", func(t *testing.T) { + t.Setenv("PROXY", "http://proxy.example.com:8080") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Proxies, 1) + assert.Equal(t, "http://proxy.example.com:8080", config.Proxies[0].String()) + }) + + t.Run("Parse with invalid PROXY", func(t *testing.T) { + t.Setenv("PROXY", "://invalid-proxy") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + assert.Len(t, fieldErr.Errors, 1) + assert.Equal(t, "PROXY", fieldErr.Errors[0].Field) + assert.Equal(t, "://invalid-proxy", fieldErr.Errors[0].Value) + }) + + t.Run("Parse PROXY with SOCKS5", func(t *testing.T) { + t.Setenv("PROXY", "socks5://127.0.0.1:1080") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Proxies, 1) + assert.Equal(t, "socks5://127.0.0.1:1080", config.Proxies[0].String()) + }) + + t.Run("Parse PROXY with prefix", func(t *testing.T) { + t.Setenv("TEST_PROXY", "http://proxy.test.com:3128") + + parser := NewConfigENVParser("TEST") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + assert.Len(t, config.Proxies, 1) + assert.Equal(t, "http://proxy.test.com:3128", config.Proxies[0].String()) + }) +} + +func TestConfigENVParser_Parse_AllFields(t *testing.T) { + t.Run("Parse with all environment variables set", func(t *testing.T) { + // Set all environment variables + t.Setenv("CONFIG_FILE", "/path/to/config.yaml") + t.Setenv("YES", "true") + t.Setenv("SKIP_VERIFY", "true") + t.Setenv("METHOD", "POST") + t.Setenv("URL", "https://api.example.com/test") + t.Setenv("DODOS", "10") + t.Setenv("REQUESTS", "1000") + t.Setenv("DURATION", "5m") + t.Setenv("TIMEOUT", "30s") + t.Setenv("PARAM", "key=value") + t.Setenv("HEADER", "Content-Type: application/json") + t.Setenv("COOKIE", "session=token") + t.Setenv("BODY", `{"test": "data"}`) + t.Setenv("PROXY", "http://proxy.example.com:8080") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + + // Verify all fields + assert.Len(t, config.Files, 1) + assert.Equal(t, "/path/to/config.yaml", config.Files[0].String()) + + require.NotNil(t, config.Yes) + assert.True(t, *config.Yes) + + require.NotNil(t, config.SkipVerify) + assert.True(t, *config.SkipVerify) + + require.NotNil(t, config.Method) + assert.Equal(t, "POST", *config.Method) + + require.NotNil(t, config.URL) + assert.Equal(t, "https://api.example.com/test", config.URL.String()) + + require.NotNil(t, config.DodosCount) + assert.Equal(t, uint(10), *config.DodosCount) + + require.NotNil(t, config.RequestCount) + assert.Equal(t, uint(1000), *config.RequestCount) + + require.NotNil(t, config.Duration) + assert.Equal(t, 5*time.Minute, *config.Duration) + + require.NotNil(t, config.Timeout) + assert.Equal(t, 30*time.Second, *config.Timeout) + + assert.Len(t, config.Params, 1) + assert.Equal(t, "key", config.Params[0].Key) + + assert.Len(t, config.Headers, 1) + assert.Equal(t, "Content-Type", config.Headers[0].Key) + + assert.Len(t, config.Cookies, 1) + assert.Equal(t, "session", config.Cookies[0].Key) + + assert.Len(t, config.Bodies, 1) + assert.Equal(t, types.Body(`{"test": "data"}`), config.Bodies[0]) //nolint:testifylint + + assert.Len(t, config.Proxies, 1) + assert.Equal(t, "http://proxy.example.com:8080", config.Proxies[0].String()) + }) + + t.Run("Parse with all fields using prefix", func(t *testing.T) { + // Set all environment variables with prefix + t.Setenv("MY_APP_CONFIG_FILE", "/app/config.yml") + t.Setenv("MY_APP_YES", "false") + t.Setenv("MY_APP_SKIP_VERIFY", "false") + t.Setenv("MY_APP_METHOD", "GET") + t.Setenv("MY_APP_URL", "https://example.com") + t.Setenv("MY_APP_DODOS", "5") + t.Setenv("MY_APP_REQUESTS", "500") + t.Setenv("MY_APP_DURATION", "10m") + t.Setenv("MY_APP_TIMEOUT", "10s") + t.Setenv("MY_APP_PARAM", "param1=value1") + t.Setenv("MY_APP_HEADER", "Accept: application/json") + t.Setenv("MY_APP_COOKIE", "auth=token123") + t.Setenv("MY_APP_BODY", "test body") + t.Setenv("MY_APP_PROXY", "http://127.0.0.1:8080") + + parser := NewConfigENVParser("MY_APP") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + + // Verify all fields + assert.Len(t, config.Files, 1) + assert.Equal(t, "/app/config.yml", config.Files[0].String()) + + require.NotNil(t, config.Yes) + assert.False(t, *config.Yes) + + require.NotNil(t, config.SkipVerify) + assert.False(t, *config.SkipVerify) + + require.NotNil(t, config.Method) + assert.Equal(t, "GET", *config.Method) + + require.NotNil(t, config.URL) + assert.Equal(t, "https://example.com", config.URL.String()) + + require.NotNil(t, config.DodosCount) + assert.Equal(t, uint(5), *config.DodosCount) + + require.NotNil(t, config.RequestCount) + assert.Equal(t, uint(500), *config.RequestCount) + + require.NotNil(t, config.Duration) + assert.Equal(t, 10*time.Minute, *config.Duration) + + require.NotNil(t, config.Timeout) + assert.Equal(t, 10*time.Second, *config.Timeout) + + assert.Len(t, config.Params, 1) + assert.Len(t, config.Headers, 1) + assert.Len(t, config.Cookies, 1) + assert.Len(t, config.Bodies, 1) + assert.Len(t, config.Proxies, 1) + }) +} + +func TestConfigENVParser_Parse_MultipleErrors(t *testing.T) { + t.Run("Parse with multiple field errors", func(t *testing.T) { + // Set multiple invalid values + t.Setenv("CONFIG_FILE", "/path/to/config") // Missing extension + t.Setenv("YES", "invalid_bool") + t.Setenv("SKIP_VERIFY", "not_a_bool") + t.Setenv("URL", "://invalid-url") + t.Setenv("DODOS", "-10") + t.Setenv("REQUESTS", "not_a_number") + t.Setenv("DURATION", "invalid_duration") + t.Setenv("TIMEOUT", "30") // Missing time unit + t.Setenv("PROXY", "://invalid-proxy") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + assert.Nil(t, config) + var fieldErr types.FieldParseErrors + require.ErrorAs(t, err, &fieldErr) + + // Should have 9 errors + assert.Len(t, fieldErr.Errors, 9) + + // Check that all expected fields have errors + errorFields := make(map[string]bool) + for _, e := range fieldErr.Errors { + errorFields[e.Field] = true + } + + assert.True(t, errorFields["CONFIG_FILE"]) + assert.True(t, errorFields["YES"]) + assert.True(t, errorFields["SKIP_VERIFY"]) + assert.True(t, errorFields["URL"]) + assert.True(t, errorFields["DODOS"]) + assert.True(t, errorFields["REQUESTS"]) + assert.True(t, errorFields["DURATION"]) + assert.True(t, errorFields["TIMEOUT"]) + assert.True(t, errorFields["PROXY"]) + }) +} + +func TestConfigENVParser_Parse_EmptyValues(t *testing.T) { + t.Run("Parse with empty string environment variables", func(t *testing.T) { + // Set empty values + t.Setenv("CONFIG_FILE", "") + t.Setenv("YES", "") + t.Setenv("SKIP_VERIFY", "") + t.Setenv("METHOD", "") + t.Setenv("URL", "") + t.Setenv("DODOS", "") + t.Setenv("REQUESTS", "") + t.Setenv("DURATION", "") + t.Setenv("TIMEOUT", "") + t.Setenv("PARAM", "") + t.Setenv("HEADER", "") + t.Setenv("COOKIE", "") + t.Setenv("BODY", "") + t.Setenv("PROXY", "") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + // Empty values should not cause errors, fields should just be nil/empty + require.NoError(t, err) + require.NotNil(t, config) + + assert.Empty(t, config.Files) + assert.Nil(t, config.Yes) + assert.Nil(t, config.SkipVerify) + assert.Nil(t, config.Method) + assert.Nil(t, config.URL) + assert.Nil(t, config.DodosCount) + assert.Nil(t, config.RequestCount) + assert.Nil(t, config.Duration) + assert.Nil(t, config.Timeout) + assert.Empty(t, config.Params) + assert.Empty(t, config.Headers) + assert.Empty(t, config.Cookies) + assert.Empty(t, config.Bodies) + assert.Empty(t, config.Proxies) + }) +} + +func TestConfigENVParser_Parse_NoEnvironmentVariables(t *testing.T) { + t.Run("Parse with no environment variables set", func(t *testing.T) { + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + + // All fields should be nil or empty + assert.Empty(t, config.Files) + assert.Nil(t, config.Yes) + assert.Nil(t, config.SkipVerify) + assert.Nil(t, config.Method) + assert.Nil(t, config.URL) + assert.Nil(t, config.DodosCount) + assert.Nil(t, config.RequestCount) + assert.Nil(t, config.Duration) + assert.Nil(t, config.Timeout) + assert.Empty(t, config.Params) + assert.Empty(t, config.Headers) + assert.Empty(t, config.Cookies) + assert.Empty(t, config.Bodies) + assert.Empty(t, config.Proxies) + }) + + t.Run("Parse with prefix but no matching environment variables", func(t *testing.T) { + parser := NewConfigENVParser("NONEXISTENT_PREFIX") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + + // All fields should be nil or empty + assert.Empty(t, config.Files) + assert.Nil(t, config.Yes) + assert.Nil(t, config.SkipVerify) + assert.Nil(t, config.Method) + assert.Nil(t, config.URL) + assert.Nil(t, config.DodosCount) + assert.Nil(t, config.RequestCount) + assert.Nil(t, config.Duration) + assert.Nil(t, config.Timeout) + assert.Empty(t, config.Params) + assert.Empty(t, config.Headers) + assert.Empty(t, config.Cookies) + assert.Empty(t, config.Bodies) + assert.Empty(t, config.Proxies) + }) +} + +func TestConfigENVParser_InterfaceConformance(t *testing.T) { + t.Run("ConfigENVParser implements IParser interface", func(t *testing.T) { + parser := ConfigENVParser{envPrefix: "TEST"} + + // This test verifies that ConfigENVParser implements the IParser interface + var _ IParser = parser + + // Also test that the pointer type implements the interface + var _ IParser = &parser + }) +} + +func TestConfigENVParser_EdgeCases(t *testing.T) { + t.Run("Parse with very long prefix", func(t *testing.T) { + longPrefix := "THIS_IS_A_VERY_LONG_PREFIX_FOR_TESTING_PURPOSES" + envName := longPrefix + "_METHOD" + t.Setenv(envName, "GET") + + parser := NewConfigENVParser(longPrefix) + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Method) + assert.Equal(t, "GET", *config.Method) + }) + + t.Run("Parse with special characters in values", func(t *testing.T) { + t.Setenv("URL", "https://example.com/path?key=value&special=%20%21%40%23") + t.Setenv("HEADER", "X-Special-Header: value with spaces and special!@#$%^&*()") + t.Setenv("BODY", `{"special": "characters!@#$%^&*()_+-=[]{}|;':\",./<>?"}`) + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + + require.NotNil(t, config.URL) + assert.Contains(t, config.URL.String(), "special=%20%21%40%23") + + assert.Len(t, config.Headers, 1) + assert.Contains(t, config.Headers[0].Value[0], "special!@#$%^&*()") + + assert.Len(t, config.Bodies, 1) + assert.Contains(t, string(config.Bodies[0]), "special") + }) + + t.Run("Parse with maximum uint values", func(t *testing.T) { + t.Setenv("DODOS", "4294967295") // Max uint32 + t.Setenv("REQUESTS", "4294967295") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.DodosCount) + assert.Equal(t, uint(4294967295), *config.DodosCount) + require.NotNil(t, config.RequestCount) + assert.Equal(t, uint(4294967295), *config.RequestCount) + }) + + t.Run("Parse with very large duration values", func(t *testing.T) { + t.Setenv("DURATION", "9999h59m59s") + t.Setenv("TIMEOUT", "999999s") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + require.NotNil(t, config.Duration) + require.NotNil(t, config.Timeout) + + // Just verify they were parsed without checking exact values + assert.Greater(t, *config.Duration, time.Hour) + assert.Greater(t, *config.Timeout, time.Second) + }) +} + +func TestConfigENVParser_RealWorldScenarios(t *testing.T) { + t.Run("Parse typical production environment configuration", func(t *testing.T) { + // Simulate a production environment configuration + t.Setenv("PROD_CONFIG_FILE", "https://config.example.com/prod.yaml") + t.Setenv("PROD_YES", "false") + t.Setenv("PROD_SKIP_VERIFY", "false") + t.Setenv("PROD_METHOD", "POST") + t.Setenv("PROD_URL", "https://api.production.com/v2/endpoint") + t.Setenv("PROD_DODOS", "100") + t.Setenv("PROD_REQUESTS", "10000") + t.Setenv("PROD_DURATION", "1h") + t.Setenv("PROD_TIMEOUT", "60s") + t.Setenv("PROD_HEADER", "Authorization: Bearer production-token-12345") + t.Setenv("PROD_PROXY", "http://corporate-proxy.example.com:8080") + + parser := NewConfigENVParser("PROD") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + + // Verify production settings + assert.Len(t, config.Files, 1) + assert.Equal(t, types.ConfigFileLocationRemote, config.Files[0].LocationType()) + + require.NotNil(t, config.Yes) + assert.False(t, *config.Yes, "Production should not auto-confirm") + + require.NotNil(t, config.SkipVerify) + assert.False(t, *config.SkipVerify, "Production should verify certificates") + + require.NotNil(t, config.DodosCount) + assert.Equal(t, uint(100), *config.DodosCount) + + require.NotNil(t, config.RequestCount) + assert.Equal(t, uint(10000), *config.RequestCount) + + require.NotNil(t, config.Duration) + assert.Equal(t, time.Hour, *config.Duration) + + require.NotNil(t, config.Timeout) + assert.Equal(t, 60*time.Second, *config.Timeout) + }) + + t.Run("Parse development environment configuration", func(t *testing.T) { + // Simulate a development environment configuration + t.Setenv("DEV_CONFIG_FILE", "/local/config/dev.yaml") + t.Setenv("DEV_YES", "true") + t.Setenv("DEV_SKIP_VERIFY", "true") + t.Setenv("DEV_METHOD", "GET") + t.Setenv("DEV_URL", "http://localhost:8080/test") + t.Setenv("DEV_DODOS", "1") + t.Setenv("DEV_REQUESTS", "10") + t.Setenv("DEV_DURATION", "30s") + t.Setenv("DEV_TIMEOUT", "5s") + t.Setenv("DEV_HEADER", "X-Debug: true") + + parser := NewConfigENVParser("DEV") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + + // Verify development settings + assert.Len(t, config.Files, 1) + assert.Equal(t, types.ConfigFileLocationLocal, config.Files[0].LocationType()) + + require.NotNil(t, config.Yes) + assert.True(t, *config.Yes, "Development can auto-confirm") + + require.NotNil(t, config.SkipVerify) + assert.True(t, *config.SkipVerify, "Development can skip certificate verification") + + require.NotNil(t, config.DodosCount) + assert.Equal(t, uint(1), *config.DodosCount, "Development uses fewer workers") + + require.NotNil(t, config.RequestCount) + assert.Equal(t, uint(10), *config.RequestCount, "Development uses fewer requests") + + require.NotNil(t, config.Duration) + assert.Equal(t, 30*time.Second, *config.Duration) + + require.NotNil(t, config.Timeout) + assert.Equal(t, 5*time.Second, *config.Timeout) + + assert.Len(t, config.Headers, 1) + assert.Equal(t, "X-Debug", config.Headers[0].Key) + }) +} + +func TestConfigENVParser_PartialConfiguration(t *testing.T) { + t.Run("Parse with only critical fields set", func(t *testing.T) { + t.Setenv("URL", "https://api.example.com") + t.Setenv("METHOD", "POST") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + + // Only URL and METHOD should be set + require.NotNil(t, config.URL) + assert.Equal(t, "https://api.example.com", config.URL.String()) + + require.NotNil(t, config.Method) + assert.Equal(t, "POST", *config.Method) + + // Everything else should be nil or empty + assert.Empty(t, config.Files) + assert.Nil(t, config.Yes) + assert.Nil(t, config.SkipVerify) + assert.Nil(t, config.DodosCount) + assert.Nil(t, config.RequestCount) + assert.Nil(t, config.Duration) + assert.Nil(t, config.Timeout) + assert.Empty(t, config.Params) + assert.Empty(t, config.Headers) + assert.Empty(t, config.Cookies) + assert.Empty(t, config.Bodies) + assert.Empty(t, config.Proxies) + }) + + t.Run("Parse with only optional fields set", func(t *testing.T) { + t.Setenv("HEADER", "User-Agent: CustomAgent/1.0") + t.Setenv("COOKIE", "preferences=dark-mode") + t.Setenv("PARAM", "debug=true") + + parser := NewConfigENVParser("") + config, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, config) + + // Only collection fields should have values + assert.Len(t, config.Headers, 1) + assert.Equal(t, "User-Agent", config.Headers[0].Key) + + assert.Len(t, config.Cookies, 1) + assert.Equal(t, "preferences", config.Cookies[0].Key) + + assert.Len(t, config.Params, 1) + assert.Equal(t, "debug", config.Params[0].Key) + + // Core fields should be nil + assert.Nil(t, config.URL) + assert.Nil(t, config.Method) + assert.Nil(t, config.DodosCount) + assert.Nil(t, config.RequestCount) + }) +} + +func TestConfigENVParser_MergeScenarios(t *testing.T) { + t.Run("Parse configuration suitable for merging", func(t *testing.T) { + // Set only specific fields that would override defaults + t.Setenv("METHOD", "PUT") + t.Setenv("TIMEOUT", "45s") + t.Setenv("HEADER", "X-Custom: value") + + parser := NewConfigENVParser("") + envConfig, err := parser.Parse() + + require.NoError(t, err) + require.NotNil(t, envConfig) + + // Create a default config + defaultConfig := &config.Config{ + Method: utils.ToPtr("GET"), + Timeout: utils.ToPtr(30 * time.Second), + DodosCount: utils.ToPtr(uint(1)), + Headers: types.Headers{{Key: "User-Agent", Value: []string{"DefaultAgent/1.0"}}}, + } + + // Merge the parsed config into defaults + defaultConfig.Merge(envConfig) + + // Verify merged values + assert.Equal(t, "PUT", *defaultConfig.Method, "Method should be overridden") + assert.Equal(t, 45*time.Second, *defaultConfig.Timeout, "Timeout should be overridden") + assert.Equal(t, uint(1), *defaultConfig.DodosCount, "DodosCount should remain from default") + + // Headers should contain the new header (merge behavior depends on implementation) + assert.Len(t, defaultConfig.Headers, 1) + assert.Equal(t, "X-Custom", defaultConfig.Headers[0].Key) + }) +}