mirror of
https://github.com/aykhans/dodo.git
synced 2025-09-05 10:43:37 +00:00
Compare commits
5 Commits
29b85d5b83
...
v1.0.0
Author | SHA1 | Date | |
---|---|---|---|
1eb969480b | |||
7e89fa174b | |||
81383d1ea7 | |||
fd7c4c6454 | |||
438e655311 |
@@ -59,7 +59,6 @@ linters:
|
|||||||
- inamedparam
|
- inamedparam
|
||||||
- interfacebloat
|
- interfacebloat
|
||||||
- intrange
|
- intrange
|
||||||
- ireturn
|
|
||||||
- loggercheck
|
- loggercheck
|
||||||
- makezero
|
- makezero
|
||||||
- mirror
|
- mirror
|
||||||
@@ -110,6 +109,7 @@ linters:
|
|||||||
- perfsprint
|
- perfsprint
|
||||||
- errcheck
|
- errcheck
|
||||||
- gosec
|
- gosec
|
||||||
|
- gocyclo
|
||||||
|
|
||||||
- path: _test\.go$
|
- path: _test\.go$
|
||||||
linters:
|
linters:
|
||||||
|
@@ -4,16 +4,27 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"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/types"
|
||||||
"github.com/aykhans/dodo/pkg/utils"
|
"github.com/aykhans/dodo/pkg/utils"
|
||||||
"github.com/jedib0t/go-pretty/v6/text"
|
"github.com/jedib0t/go-pretty/v6/text"
|
||||||
|
"github.com/k0kubun/pp/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cliParser := config.NewConfigCLIParser(os.Args)
|
envParser := parser.NewConfigENVParser("DODO")
|
||||||
cfg, err := cliParser.Parse()
|
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.HandleErrorOrDie(err,
|
||||||
utils.OnSentinelError(types.ErrCLINoArgs, func(err error) error {
|
utils.OnSentinelError(types.ErrCLINoArgs, func(err error) error {
|
||||||
cliParser.PrintHelp()
|
cliParser.PrintHelp()
|
||||||
@@ -35,11 +46,16 @@ func main() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
fmt.Println(cfg)
|
envConfig.Merge(cliConf)
|
||||||
|
pp.Println(cliConf) //nolint
|
||||||
|
pp.Println(envConfig) //nolint
|
||||||
}
|
}
|
||||||
|
|
||||||
func printValidationErrors(parserName string, errors ...types.FieldParseError) {
|
func printValidationErrors(parserName string, errors ...types.FieldParseError) {
|
||||||
for _, fieldErr := range errors {
|
for _, fieldErr := range errors {
|
||||||
utils.PrintErr(text.FgRed, "[%s] Field '%s': %v", parserName, fieldErr.Field, fieldErr.Err)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
go.mod
5
go.mod
@@ -4,15 +4,18 @@ go 1.25
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jedib0t/go-pretty/v6 v6.6.8
|
github.com/jedib0t/go-pretty/v6 v6.6.8
|
||||||
|
github.com/k0kubun/pp/v3 v3.5.0
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
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/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
golang.org/x/sys v0.30.0 // 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
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
11
go.sum
11
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/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 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
|
||||||
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
|
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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
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/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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
@@ -29,6 +29,7 @@ var Defaults = struct {
|
|||||||
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
|
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Files []types.ConfigFile
|
||||||
Method *string
|
Method *string
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
Timeout *time.Duration
|
Timeout *time.Duration
|
||||||
@@ -48,7 +49,8 @@ func NewConfig() *Config {
|
|||||||
return &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 {
|
if newConfig.Method != nil {
|
||||||
config.Method = newConfig.Method
|
config.Method = newConfig.Method
|
||||||
}
|
}
|
||||||
@@ -74,19 +76,19 @@ func (config *Config) MergeConfig(newConfig *Config) {
|
|||||||
config.SkipVerify = newConfig.SkipVerify
|
config.SkipVerify = newConfig.SkipVerify
|
||||||
}
|
}
|
||||||
if len(newConfig.Params) != 0 {
|
if len(newConfig.Params) != 0 {
|
||||||
config.Params = newConfig.Params
|
config.Params.Append(newConfig.Params...)
|
||||||
}
|
}
|
||||||
if len(newConfig.Headers) != 0 {
|
if len(newConfig.Headers) != 0 {
|
||||||
config.Headers = newConfig.Headers
|
config.Headers.Append(newConfig.Headers...)
|
||||||
}
|
}
|
||||||
if len(newConfig.Cookies) != 0 {
|
if len(newConfig.Cookies) != 0 {
|
||||||
config.Cookies = newConfig.Cookies
|
config.Cookies.Append(newConfig.Cookies...)
|
||||||
}
|
}
|
||||||
if len(newConfig.Bodies) != 0 {
|
if len(newConfig.Bodies) != 0 {
|
||||||
config.Bodies = newConfig.Bodies
|
config.Bodies.Append(newConfig.Bodies...)
|
||||||
}
|
}
|
||||||
if len(newConfig.Proxies) != 0 {
|
if len(newConfig.Proxies) != 0 {
|
||||||
config.Proxies = newConfig.Proxies
|
config.Proxies.Append(newConfig.Proxies...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,6 +23,7 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
newDuration := 2 * time.Minute
|
newDuration := 2 * time.Minute
|
||||||
|
|
||||||
config := &Config{
|
config := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
Method: utils.ToPtr("GET"),
|
Method: utils.ToPtr("GET"),
|
||||||
URL: originalURL,
|
URL: originalURL,
|
||||||
Timeout: &originalTimeout,
|
Timeout: &originalTimeout,
|
||||||
@@ -39,6 +40,7 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newConfig := &Config{
|
newConfig := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
Method: utils.ToPtr("POST"),
|
Method: utils.ToPtr("POST"),
|
||||||
URL: newURL,
|
URL: newURL,
|
||||||
Timeout: &newTimeout,
|
Timeout: &newTimeout,
|
||||||
@@ -54,7 +56,7 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
Proxies: types.Proxies{},
|
Proxies: types.Proxies{},
|
||||||
}
|
}
|
||||||
|
|
||||||
config.MergeConfig(newConfig)
|
config.Merge(newConfig)
|
||||||
|
|
||||||
assert.Equal(t, "POST", *config.Method)
|
assert.Equal(t, "POST", *config.Method)
|
||||||
assert.Equal(t, newURL, config.URL)
|
assert.Equal(t, newURL, config.URL)
|
||||||
@@ -64,10 +66,10 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
assert.Equal(t, newDuration, *config.Duration)
|
assert.Equal(t, newDuration, *config.Duration)
|
||||||
assert.True(t, *config.Yes)
|
assert.True(t, *config.Yes)
|
||||||
assert.True(t, *config.SkipVerify)
|
assert.True(t, *config.SkipVerify)
|
||||||
assert.Equal(t, types.Params{{Key: "new", Value: []string{"value"}}}, config.Params)
|
assert.Equal(t, types.Params{{Key: "old", Value: []string{"value"}}, {Key: "new", Value: []string{"value"}}}, config.Params)
|
||||||
assert.Equal(t, types.Headers{{Key: "New-Header", Value: []string{"new"}}}, config.Headers)
|
assert.Equal(t, types.Headers{{Key: "Old-Header", Value: []string{"old"}}, {Key: "New-Header", Value: []string{"new"}}}, config.Headers)
|
||||||
assert.Equal(t, types.Cookies{{Key: "newCookie", Value: []string{"newValue"}}}, config.Cookies)
|
assert.Equal(t, types.Cookies{{Key: "oldCookie", Value: []string{"oldValue"}}, {Key: "newCookie", Value: []string{"newValue"}}}, config.Cookies)
|
||||||
assert.Equal(t, types.Bodies{types.Body("new body")}, config.Bodies)
|
assert.Equal(t, types.Bodies{types.Body("old body"), types.Body("new body")}, config.Bodies)
|
||||||
assert.Empty(t, config.Proxies)
|
assert.Empty(t, config.Proxies)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -76,6 +78,7 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
originalTimeout := 5 * time.Second
|
originalTimeout := 5 * time.Second
|
||||||
|
|
||||||
config := &Config{
|
config := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
Method: utils.ToPtr("GET"),
|
Method: utils.ToPtr("GET"),
|
||||||
URL: originalURL,
|
URL: originalURL,
|
||||||
Timeout: &originalTimeout,
|
Timeout: &originalTimeout,
|
||||||
@@ -85,11 +88,12 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
|
|
||||||
newURL, _ := url.Parse("https://new.example.com")
|
newURL, _ := url.Parse("https://new.example.com")
|
||||||
newConfig := &Config{
|
newConfig := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
URL: newURL,
|
URL: newURL,
|
||||||
DodosCount: utils.ToPtr(uint(10)),
|
DodosCount: utils.ToPtr(uint(10)),
|
||||||
}
|
}
|
||||||
|
|
||||||
config.MergeConfig(newConfig)
|
config.Merge(newConfig)
|
||||||
|
|
||||||
assert.Equal(t, "GET", *config.Method, "Method should remain unchanged")
|
assert.Equal(t, "GET", *config.Method, "Method should remain unchanged")
|
||||||
assert.Equal(t, newURL, config.URL, "URL should be updated")
|
assert.Equal(t, newURL, config.URL, "URL should be updated")
|
||||||
@@ -103,6 +107,7 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
originalTimeout := 5 * time.Second
|
originalTimeout := 5 * time.Second
|
||||||
|
|
||||||
config := &Config{
|
config := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
Method: utils.ToPtr("GET"),
|
Method: utils.ToPtr("GET"),
|
||||||
URL: originalURL,
|
URL: originalURL,
|
||||||
Timeout: &originalTimeout,
|
Timeout: &originalTimeout,
|
||||||
@@ -112,6 +117,7 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newConfig := &Config{
|
newConfig := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
Method: nil,
|
Method: nil,
|
||||||
URL: nil,
|
URL: nil,
|
||||||
Timeout: nil,
|
Timeout: nil,
|
||||||
@@ -121,7 +127,7 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
originalConfigCopy := *config
|
originalConfigCopy := *config
|
||||||
config.MergeConfig(newConfig)
|
config.Merge(newConfig)
|
||||||
|
|
||||||
assert.Equal(t, originalConfigCopy.Method, config.Method)
|
assert.Equal(t, originalConfigCopy.Method, config.Method)
|
||||||
assert.Equal(t, originalConfigCopy.URL, config.URL)
|
assert.Equal(t, originalConfigCopy.URL, config.URL)
|
||||||
@@ -132,7 +138,9 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("MergeConfig with empty slices", func(t *testing.T) {
|
t.Run("MergeConfig with empty slices", func(t *testing.T) {
|
||||||
|
configFile, _ := types.ParseConfigFile("original.yml")
|
||||||
config := &Config{
|
config := &Config{
|
||||||
|
Files: []types.ConfigFile{*configFile},
|
||||||
Params: types.Params{{Key: "original", Value: []string{"value"}}},
|
Params: types.Params{{Key: "original", Value: []string{"value"}}},
|
||||||
Headers: types.Headers{{Key: "Original-Header", Value: []string{"original"}}},
|
Headers: types.Headers{{Key: "Original-Header", Value: []string{"original"}}},
|
||||||
Cookies: types.Cookies{{Key: "originalCookie", Value: []string{"originalValue"}}},
|
Cookies: types.Cookies{{Key: "originalCookie", Value: []string{"originalValue"}}},
|
||||||
@@ -141,6 +149,7 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
newConfig := &Config{
|
newConfig := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
Params: types.Params{},
|
Params: types.Params{},
|
||||||
Headers: types.Headers{},
|
Headers: types.Headers{},
|
||||||
Cookies: types.Cookies{},
|
Cookies: types.Cookies{},
|
||||||
@@ -148,8 +157,9 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
Proxies: types.Proxies{},
|
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")
|
assert.Equal(t, types.Params{{Key: "original", Value: []string{"value"}}}, config.Params, "Empty Params should not override")
|
||||||
assert.Equal(t, types.Headers{{Key: "Original-Header", Value: []string{"original"}}}, config.Headers, "Empty Headers should not override")
|
assert.Equal(t, types.Headers{{Key: "Original-Header", Value: []string{"original"}}}, config.Headers, "Empty Headers should not override")
|
||||||
assert.Equal(t, types.Cookies{{Key: "originalCookie", Value: []string{"originalValue"}}}, config.Cookies, "Empty Cookies should not override")
|
assert.Equal(t, types.Cookies{{Key: "originalCookie", Value: []string{"originalValue"}}}, config.Cookies, "Empty Cookies should not override")
|
||||||
@@ -157,6 +167,28 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
assert.Equal(t, types.Proxies{}, config.Proxies, "Empty Proxies should not override")
|
assert.Equal(t, types.Proxies{}, config.Proxies, "Empty Proxies should not override")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("MergeConfig with Files field", func(t *testing.T) {
|
||||||
|
configFile1, _ := types.ParseConfigFile("config1.yml")
|
||||||
|
configFile2, _ := types.ParseConfigFile("config2.yaml")
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Files: []types.ConfigFile{*configFile1},
|
||||||
|
Method: utils.ToPtr("GET"),
|
||||||
|
Headers: types.Headers{{Key: "Original-Header", Value: []string{"original"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := &Config{
|
||||||
|
Files: []types.ConfigFile{*configFile2},
|
||||||
|
Method: utils.ToPtr("POST"),
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
assert.Equal(t, types.Headers{{Key: "Original-Header", Value: []string{"original"}}}, config.Headers, "Headers should remain unchanged")
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("MergeConfig on empty original config", func(t *testing.T) {
|
t.Run("MergeConfig on empty original config", func(t *testing.T) {
|
||||||
config := &Config{}
|
config := &Config{}
|
||||||
|
|
||||||
@@ -165,6 +197,7 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
newDuration := 2 * time.Minute
|
newDuration := 2 * time.Minute
|
||||||
|
|
||||||
newConfig := &Config{
|
newConfig := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
Method: utils.ToPtr("POST"),
|
Method: utils.ToPtr("POST"),
|
||||||
URL: newURL,
|
URL: newURL,
|
||||||
Timeout: &newTimeout,
|
Timeout: &newTimeout,
|
||||||
@@ -180,7 +213,7 @@ func TestMergeConfig(t *testing.T) {
|
|||||||
Proxies: types.Proxies{},
|
Proxies: types.Proxies{},
|
||||||
}
|
}
|
||||||
|
|
||||||
config.MergeConfig(newConfig)
|
config.Merge(newConfig)
|
||||||
|
|
||||||
assert.Equal(t, "POST", *config.Method)
|
assert.Equal(t, "POST", *config.Method)
|
||||||
assert.Equal(t, newURL, config.URL)
|
assert.Equal(t, newURL, config.URL)
|
||||||
@@ -246,6 +279,7 @@ func TestSetDefaults(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("SetDefaults adds User-Agent when missing", func(t *testing.T) {
|
t.Run("SetDefaults adds User-Agent when missing", func(t *testing.T) {
|
||||||
config := &Config{
|
config := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
Headers: types.Headers{{Key: "Content-Type", Value: []string{"application/json"}}},
|
Headers: types.Headers{{Key: "Content-Type", Value: []string{"application/json"}}},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +302,7 @@ func TestSetDefaults(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("SetDefaults with partial config", func(t *testing.T) {
|
t.Run("SetDefaults with partial config", func(t *testing.T) {
|
||||||
config := &Config{
|
config := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
Method: utils.ToPtr("PUT"),
|
Method: utils.ToPtr("PUT"),
|
||||||
Yes: utils.ToPtr(true),
|
Yes: utils.ToPtr(true),
|
||||||
}
|
}
|
||||||
@@ -306,6 +341,7 @@ func TestSetDefaults(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("SetDefaults with empty Headers initializes correctly", func(t *testing.T) {
|
t.Run("SetDefaults with empty Headers initializes correctly", func(t *testing.T) {
|
||||||
config := &Config{
|
config := &Config{
|
||||||
|
Files: []types.ConfigFile{},
|
||||||
Headers: types.Headers{},
|
Headers: types.Headers{},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,3 +352,176 @@ func TestSetDefaults(t *testing.T) {
|
|||||||
assert.Equal(t, Defaults.UserAgent, config.Headers[0].Value[0])
|
assert.Equal(t, Defaults.UserAgent, config.Headers[0].Value[0])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMergeConfig_AppendBehavior(t *testing.T) {
|
||||||
|
t.Run("MergeConfig appends params with same key", func(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Params: types.Params{{Key: "filter", Value: []string{"active"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := &Config{
|
||||||
|
Params: types.Params{{Key: "filter", Value: []string{"verified"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Merge(newConfig)
|
||||||
|
|
||||||
|
assert.Len(t, config.Params, 1)
|
||||||
|
paramValue := config.Params.GetValue("filter")
|
||||||
|
require.NotNil(t, paramValue)
|
||||||
|
assert.Equal(t, []string{"active", "verified"}, *paramValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MergeConfig appends headers with same key", func(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Headers: types.Headers{{Key: "Accept", Value: []string{"text/html"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := &Config{
|
||||||
|
Headers: types.Headers{{Key: "Accept", Value: []string{"application/json"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Merge(newConfig)
|
||||||
|
|
||||||
|
assert.Len(t, config.Headers, 1)
|
||||||
|
headerValue := config.Headers.GetValue("Accept")
|
||||||
|
require.NotNil(t, headerValue)
|
||||||
|
assert.Equal(t, []string{"text/html", "application/json"}, *headerValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MergeConfig appends cookies with same key", func(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Cookies: types.Cookies{{Key: "session", Value: []string{"old_token"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := &Config{
|
||||||
|
Cookies: types.Cookies{{Key: "session", Value: []string{"new_token"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Merge(newConfig)
|
||||||
|
|
||||||
|
assert.Len(t, config.Cookies, 1)
|
||||||
|
cookieValue := config.Cookies.GetValue("session")
|
||||||
|
require.NotNil(t, cookieValue)
|
||||||
|
assert.Equal(t, []string{"old_token", "new_token"}, *cookieValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MergeConfig appends bodies", func(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Bodies: types.Bodies{types.Body("first body")},
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := &Config{
|
||||||
|
Bodies: types.Bodies{types.Body("second body"), types.Body("third body")},
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Merge(newConfig)
|
||||||
|
|
||||||
|
assert.Len(t, config.Bodies, 3)
|
||||||
|
assert.Equal(t, types.Body("first body"), config.Bodies[0])
|
||||||
|
assert.Equal(t, types.Body("second body"), config.Bodies[1])
|
||||||
|
assert.Equal(t, types.Body("third body"), config.Bodies[2])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MergeConfig appends proxies", func(t *testing.T) {
|
||||||
|
proxy1URL, _ := url.Parse("http://proxy1.example.com:8080")
|
||||||
|
proxy2URL, _ := url.Parse("http://proxy2.example.com:8080")
|
||||||
|
proxy3URL, _ := url.Parse("https://proxy3.example.com:443")
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Proxies: types.Proxies{types.Proxy(*proxy1URL)},
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := &Config{
|
||||||
|
Proxies: types.Proxies{types.Proxy(*proxy2URL), types.Proxy(*proxy3URL)},
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Merge(newConfig)
|
||||||
|
|
||||||
|
assert.Len(t, config.Proxies, 3)
|
||||||
|
assert.Equal(t, "http://proxy1.example.com:8080", config.Proxies[0].String())
|
||||||
|
assert.Equal(t, "http://proxy2.example.com:8080", config.Proxies[1].String())
|
||||||
|
assert.Equal(t, "https://proxy3.example.com:443", config.Proxies[2].String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MergeConfig appends mixed content", func(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Params: types.Params{{Key: "limit", Value: []string{"10"}}},
|
||||||
|
Headers: types.Headers{{Key: "Authorization", Value: []string{"Bearer token1"}}},
|
||||||
|
Cookies: types.Cookies{{Key: "theme", Value: []string{"dark"}}},
|
||||||
|
Bodies: types.Bodies{types.Body("original")},
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := &Config{
|
||||||
|
Params: types.Params{{Key: "offset", Value: []string{"0"}}, {Key: "limit", Value: []string{"20"}}},
|
||||||
|
Headers: types.Headers{{Key: "Content-Type", Value: []string{"application/json"}}, {Key: "Authorization", Value: []string{"Bearer token2"}}},
|
||||||
|
Cookies: types.Cookies{{Key: "lang", Value: []string{"en"}}, {Key: "theme", Value: []string{"light"}}},
|
||||||
|
Bodies: types.Bodies{types.Body("updated")},
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Merge(newConfig)
|
||||||
|
|
||||||
|
// Check params
|
||||||
|
assert.Len(t, config.Params, 2)
|
||||||
|
limitValue := config.Params.GetValue("limit")
|
||||||
|
require.NotNil(t, limitValue)
|
||||||
|
assert.Equal(t, []string{"10", "20"}, *limitValue)
|
||||||
|
offsetValue := config.Params.GetValue("offset")
|
||||||
|
require.NotNil(t, offsetValue)
|
||||||
|
assert.Equal(t, []string{"0"}, *offsetValue)
|
||||||
|
|
||||||
|
// Check headers
|
||||||
|
assert.Len(t, config.Headers, 2)
|
||||||
|
authValue := config.Headers.GetValue("Authorization")
|
||||||
|
require.NotNil(t, authValue)
|
||||||
|
assert.Equal(t, []string{"Bearer token1", "Bearer token2"}, *authValue)
|
||||||
|
contentTypeValue := config.Headers.GetValue("Content-Type")
|
||||||
|
require.NotNil(t, contentTypeValue)
|
||||||
|
assert.Equal(t, []string{"application/json"}, *contentTypeValue)
|
||||||
|
|
||||||
|
// Check cookies
|
||||||
|
assert.Len(t, config.Cookies, 2)
|
||||||
|
themeValue := config.Cookies.GetValue("theme")
|
||||||
|
require.NotNil(t, themeValue)
|
||||||
|
assert.Equal(t, []string{"dark", "light"}, *themeValue)
|
||||||
|
langValue := config.Cookies.GetValue("lang")
|
||||||
|
require.NotNil(t, langValue)
|
||||||
|
assert.Equal(t, []string{"en"}, *langValue)
|
||||||
|
|
||||||
|
// Check bodies
|
||||||
|
assert.Len(t, config.Bodies, 2)
|
||||||
|
assert.Equal(t, types.Body("original"), config.Bodies[0])
|
||||||
|
assert.Equal(t, types.Body("updated"), config.Bodies[1])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("MergeConfig with empty slices does not append", func(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
Params: types.Params{{Key: "existing", Value: []string{"value"}}},
|
||||||
|
Headers: types.Headers{{Key: "Existing-Header", Value: []string{"value"}}},
|
||||||
|
Cookies: types.Cookies{{Key: "existing", Value: []string{"value"}}},
|
||||||
|
Bodies: types.Bodies{types.Body("existing")},
|
||||||
|
Proxies: types.Proxies{},
|
||||||
|
}
|
||||||
|
|
||||||
|
originalParams := len(config.Params)
|
||||||
|
originalHeaders := len(config.Headers)
|
||||||
|
originalCookies := len(config.Cookies)
|
||||||
|
originalBodies := len(config.Bodies)
|
||||||
|
originalProxies := len(config.Proxies)
|
||||||
|
|
||||||
|
newConfig := &Config{
|
||||||
|
Params: types.Params{},
|
||||||
|
Headers: types.Headers{},
|
||||||
|
Cookies: types.Cookies{},
|
||||||
|
Bodies: types.Bodies{},
|
||||||
|
Proxies: types.Proxies{},
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Merge(newConfig)
|
||||||
|
|
||||||
|
assert.Len(t, config.Params, originalParams, "Empty params should not change existing params")
|
||||||
|
assert.Len(t, config.Headers, originalHeaders, "Empty headers should not change existing headers")
|
||||||
|
assert.Len(t, config.Cookies, originalCookies, "Empty cookies should not change existing cookies")
|
||||||
|
assert.Len(t, config.Bodies, originalBodies, "Empty bodies should not change existing bodies")
|
||||||
|
assert.Len(t, config.Proxies, originalProxies, "Empty proxies should not change existing proxies")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
7
pkg/config/parser/base.go
Normal file
7
pkg/config/parser/base.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import "github.com/aykhans/dodo/pkg/config"
|
||||||
|
|
||||||
|
type IParser interface {
|
||||||
|
Parse() (*config.Config, error)
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
package config
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/aykhans/dodo/pkg/config"
|
||||||
"github.com/aykhans/dodo/pkg/types"
|
"github.com/aykhans/dodo/pkg/types"
|
||||||
"github.com/aykhans/dodo/pkg/utils"
|
"github.com/aykhans/dodo/pkg/utils"
|
||||||
)
|
)
|
||||||
@@ -52,9 +53,10 @@ Flags:
|
|||||||
-x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080")
|
-x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080")
|
||||||
-skip-verify bool Skip SSL/TLS certificate verification (default %v)`
|
-skip-verify bool Skip SSL/TLS certificate verification (default %v)`
|
||||||
|
|
||||||
|
var _ IParser = ConfigCLIParser{}
|
||||||
|
|
||||||
type ConfigCLIParser struct {
|
type ConfigCLIParser struct {
|
||||||
args []string
|
args []string
|
||||||
configFile *types.ConfigFile
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfigCLIParser(args []string) *ConfigCLIParser {
|
func NewConfigCLIParser(args []string) *ConfigCLIParser {
|
||||||
@@ -64,10 +66,6 @@ func NewConfigCLIParser(args []string) *ConfigCLIParser {
|
|||||||
return &ConfigCLIParser{args: args}
|
return &ConfigCLIParser{args: args}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (parser ConfigCLIParser) GetConfigFile() *types.ConfigFile {
|
|
||||||
return parser.configFile
|
|
||||||
}
|
|
||||||
|
|
||||||
type stringSliceArg []string
|
type stringSliceArg []string
|
||||||
|
|
||||||
func (arg *stringSliceArg) String() string {
|
func (arg *stringSliceArg) String() string {
|
||||||
@@ -84,14 +82,14 @@ func (arg *stringSliceArg) Set(value string) error {
|
|||||||
// - types.ErrCLINoArgs
|
// - types.ErrCLINoArgs
|
||||||
// - types.CLIUnexpectedArgsError
|
// - types.CLIUnexpectedArgsError
|
||||||
// - types.FieldParseErrors
|
// - types.FieldParseErrors
|
||||||
func (parser *ConfigCLIParser) Parse() (*Config, error) {
|
func (parser ConfigCLIParser) Parse() (*config.Config, error) {
|
||||||
flagSet := flag.NewFlagSet("dodo", flag.ExitOnError)
|
flagSet := flag.NewFlagSet("dodo", flag.ExitOnError)
|
||||||
|
|
||||||
flagSet.Usage = func() { parser.PrintHelp() }
|
flagSet.Usage = func() { parser.PrintHelp() }
|
||||||
|
|
||||||
var (
|
var (
|
||||||
config = &Config{}
|
config = &config.Config{}
|
||||||
configFile string
|
configFiles = stringSliceArg{}
|
||||||
yes bool
|
yes bool
|
||||||
skipVerify bool
|
skipVerify bool
|
||||||
method string
|
method string
|
||||||
@@ -108,8 +106,8 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
{
|
{
|
||||||
flagSet.StringVar(&configFile, "config-file", "", "Config file")
|
flagSet.Var(&configFiles, "config-file", "Config file")
|
||||||
flagSet.StringVar(&configFile, "f", "", "Config file")
|
flagSet.Var(&configFiles, "f", "Config file")
|
||||||
|
|
||||||
flagSet.BoolVar(&yes, "yes", false, "Answer yes to all questions")
|
flagSet.BoolVar(&yes, "yes", false, "Answer yes to all questions")
|
||||||
flagSet.BoolVar(&yes, "y", false, "Answer yes to all questions")
|
flagSet.BoolVar(&yes, "y", false, "Answer yes to all questions")
|
||||||
@@ -171,28 +169,50 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
|
|||||||
flagSet.Visit(func(flagVar *flag.Flag) {
|
flagSet.Visit(func(flagVar *flag.Flag) {
|
||||||
switch flagVar.Name {
|
switch flagVar.Name {
|
||||||
case "config-file", "f":
|
case "config-file", "f":
|
||||||
var err error
|
for i, configFile := range configFiles {
|
||||||
parser.configFile, err = types.ParseConfigFile(configFile)
|
configFileParsed, err := types.ParseConfigFile(configFile)
|
||||||
|
|
||||||
_ = utils.HandleErrorOrDie(err,
|
_ = utils.HandleErrorOrDie(err,
|
||||||
utils.OnSentinelError(types.ErrConfigFileExtensionNotFound, func(err error) error {
|
utils.OnSentinelError(types.ErrConfigFileExtensionNotFound, func(err error) error {
|
||||||
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("config-file", errors.New("file extension not found")))
|
fieldParseErrors = append(
|
||||||
|
fieldParseErrors,
|
||||||
|
*types.NewFieldParseError(
|
||||||
|
fmt.Sprintf("config-file[%d]", i),
|
||||||
|
configFile,
|
||||||
|
errors.New("file extension not found"),
|
||||||
|
),
|
||||||
|
)
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
utils.OnCustomError(func(err types.RemoteConfigFileParseError) error {
|
utils.OnCustomError(func(err types.RemoteConfigFileParseError) error {
|
||||||
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("config-file", fmt.Errorf("parse error: %w", err)))
|
fieldParseErrors = append(
|
||||||
|
fieldParseErrors,
|
||||||
|
*types.NewFieldParseError(
|
||||||
|
fmt.Sprintf("config-file[%d]", i),
|
||||||
|
configFile,
|
||||||
|
fmt.Errorf("parse error: %w", err),
|
||||||
|
),
|
||||||
|
)
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
utils.OnCustomError(func(err types.UnknownConfigFileTypeError) error {
|
utils.OnCustomError(func(err types.UnknownConfigFileTypeError) error {
|
||||||
fieldParseErrors = append(
|
fieldParseErrors = append(
|
||||||
fieldParseErrors,
|
fieldParseErrors,
|
||||||
*types.NewFieldParseError(
|
*types.NewFieldParseError(
|
||||||
"config-file",
|
fmt.Sprintf("config-file[%d]", i),
|
||||||
fmt.Errorf("file type '%s' not supported (supported types: %s)", err.Type, types.ConfigFileTypeYAML.String()),
|
configFile,
|
||||||
|
fmt.Errorf("file type '%s' not supported (supported types: %s)", err.Type, types.ConfigFileTypeYAML),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
config.Files = append(config.Files, *configFileParsed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case "yes", "y":
|
case "yes", "y":
|
||||||
config.Yes = utils.ToPtr(yes)
|
config.Yes = utils.ToPtr(yes)
|
||||||
case "skip-verify":
|
case "skip-verify":
|
||||||
@@ -202,7 +222,7 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
|
|||||||
case "url", "u":
|
case "url", "u":
|
||||||
urlParsed, err := url.Parse(urlInput)
|
urlParsed, err := url.Parse(urlInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("url", err))
|
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("url", urlInput, err))
|
||||||
} else {
|
} else {
|
||||||
config.URL = urlParsed
|
config.URL = urlParsed
|
||||||
}
|
}
|
||||||
@@ -228,7 +248,7 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
fieldParseErrors = append(
|
fieldParseErrors = append(
|
||||||
fieldParseErrors,
|
fieldParseErrors,
|
||||||
*types.NewFieldParseError(fmt.Sprintf("proxy[%d]", i), err),
|
*types.NewFieldParseError(fmt.Sprintf("proxy[%d]", i), proxy, err),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,14 +262,14 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (parser *ConfigCLIParser) PrintHelp() {
|
func (parser ConfigCLIParser) PrintHelp() {
|
||||||
fmt.Printf(
|
fmt.Printf(
|
||||||
cliUsageText+"\n",
|
cliUsageText+"\n",
|
||||||
Defaults.Yes,
|
config.Defaults.Yes,
|
||||||
Defaults.DodosCount,
|
config.Defaults.DodosCount,
|
||||||
Defaults.RequestTimeout,
|
config.Defaults.RequestTimeout,
|
||||||
Defaults.Method,
|
config.Defaults.Method,
|
||||||
Defaults.SkipVerify,
|
config.Defaults.SkipVerify,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
package config
|
package parser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/aykhans/dodo/pkg/config"
|
||||||
"github.com/aykhans/dodo/pkg/types"
|
"github.com/aykhans/dodo/pkg/types"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -20,7 +21,6 @@ func TestNewConfigCLIParser(t *testing.T) {
|
|||||||
|
|
||||||
require.NotNil(t, parser)
|
require.NotNil(t, parser)
|
||||||
assert.Equal(t, args, parser.args)
|
assert.Equal(t, args, parser.args)
|
||||||
assert.Nil(t, parser.configFile)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NewConfigCLIParser with nil args", func(t *testing.T) {
|
t.Run("NewConfigCLIParser with nil args", func(t *testing.T) {
|
||||||
@@ -28,7 +28,6 @@ func TestNewConfigCLIParser(t *testing.T) {
|
|||||||
|
|
||||||
require.NotNil(t, parser)
|
require.NotNil(t, parser)
|
||||||
assert.Equal(t, []string{}, parser.args)
|
assert.Equal(t, []string{}, parser.args)
|
||||||
assert.Nil(t, parser.configFile)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("NewConfigCLIParser with empty args", func(t *testing.T) {
|
t.Run("NewConfigCLIParser with empty args", func(t *testing.T) {
|
||||||
@@ -37,21 +36,6 @@ func TestNewConfigCLIParser(t *testing.T) {
|
|||||||
|
|
||||||
require.NotNil(t, parser)
|
require.NotNil(t, parser)
|
||||||
assert.Equal(t, args, parser.args)
|
assert.Equal(t, args, parser.args)
|
||||||
assert.Nil(t, parser.configFile)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigCLIParser_GetConfigFile(t *testing.T) {
|
|
||||||
t.Run("GetConfigFile returns nil when no config file is set", func(t *testing.T) {
|
|
||||||
parser := NewConfigCLIParser([]string{"dodo"})
|
|
||||||
assert.Nil(t, parser.GetConfigFile())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("GetConfigFile returns config file when set", func(t *testing.T) {
|
|
||||||
parser := NewConfigCLIParser([]string{"dodo"})
|
|
||||||
expectedConfigFile := &types.ConfigFile{}
|
|
||||||
parser.configFile = expectedConfigFile
|
|
||||||
assert.Equal(t, expectedConfigFile, parser.GetConfigFile())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +114,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
|
|||||||
require.ErrorAs(t, err, &fieldErr)
|
require.ErrorAs(t, err, &fieldErr)
|
||||||
assert.Len(t, fieldErr.Errors, 1)
|
assert.Len(t, fieldErr.Errors, 1)
|
||||||
assert.Equal(t, "url", fieldErr.Errors[0].Field)
|
assert.Equal(t, "url", fieldErr.Errors[0].Field)
|
||||||
|
assert.Equal(t, "://invalid-url", fieldErr.Errors[0].Value)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Parse with method flag", func(t *testing.T) {
|
t.Run("Parse with method flag", func(t *testing.T) {
|
||||||
@@ -272,6 +257,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
|
|||||||
require.ErrorAs(t, err, &fieldErr)
|
require.ErrorAs(t, err, &fieldErr)
|
||||||
assert.Len(t, fieldErr.Errors, 1)
|
assert.Len(t, fieldErr.Errors, 1)
|
||||||
assert.Equal(t, "proxy[0]", fieldErr.Errors[0].Field)
|
assert.Equal(t, "proxy[0]", fieldErr.Errors[0].Field)
|
||||||
|
assert.Equal(t, "://invalid-proxy", fieldErr.Errors[0].Value)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Parse with mixed valid and invalid proxies", func(t *testing.T) {
|
t.Run("Parse with mixed valid and invalid proxies", func(t *testing.T) {
|
||||||
@@ -283,6 +269,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
|
|||||||
require.ErrorAs(t, err, &fieldErr)
|
require.ErrorAs(t, err, &fieldErr)
|
||||||
assert.Len(t, fieldErr.Errors, 1)
|
assert.Len(t, fieldErr.Errors, 1)
|
||||||
assert.Equal(t, "proxy[1]", fieldErr.Errors[0].Field)
|
assert.Equal(t, "proxy[1]", fieldErr.Errors[0].Field)
|
||||||
|
assert.Equal(t, "://invalid", fieldErr.Errors[0].Value)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Parse with long flag names", func(t *testing.T) {
|
t.Run("Parse with long flag names", func(t *testing.T) {
|
||||||
@@ -318,7 +305,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, config)
|
require.NotNil(t, config)
|
||||||
assert.NotNil(t, parser.GetConfigFile())
|
assert.Len(t, config.Files, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Parse with config-file flag using long form", func(t *testing.T) {
|
t.Run("Parse with config-file flag using long form", func(t *testing.T) {
|
||||||
@@ -327,7 +314,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, config)
|
require.NotNil(t, config)
|
||||||
assert.NotNil(t, parser.GetConfigFile())
|
assert.Len(t, config.Files, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Parse with config-file flag invalid extension", func(t *testing.T) {
|
t.Run("Parse with config-file flag invalid extension", func(t *testing.T) {
|
||||||
@@ -338,7 +325,8 @@ func TestConfigCLIParser_Parse(t *testing.T) {
|
|||||||
var fieldErr types.FieldParseErrors
|
var fieldErr types.FieldParseErrors
|
||||||
require.ErrorAs(t, err, &fieldErr)
|
require.ErrorAs(t, err, &fieldErr)
|
||||||
assert.Len(t, fieldErr.Errors, 1)
|
assert.Len(t, fieldErr.Errors, 1)
|
||||||
assert.Equal(t, "config-file", fieldErr.Errors[0].Field)
|
assert.Equal(t, "config-file[0]", 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")
|
assert.Contains(t, fieldErr.Errors[0].Err.Error(), "file extension not found")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -350,7 +338,8 @@ func TestConfigCLIParser_Parse(t *testing.T) {
|
|||||||
var fieldErr types.FieldParseErrors
|
var fieldErr types.FieldParseErrors
|
||||||
require.ErrorAs(t, err, &fieldErr)
|
require.ErrorAs(t, err, &fieldErr)
|
||||||
assert.Len(t, fieldErr.Errors, 1)
|
assert.Len(t, fieldErr.Errors, 1)
|
||||||
assert.Equal(t, "config-file", fieldErr.Errors[0].Field)
|
assert.Equal(t, "config-file[0]", 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(), "file type")
|
||||||
assert.Contains(t, fieldErr.Errors[0].Err.Error(), "not supported")
|
assert.Contains(t, fieldErr.Errors[0].Err.Error(), "not supported")
|
||||||
})
|
})
|
||||||
@@ -361,7 +350,16 @@ func TestConfigCLIParser_Parse(t *testing.T) {
|
|||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, config)
|
require.NotNil(t, config)
|
||||||
assert.NotNil(t, parser.GetConfigFile())
|
assert.Len(t, config.Files, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Parse with multiple config files", func(t *testing.T) {
|
||||||
|
parser := NewConfigCLIParser([]string{"dodo", "-f", "/path/config1.yaml", "-f", "/path/config2.yml"})
|
||||||
|
config, err := parser.Parse()
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, config)
|
||||||
|
assert.Len(t, config.Files, 2)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Parse with all flags combined", func(t *testing.T) {
|
t.Run("Parse with all flags combined", func(t *testing.T) {
|
||||||
@@ -412,7 +410,7 @@ func TestConfigCLIParser_Parse(t *testing.T) {
|
|||||||
assert.Len(t, config.Proxies, 1)
|
assert.Len(t, config.Proxies, 1)
|
||||||
assert.Equal(t, "http://proxy.example.com:3128", config.Proxies[0].String())
|
assert.Equal(t, "http://proxy.example.com:3128", config.Proxies[0].String())
|
||||||
|
|
||||||
assert.NotNil(t, parser.GetConfigFile())
|
assert.Len(t, config.Files, 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Parse with multiple field parse errors", func(t *testing.T) {
|
t.Run("Parse with multiple field parse errors", func(t *testing.T) {
|
||||||
@@ -437,6 +435,15 @@ func TestConfigCLIParser_Parse(t *testing.T) {
|
|||||||
assert.True(t, fields["url"])
|
assert.True(t, fields["url"])
|
||||||
assert.True(t, fields["proxy[0]"])
|
assert.True(t, fields["proxy[0]"])
|
||||||
assert.True(t, fields["proxy[1]"])
|
assert.True(t, fields["proxy[1]"])
|
||||||
|
|
||||||
|
// Check error values
|
||||||
|
values := make(map[string]string)
|
||||||
|
for _, parseErr := range fieldErr.Errors {
|
||||||
|
values[parseErr.Field] = parseErr.Value
|
||||||
|
}
|
||||||
|
assert.Equal(t, "://invalid-url", values["url"])
|
||||||
|
assert.Equal(t, "://invalid-proxy1", values["proxy[0]"])
|
||||||
|
assert.Equal(t, "://invalid-proxy2", values["proxy[1]"])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +486,7 @@ func TestConfigCLIParser_PrintHelp(t *testing.T) {
|
|||||||
assert.Contains(t, output, "-f, -config-file")
|
assert.Contains(t, output, "-f, -config-file")
|
||||||
|
|
||||||
// Verify default values are included
|
// 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, "1") // DodosCount default
|
||||||
assert.Contains(t, output, "10s") // RequestTimeout default
|
assert.Contains(t, output, "10s") // RequestTimeout default
|
||||||
assert.Contains(t, output, "false") // Yes default
|
assert.Contains(t, output, "false") // Yes default
|
236
pkg/config/parser/env.go
Normal file
236
pkg/config/parser/env.go
Normal file
@@ -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))
|
||||||
|
}
|
1129
pkg/config/parser/env_test.go
Normal file
1129
pkg/config/parser/env_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ func (body Body) String() string {
|
|||||||
|
|
||||||
type Bodies []Body
|
type Bodies []Body
|
||||||
|
|
||||||
func (bodies *Bodies) Append(body Body) {
|
func (bodies *Bodies) Append(body ...Body) {
|
||||||
*bodies = append(*bodies, body)
|
*bodies = append(*bodies, body...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bodies *Bodies) Parse(rawValues ...string) {
|
func (bodies *Bodies) Parse(rawValues ...string) {
|
||||||
|
@@ -49,6 +49,16 @@ func TestBodies_Append(t *testing.T) {
|
|||||||
assert.Equal(t, Body("third"), (*bodies)[2])
|
assert.Equal(t, Body("third"), (*bodies)[2])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append multiple bodies in single call", func(t *testing.T) {
|
||||||
|
bodies := &Bodies{}
|
||||||
|
bodies.Append(Body("first"), Body("second"), Body("third"))
|
||||||
|
|
||||||
|
assert.Len(t, *bodies, 3)
|
||||||
|
assert.Equal(t, Body("first"), (*bodies)[0])
|
||||||
|
assert.Equal(t, Body("second"), (*bodies)[1])
|
||||||
|
assert.Equal(t, Body("third"), (*bodies)[2])
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Append to existing bodies", func(t *testing.T) {
|
t.Run("Append to existing bodies", func(t *testing.T) {
|
||||||
bodies := &Bodies{Body("existing")}
|
bodies := &Bodies{Body("existing")}
|
||||||
bodies.Append(Body("new"))
|
bodies.Append(Body("new"))
|
||||||
@@ -58,6 +68,17 @@ func TestBodies_Append(t *testing.T) {
|
|||||||
assert.Equal(t, Body("new"), (*bodies)[1])
|
assert.Equal(t, Body("new"), (*bodies)[1])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append multiple to existing bodies", func(t *testing.T) {
|
||||||
|
bodies := &Bodies{Body("existing")}
|
||||||
|
bodies.Append(Body("new1"), Body("new2"), Body("new3"))
|
||||||
|
|
||||||
|
assert.Len(t, *bodies, 4)
|
||||||
|
assert.Equal(t, Body("existing"), (*bodies)[0])
|
||||||
|
assert.Equal(t, Body("new1"), (*bodies)[1])
|
||||||
|
assert.Equal(t, Body("new2"), (*bodies)[2])
|
||||||
|
assert.Equal(t, Body("new3"), (*bodies)[3])
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Append empty body", func(t *testing.T) {
|
t.Run("Append empty body", func(t *testing.T) {
|
||||||
bodies := &Bodies{}
|
bodies := &Bodies{}
|
||||||
bodies.Append(Body(""))
|
bodies.Append(Body(""))
|
||||||
@@ -65,6 +86,29 @@ func TestBodies_Append(t *testing.T) {
|
|||||||
assert.Len(t, *bodies, 1)
|
assert.Len(t, *bodies, 1)
|
||||||
assert.Empty(t, (*bodies)[0].String())
|
assert.Empty(t, (*bodies)[0].String())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append no bodies", func(t *testing.T) {
|
||||||
|
bodies := &Bodies{Body("existing")}
|
||||||
|
bodies.Append()
|
||||||
|
|
||||||
|
assert.Len(t, *bodies, 1)
|
||||||
|
assert.Equal(t, Body("existing"), (*bodies)[0])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append mixed bodies", func(t *testing.T) {
|
||||||
|
bodies := &Bodies{}
|
||||||
|
bodies.Append(Body("first"), Body(""))
|
||||||
|
bodies.Append(Body("second"))
|
||||||
|
bodies.Append(Body("third"), Body("fourth"), Body("fifth"))
|
||||||
|
|
||||||
|
assert.Len(t, *bodies, 6)
|
||||||
|
assert.Equal(t, Body("first"), (*bodies)[0])
|
||||||
|
assert.Equal(t, Body(""), (*bodies)[1])
|
||||||
|
assert.Equal(t, Body("second"), (*bodies)[2])
|
||||||
|
assert.Equal(t, Body("third"), (*bodies)[3])
|
||||||
|
assert.Equal(t, Body("fourth"), (*bodies)[4])
|
||||||
|
assert.Equal(t, Body("fifth"), (*bodies)[5])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBodies_Parse(t *testing.T) {
|
func TestBodies_Parse(t *testing.T) {
|
||||||
|
@@ -6,39 +6,19 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ConfigFileType int
|
type ConfigFileType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ConfigFileTypeYAML ConfigFileType = iota
|
ConfigFileTypeYAML ConfigFileType = "yaml/yml"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (t ConfigFileType) String() string {
|
type ConfigFileLocationType string
|
||||||
switch t {
|
|
||||||
case ConfigFileTypeYAML:
|
|
||||||
return "yaml/yml"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigFileLocationType int
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ConfigFileLocationLocal ConfigFileLocationType = iota
|
ConfigFileLocationLocal ConfigFileLocationType = "local"
|
||||||
ConfigFileLocationRemote
|
ConfigFileLocationRemote ConfigFileLocationType = "remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (l ConfigFileLocationType) String() string {
|
|
||||||
switch l {
|
|
||||||
case ConfigFileLocationLocal:
|
|
||||||
return "local"
|
|
||||||
case ConfigFileLocationRemote:
|
|
||||||
return "remote"
|
|
||||||
default:
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigFile struct {
|
type ConfigFile struct {
|
||||||
path string
|
path string
|
||||||
_type ConfigFileType
|
_type ConfigFileType
|
||||||
|
@@ -7,35 +7,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConfigFileType_String(t *testing.T) {
|
|
||||||
t.Run("ConfigFileTypeYAML returns yaml", func(t *testing.T) {
|
|
||||||
configType := ConfigFileTypeYAML
|
|
||||||
assert.Equal(t, "yaml/yml", configType.String())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Unknown config file type returns unknown", func(t *testing.T) {
|
|
||||||
configType := ConfigFileType(999)
|
|
||||||
assert.Equal(t, "unknown", configType.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigFileLocationType_String(t *testing.T) {
|
|
||||||
t.Run("ConfigFileLocationLocal returns local", func(t *testing.T) {
|
|
||||||
locationType := ConfigFileLocationLocal
|
|
||||||
assert.Equal(t, "local", locationType.String())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("ConfigFileLocationRemote returns remote", func(t *testing.T) {
|
|
||||||
locationType := ConfigFileLocationRemote
|
|
||||||
assert.Equal(t, "remote", locationType.String())
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Unknown location type returns unknown", func(t *testing.T) {
|
|
||||||
locationType := ConfigFileLocationType(999)
|
|
||||||
assert.Equal(t, "unknown", locationType.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigFile_String(t *testing.T) {
|
func TestConfigFile_String(t *testing.T) {
|
||||||
t.Run("String returns the file path", func(t *testing.T) {
|
t.Run("String returns the file path", func(t *testing.T) {
|
||||||
configFile := ConfigFile{path: "/path/to/config.yaml"}
|
configFile := ConfigFile{path: "/path/to/config.yaml"}
|
||||||
|
@@ -15,11 +15,13 @@ func (cookies Cookies) GetValue(key string) *[]string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cookies *Cookies) Append(cookie Cookie) {
|
func (cookies *Cookies) Append(cookie ...Cookie) {
|
||||||
if item := cookies.GetValue(cookie.Key); item != nil {
|
for _, c := range cookie {
|
||||||
*item = append(*item, cookie.Value...)
|
if item := cookies.GetValue(c.Key); item != nil {
|
||||||
|
*item = append(*item, c.Value...)
|
||||||
} else {
|
} else {
|
||||||
*cookies = append(*cookies, cookie)
|
*cookies = append(*cookies, c)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -88,6 +88,39 @@ func TestCookies_Append(t *testing.T) {
|
|||||||
assert.Len(t, *cookies, 3)
|
assert.Len(t, *cookies, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append multiple cookies in single call", func(t *testing.T) {
|
||||||
|
cookies := &Cookies{}
|
||||||
|
cookies.Append(
|
||||||
|
Cookie{Key: "session", Value: []string{"abc123"}},
|
||||||
|
Cookie{Key: "user", Value: []string{"john"}},
|
||||||
|
Cookie{Key: "token", Value: []string{"xyz789"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *cookies, 3)
|
||||||
|
assert.Equal(t, "session", (*cookies)[0].Key)
|
||||||
|
assert.Equal(t, []string{"abc123"}, (*cookies)[0].Value)
|
||||||
|
assert.Equal(t, "user", (*cookies)[1].Key)
|
||||||
|
assert.Equal(t, []string{"john"}, (*cookies)[1].Value)
|
||||||
|
assert.Equal(t, "token", (*cookies)[2].Key)
|
||||||
|
assert.Equal(t, []string{"xyz789"}, (*cookies)[2].Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append multiple with existing keys", func(t *testing.T) {
|
||||||
|
cookies := &Cookies{
|
||||||
|
{Key: "prefs", Value: []string{"theme=dark"}},
|
||||||
|
}
|
||||||
|
cookies.Append(
|
||||||
|
Cookie{Key: "prefs", Value: []string{"lang=en"}},
|
||||||
|
Cookie{Key: "prefs", Value: []string{"tz=UTC"}},
|
||||||
|
Cookie{Key: "session", Value: []string{"abc123"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *cookies, 2)
|
||||||
|
assert.Equal(t, []string{"theme=dark", "lang=en", "tz=UTC"}, (*cookies)[0].Value)
|
||||||
|
assert.Equal(t, "session", (*cookies)[1].Key)
|
||||||
|
assert.Equal(t, []string{"abc123"}, (*cookies)[1].Value)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Append multiple values at once", func(t *testing.T) {
|
t.Run("Append multiple values at once", func(t *testing.T) {
|
||||||
cookies := &Cookies{
|
cookies := &Cookies{
|
||||||
{Key: "tags", Value: []string{"tag1"}},
|
{Key: "tags", Value: []string{"tag1"}},
|
||||||
@@ -105,6 +138,88 @@ func TestCookies_Append(t *testing.T) {
|
|||||||
assert.Len(t, *cookies, 1)
|
assert.Len(t, *cookies, 1)
|
||||||
assert.Equal(t, []string{""}, (*cookies)[0].Value)
|
assert.Equal(t, []string{""}, (*cookies)[0].Value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append no cookies", func(t *testing.T) {
|
||||||
|
cookies := &Cookies{
|
||||||
|
{Key: "existing", Value: []string{"value"}},
|
||||||
|
}
|
||||||
|
cookies.Append()
|
||||||
|
|
||||||
|
assert.Len(t, *cookies, 1)
|
||||||
|
assert.Equal(t, "existing", (*cookies)[0].Key)
|
||||||
|
assert.Equal(t, []string{"value"}, (*cookies)[0].Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append mixed new and existing cookies", func(t *testing.T) {
|
||||||
|
cookies := &Cookies{
|
||||||
|
{Key: "session", Value: []string{"old_session"}},
|
||||||
|
{Key: "theme", Value: []string{"dark"}},
|
||||||
|
}
|
||||||
|
cookies.Append(
|
||||||
|
Cookie{Key: "session", Value: []string{"new_session"}},
|
||||||
|
Cookie{Key: "user", Value: []string{"john"}},
|
||||||
|
Cookie{Key: "theme", Value: []string{"light"}},
|
||||||
|
Cookie{Key: "lang", Value: []string{"en"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *cookies, 4)
|
||||||
|
|
||||||
|
sessionValue := cookies.GetValue("session")
|
||||||
|
require.NotNil(t, sessionValue)
|
||||||
|
assert.Equal(t, []string{"old_session", "new_session"}, *sessionValue)
|
||||||
|
|
||||||
|
themeValue := cookies.GetValue("theme")
|
||||||
|
require.NotNil(t, themeValue)
|
||||||
|
assert.Equal(t, []string{"dark", "light"}, *themeValue)
|
||||||
|
|
||||||
|
userValue := cookies.GetValue("user")
|
||||||
|
require.NotNil(t, userValue)
|
||||||
|
assert.Equal(t, []string{"john"}, *userValue)
|
||||||
|
|
||||||
|
langValue := cookies.GetValue("lang")
|
||||||
|
require.NotNil(t, langValue)
|
||||||
|
assert.Equal(t, []string{"en"}, *langValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append cookies with multiple values", func(t *testing.T) {
|
||||||
|
cookies := &Cookies{}
|
||||||
|
cookies.Append(
|
||||||
|
Cookie{Key: "permissions", Value: []string{"read", "write", "delete"}},
|
||||||
|
Cookie{Key: "features", Value: []string{"advanced", "beta"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *cookies, 2)
|
||||||
|
assert.Equal(t, []string{"read", "write", "delete"}, (*cookies)[0].Value)
|
||||||
|
assert.Equal(t, []string{"advanced", "beta"}, (*cookies)[1].Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append typical session cookies", func(t *testing.T) {
|
||||||
|
cookies := &Cookies{}
|
||||||
|
cookies.Append(
|
||||||
|
Cookie{Key: "JSESSIONID", Value: []string{"A1B2C3D4E5F6"}},
|
||||||
|
Cookie{Key: "PHPSESSID", Value: []string{"abcdef123456"}},
|
||||||
|
Cookie{Key: "session_token", Value: []string{"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}},
|
||||||
|
Cookie{Key: "__Secure-next-auth.session-token", Value: []string{"secure_token_123"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *cookies, 4)
|
||||||
|
|
||||||
|
jsessionValue := cookies.GetValue("JSESSIONID")
|
||||||
|
require.NotNil(t, jsessionValue)
|
||||||
|
assert.Equal(t, []string{"A1B2C3D4E5F6"}, *jsessionValue)
|
||||||
|
|
||||||
|
phpValue := cookies.GetValue("PHPSESSID")
|
||||||
|
require.NotNil(t, phpValue)
|
||||||
|
assert.Equal(t, []string{"abcdef123456"}, *phpValue)
|
||||||
|
|
||||||
|
sessionValue := cookies.GetValue("session_token")
|
||||||
|
require.NotNil(t, sessionValue)
|
||||||
|
assert.Equal(t, []string{"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"}, *sessionValue)
|
||||||
|
|
||||||
|
secureValue := cookies.GetValue("__Secure-next-auth.session-token")
|
||||||
|
require.NotNil(t, secureValue)
|
||||||
|
assert.Equal(t, []string{"secure_token_123"}, *secureValue)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCookies_Parse(t *testing.T) {
|
func TestCookies_Parse(t *testing.T) {
|
||||||
|
@@ -22,14 +22,15 @@ var (
|
|||||||
|
|
||||||
type FieldParseError struct {
|
type FieldParseError struct {
|
||||||
Field string
|
Field string
|
||||||
|
Value string
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFieldParseError(field string, err error) *FieldParseError {
|
func NewFieldParseError(field string, value string, err error) *FieldParseError {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = ErrNoError
|
err = ErrNoError
|
||||||
}
|
}
|
||||||
return &FieldParseError{field, err}
|
return &FieldParseError{field, value, err}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e FieldParseError) Error() string {
|
func (e FieldParseError) Error() string {
|
||||||
|
@@ -10,7 +10,7 @@ import (
|
|||||||
func TestFieldParseError_Error(t *testing.T) {
|
func TestFieldParseError_Error(t *testing.T) {
|
||||||
t.Run("Error returns formatted message", func(t *testing.T) {
|
t.Run("Error returns formatted message", func(t *testing.T) {
|
||||||
originalErr := errors.New("invalid value")
|
originalErr := errors.New("invalid value")
|
||||||
fieldErr := NewFieldParseError("username", originalErr)
|
fieldErr := NewFieldParseError("username", "testuser", originalErr)
|
||||||
|
|
||||||
expected := "Field 'username' parse failed: invalid value"
|
expected := "Field 'username' parse failed: invalid value"
|
||||||
assert.Equal(t, expected, fieldErr.Error())
|
assert.Equal(t, expected, fieldErr.Error())
|
||||||
@@ -18,14 +18,14 @@ func TestFieldParseError_Error(t *testing.T) {
|
|||||||
|
|
||||||
t.Run("Error with empty field name", func(t *testing.T) {
|
t.Run("Error with empty field name", func(t *testing.T) {
|
||||||
originalErr := errors.New("test error")
|
originalErr := errors.New("test error")
|
||||||
fieldErr := NewFieldParseError("", originalErr)
|
fieldErr := NewFieldParseError("", "somevalue", originalErr)
|
||||||
|
|
||||||
expected := "Field '' parse failed: test error"
|
expected := "Field '' parse failed: test error"
|
||||||
assert.Equal(t, expected, fieldErr.Error())
|
assert.Equal(t, expected, fieldErr.Error())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Error with nil underlying error", func(t *testing.T) {
|
t.Run("Error with nil underlying error", func(t *testing.T) {
|
||||||
fieldErr := NewFieldParseError("field", nil)
|
fieldErr := NewFieldParseError("field", "value123", nil)
|
||||||
|
|
||||||
expected := "Field 'field' parse failed: no error (internal)"
|
expected := "Field 'field' parse failed: no error (internal)"
|
||||||
assert.Equal(t, expected, fieldErr.Error())
|
assert.Equal(t, expected, fieldErr.Error())
|
||||||
@@ -35,13 +35,13 @@ func TestFieldParseError_Error(t *testing.T) {
|
|||||||
func TestFieldParseError_Unwrap(t *testing.T) {
|
func TestFieldParseError_Unwrap(t *testing.T) {
|
||||||
t.Run("Unwrap returns original error", func(t *testing.T) {
|
t.Run("Unwrap returns original error", func(t *testing.T) {
|
||||||
originalErr := errors.New("original error")
|
originalErr := errors.New("original error")
|
||||||
fieldErr := NewFieldParseError("field", originalErr)
|
fieldErr := NewFieldParseError("field", "value", originalErr)
|
||||||
|
|
||||||
assert.Equal(t, originalErr, fieldErr.Unwrap())
|
assert.Equal(t, originalErr, fieldErr.Unwrap())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Unwrap with nil error", func(t *testing.T) {
|
t.Run("Unwrap with nil error", func(t *testing.T) {
|
||||||
fieldErr := NewFieldParseError("field", nil)
|
fieldErr := NewFieldParseError("field", "value", nil)
|
||||||
|
|
||||||
assert.Equal(t, ErrNoError, fieldErr.Unwrap())
|
assert.Equal(t, ErrNoError, fieldErr.Unwrap())
|
||||||
})
|
})
|
||||||
@@ -50,16 +50,18 @@ func TestFieldParseError_Unwrap(t *testing.T) {
|
|||||||
func TestNewFieldParseError(t *testing.T) {
|
func TestNewFieldParseError(t *testing.T) {
|
||||||
t.Run("Creates FieldParseError with correct values", func(t *testing.T) {
|
t.Run("Creates FieldParseError with correct values", func(t *testing.T) {
|
||||||
originalErr := errors.New("test error")
|
originalErr := errors.New("test error")
|
||||||
fieldErr := NewFieldParseError("testField", originalErr)
|
fieldErr := NewFieldParseError("testField", "testValue", originalErr)
|
||||||
|
|
||||||
assert.Equal(t, "testField", fieldErr.Field)
|
assert.Equal(t, "testField", fieldErr.Field)
|
||||||
|
assert.Equal(t, "testValue", fieldErr.Value)
|
||||||
assert.Equal(t, originalErr, fieldErr.Err)
|
assert.Equal(t, originalErr, fieldErr.Err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Creates FieldParseError with ErrNoError when nil passed", func(t *testing.T) {
|
t.Run("Creates FieldParseError with ErrNoError when nil passed", func(t *testing.T) {
|
||||||
fieldErr := NewFieldParseError("testField", nil)
|
fieldErr := NewFieldParseError("testField", "testValue", nil)
|
||||||
|
|
||||||
assert.Equal(t, "testField", fieldErr.Field)
|
assert.Equal(t, "testField", fieldErr.Field)
|
||||||
|
assert.Equal(t, "testValue", fieldErr.Value)
|
||||||
assert.Equal(t, ErrNoError, fieldErr.Err)
|
assert.Equal(t, ErrNoError, fieldErr.Err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -72,7 +74,7 @@ func TestFieldParseErrors_Error(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Error with single error returns single error message", func(t *testing.T) {
|
t.Run("Error with single error returns single error message", func(t *testing.T) {
|
||||||
fieldErr := *NewFieldParseError("field1", errors.New("error1"))
|
fieldErr := *NewFieldParseError("field1", "value1", errors.New("error1"))
|
||||||
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr})
|
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr})
|
||||||
|
|
||||||
expected := "Field 'field1' parse failed: error1"
|
expected := "Field 'field1' parse failed: error1"
|
||||||
@@ -80,9 +82,9 @@ func TestFieldParseErrors_Error(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Error with multiple errors returns concatenated messages", func(t *testing.T) {
|
t.Run("Error with multiple errors returns concatenated messages", func(t *testing.T) {
|
||||||
fieldErr1 := *NewFieldParseError("field1", errors.New("error1"))
|
fieldErr1 := *NewFieldParseError("field1", "value1", errors.New("error1"))
|
||||||
fieldErr2 := *NewFieldParseError("field2", errors.New("error2"))
|
fieldErr2 := *NewFieldParseError("field2", "value2", errors.New("error2"))
|
||||||
fieldErr3 := *NewFieldParseError("field3", errors.New("error3"))
|
fieldErr3 := *NewFieldParseError("field3", "value3", errors.New("error3"))
|
||||||
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2, fieldErr3})
|
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2, fieldErr3})
|
||||||
|
|
||||||
expected := "Field 'field1' parse failed: error1\nField 'field2' parse failed: error2\nField 'field3' parse failed: error3"
|
expected := "Field 'field1' parse failed: error1\nField 'field2' parse failed: error2\nField 'field3' parse failed: error3"
|
||||||
@@ -90,8 +92,8 @@ func TestFieldParseErrors_Error(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Error with two errors", func(t *testing.T) {
|
t.Run("Error with two errors", func(t *testing.T) {
|
||||||
fieldErr1 := *NewFieldParseError("username", errors.New("too short"))
|
fieldErr1 := *NewFieldParseError("username", "john", errors.New("too short"))
|
||||||
fieldErr2 := *NewFieldParseError("email", errors.New("invalid format"))
|
fieldErr2 := *NewFieldParseError("email", "invalid", errors.New("invalid format"))
|
||||||
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2})
|
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2})
|
||||||
|
|
||||||
expected := "Field 'username' parse failed: too short\nField 'email' parse failed: invalid format"
|
expected := "Field 'username' parse failed: too short\nField 'email' parse failed: invalid format"
|
||||||
@@ -101,8 +103,8 @@ func TestFieldParseErrors_Error(t *testing.T) {
|
|||||||
|
|
||||||
func TestNewFieldParseErrors(t *testing.T) {
|
func TestNewFieldParseErrors(t *testing.T) {
|
||||||
t.Run("Creates FieldParseErrors with correct values", func(t *testing.T) {
|
t.Run("Creates FieldParseErrors with correct values", func(t *testing.T) {
|
||||||
fieldErr1 := *NewFieldParseError("field1", errors.New("error1"))
|
fieldErr1 := *NewFieldParseError("field1", "value1", errors.New("error1"))
|
||||||
fieldErr2 := *NewFieldParseError("field2", errors.New("error2"))
|
fieldErr2 := *NewFieldParseError("field2", "value2", errors.New("error2"))
|
||||||
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2})
|
fieldErrors := NewFieldParseErrors([]FieldParseError{fieldErr1, fieldErr2})
|
||||||
|
|
||||||
assert.Len(t, fieldErrors.Errors, 2)
|
assert.Len(t, fieldErrors.Errors, 2)
|
||||||
@@ -258,7 +260,7 @@ func TestErrorConstants(t *testing.T) {
|
|||||||
|
|
||||||
func TestErrorImplementsErrorInterface(t *testing.T) {
|
func TestErrorImplementsErrorInterface(t *testing.T) {
|
||||||
t.Run("FieldParseError implements error interface", func(t *testing.T) {
|
t.Run("FieldParseError implements error interface", func(t *testing.T) {
|
||||||
var err error = NewFieldParseError("field", errors.New("test"))
|
var err error = NewFieldParseError("field", "value", errors.New("test"))
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -25,11 +25,13 @@ func (headers Headers) GetValue(key string) *[]string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (headers *Headers) Append(header Header) {
|
func (headers *Headers) Append(header ...Header) {
|
||||||
if item := headers.GetValue(header.Key); item != nil {
|
for _, h := range header {
|
||||||
*item = append(*item, header.Value...)
|
if item := headers.GetValue(h.Key); item != nil {
|
||||||
|
*item = append(*item, h.Value...)
|
||||||
} else {
|
} else {
|
||||||
*headers = append(*headers, header)
|
*headers = append(*headers, h)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -125,6 +125,39 @@ func TestHeaders_Append(t *testing.T) {
|
|||||||
assert.Len(t, *headers, 3)
|
assert.Len(t, *headers, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append multiple headers in single call", func(t *testing.T) {
|
||||||
|
headers := &Headers{}
|
||||||
|
headers.Append(
|
||||||
|
Header{Key: "Content-Type", Value: []string{"application/json"}},
|
||||||
|
Header{Key: "Authorization", Value: []string{"Bearer token"}},
|
||||||
|
Header{Key: "Accept", Value: []string{"*/*"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *headers, 3)
|
||||||
|
assert.Equal(t, "Content-Type", (*headers)[0].Key)
|
||||||
|
assert.Equal(t, []string{"application/json"}, (*headers)[0].Value)
|
||||||
|
assert.Equal(t, "Authorization", (*headers)[1].Key)
|
||||||
|
assert.Equal(t, []string{"Bearer token"}, (*headers)[1].Value)
|
||||||
|
assert.Equal(t, "Accept", (*headers)[2].Key)
|
||||||
|
assert.Equal(t, []string{"*/*"}, (*headers)[2].Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append multiple with existing keys", func(t *testing.T) {
|
||||||
|
headers := &Headers{
|
||||||
|
{Key: "Accept", Value: []string{"text/html"}},
|
||||||
|
}
|
||||||
|
headers.Append(
|
||||||
|
Header{Key: "Accept", Value: []string{"application/json"}},
|
||||||
|
Header{Key: "Accept", Value: []string{"application/xml"}},
|
||||||
|
Header{Key: "Content-Type", Value: []string{"text/plain"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *headers, 2)
|
||||||
|
assert.Equal(t, []string{"text/html", "application/json", "application/xml"}, (*headers)[0].Value)
|
||||||
|
assert.Equal(t, "Content-Type", (*headers)[1].Key)
|
||||||
|
assert.Equal(t, []string{"text/plain"}, (*headers)[1].Value)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Append multiple values at once", func(t *testing.T) {
|
t.Run("Append multiple values at once", func(t *testing.T) {
|
||||||
headers := &Headers{
|
headers := &Headers{
|
||||||
{Key: "Accept-Language", Value: []string{"en"}},
|
{Key: "Accept-Language", Value: []string{"en"}},
|
||||||
@@ -142,6 +175,78 @@ func TestHeaders_Append(t *testing.T) {
|
|||||||
assert.Len(t, *headers, 1)
|
assert.Len(t, *headers, 1)
|
||||||
assert.Equal(t, []string{""}, (*headers)[0].Value)
|
assert.Equal(t, []string{""}, (*headers)[0].Value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append no headers", func(t *testing.T) {
|
||||||
|
headers := &Headers{
|
||||||
|
{Key: "Existing", Value: []string{"value"}},
|
||||||
|
}
|
||||||
|
headers.Append()
|
||||||
|
|
||||||
|
assert.Len(t, *headers, 1)
|
||||||
|
assert.Equal(t, "Existing", (*headers)[0].Key)
|
||||||
|
assert.Equal(t, []string{"value"}, (*headers)[0].Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append mixed new and existing headers", func(t *testing.T) {
|
||||||
|
headers := &Headers{
|
||||||
|
{Key: "Cache-Control", Value: []string{"no-cache"}},
|
||||||
|
{Key: "Accept", Value: []string{"text/html"}},
|
||||||
|
}
|
||||||
|
headers.Append(
|
||||||
|
Header{Key: "Cache-Control", Value: []string{"no-store"}},
|
||||||
|
Header{Key: "User-Agent", Value: []string{"Mozilla/5.0"}},
|
||||||
|
Header{Key: "Accept", Value: []string{"application/json"}},
|
||||||
|
Header{Key: "X-Custom", Value: []string{"custom-value"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *headers, 4)
|
||||||
|
|
||||||
|
cacheValue := headers.GetValue("Cache-Control")
|
||||||
|
require.NotNil(t, cacheValue)
|
||||||
|
assert.Equal(t, []string{"no-cache", "no-store"}, *cacheValue)
|
||||||
|
|
||||||
|
acceptValue := headers.GetValue("Accept")
|
||||||
|
require.NotNil(t, acceptValue)
|
||||||
|
assert.Equal(t, []string{"text/html", "application/json"}, *acceptValue)
|
||||||
|
|
||||||
|
userAgentValue := headers.GetValue("User-Agent")
|
||||||
|
require.NotNil(t, userAgentValue)
|
||||||
|
assert.Equal(t, []string{"Mozilla/5.0"}, *userAgentValue)
|
||||||
|
|
||||||
|
customValue := headers.GetValue("X-Custom")
|
||||||
|
require.NotNil(t, customValue)
|
||||||
|
assert.Equal(t, []string{"custom-value"}, *customValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append headers with multiple values", func(t *testing.T) {
|
||||||
|
headers := &Headers{}
|
||||||
|
headers.Append(
|
||||||
|
Header{Key: "Accept", Value: []string{"text/html", "application/json", "application/xml"}},
|
||||||
|
Header{Key: "Accept-Encoding", Value: []string{"gzip", "deflate"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *headers, 2)
|
||||||
|
assert.Equal(t, []string{"text/html", "application/json", "application/xml"}, (*headers)[0].Value)
|
||||||
|
assert.Equal(t, []string{"gzip", "deflate"}, (*headers)[1].Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append common HTTP headers", func(t *testing.T) {
|
||||||
|
headers := &Headers{}
|
||||||
|
headers.Append(
|
||||||
|
Header{Key: "Content-Type", Value: []string{"application/json"}},
|
||||||
|
Header{Key: "Authorization", Value: []string{"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"}},
|
||||||
|
Header{Key: "User-Agent", Value: []string{"Go-http-client/1.1"}},
|
||||||
|
Header{Key: "Accept", Value: []string{"*/*"}},
|
||||||
|
Header{Key: "X-Requested-With", Value: []string{"XMLHttpRequest"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *headers, 5)
|
||||||
|
assert.True(t, headers.Has("Content-Type"))
|
||||||
|
assert.True(t, headers.Has("Authorization"))
|
||||||
|
assert.True(t, headers.Has("User-Agent"))
|
||||||
|
assert.True(t, headers.Has("Accept"))
|
||||||
|
assert.True(t, headers.Has("X-Requested-With"))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHeaders_Parse(t *testing.T) {
|
func TestHeaders_Parse(t *testing.T) {
|
||||||
|
@@ -15,11 +15,13 @@ func (params Params) GetValue(key string) *[]string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (params *Params) Append(param Param) {
|
func (params *Params) Append(param ...Param) {
|
||||||
if item := params.GetValue(param.Key); item != nil {
|
for _, p := range param {
|
||||||
*item = append(*item, param.Value...)
|
if item := params.GetValue(p.Key); item != nil {
|
||||||
|
*item = append(*item, p.Value...)
|
||||||
} else {
|
} else {
|
||||||
*params = append(*params, param)
|
*params = append(*params, p)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -88,6 +88,39 @@ func TestParams_Append(t *testing.T) {
|
|||||||
assert.Len(t, *params, 3)
|
assert.Len(t, *params, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append multiple parameters in single call", func(t *testing.T) {
|
||||||
|
params := &Params{}
|
||||||
|
params.Append(
|
||||||
|
Param{Key: "name", Value: []string{"john"}},
|
||||||
|
Param{Key: "age", Value: []string{"25"}},
|
||||||
|
Param{Key: "city", Value: []string{"NYC"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *params, 3)
|
||||||
|
assert.Equal(t, "name", (*params)[0].Key)
|
||||||
|
assert.Equal(t, []string{"john"}, (*params)[0].Value)
|
||||||
|
assert.Equal(t, "age", (*params)[1].Key)
|
||||||
|
assert.Equal(t, []string{"25"}, (*params)[1].Value)
|
||||||
|
assert.Equal(t, "city", (*params)[2].Key)
|
||||||
|
assert.Equal(t, []string{"NYC"}, (*params)[2].Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append multiple with existing keys", func(t *testing.T) {
|
||||||
|
params := &Params{
|
||||||
|
{Key: "tags", Value: []string{"go"}},
|
||||||
|
}
|
||||||
|
params.Append(
|
||||||
|
Param{Key: "tags", Value: []string{"test"}},
|
||||||
|
Param{Key: "tags", Value: []string{"api"}},
|
||||||
|
Param{Key: "version", Value: []string{"1.0"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *params, 2)
|
||||||
|
assert.Equal(t, []string{"go", "test", "api"}, (*params)[0].Value)
|
||||||
|
assert.Equal(t, "version", (*params)[1].Key)
|
||||||
|
assert.Equal(t, []string{"1.0"}, (*params)[1].Value)
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Append multiple values at once", func(t *testing.T) {
|
t.Run("Append multiple values at once", func(t *testing.T) {
|
||||||
params := &Params{
|
params := &Params{
|
||||||
{Key: "colors", Value: []string{"red"}},
|
{Key: "colors", Value: []string{"red"}},
|
||||||
@@ -105,6 +138,60 @@ func TestParams_Append(t *testing.T) {
|
|||||||
assert.Len(t, *params, 1)
|
assert.Len(t, *params, 1)
|
||||||
assert.Equal(t, []string{""}, (*params)[0].Value)
|
assert.Equal(t, []string{""}, (*params)[0].Value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append no parameters", func(t *testing.T) {
|
||||||
|
params := &Params{
|
||||||
|
{Key: "existing", Value: []string{"value"}},
|
||||||
|
}
|
||||||
|
params.Append()
|
||||||
|
|
||||||
|
assert.Len(t, *params, 1)
|
||||||
|
assert.Equal(t, "existing", (*params)[0].Key)
|
||||||
|
assert.Equal(t, []string{"value"}, (*params)[0].Value)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append mixed new and existing keys", func(t *testing.T) {
|
||||||
|
params := &Params{
|
||||||
|
{Key: "filter", Value: []string{"active"}},
|
||||||
|
{Key: "sort", Value: []string{"name"}},
|
||||||
|
}
|
||||||
|
params.Append(
|
||||||
|
Param{Key: "filter", Value: []string{"verified"}},
|
||||||
|
Param{Key: "limit", Value: []string{"10"}},
|
||||||
|
Param{Key: "sort", Value: []string{"date"}},
|
||||||
|
Param{Key: "offset", Value: []string{"0"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *params, 4)
|
||||||
|
|
||||||
|
filterValue := params.GetValue("filter")
|
||||||
|
require.NotNil(t, filterValue)
|
||||||
|
assert.Equal(t, []string{"active", "verified"}, *filterValue)
|
||||||
|
|
||||||
|
sortValue := params.GetValue("sort")
|
||||||
|
require.NotNil(t, sortValue)
|
||||||
|
assert.Equal(t, []string{"name", "date"}, *sortValue)
|
||||||
|
|
||||||
|
limitValue := params.GetValue("limit")
|
||||||
|
require.NotNil(t, limitValue)
|
||||||
|
assert.Equal(t, []string{"10"}, *limitValue)
|
||||||
|
|
||||||
|
offsetValue := params.GetValue("offset")
|
||||||
|
require.NotNil(t, offsetValue)
|
||||||
|
assert.Equal(t, []string{"0"}, *offsetValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append parameters with multiple values", func(t *testing.T) {
|
||||||
|
params := &Params{}
|
||||||
|
params.Append(
|
||||||
|
Param{Key: "ids", Value: []string{"1", "2", "3"}},
|
||||||
|
Param{Key: "types", Value: []string{"A", "B"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Len(t, *params, 2)
|
||||||
|
assert.Equal(t, []string{"1", "2", "3"}, (*params)[0].Value)
|
||||||
|
assert.Equal(t, []string{"A", "B"}, (*params)[1].Value)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParams_Parse(t *testing.T) {
|
func TestParams_Parse(t *testing.T) {
|
||||||
|
@@ -13,8 +13,8 @@ func (proxy Proxy) String() string {
|
|||||||
|
|
||||||
type Proxies []Proxy
|
type Proxies []Proxy
|
||||||
|
|
||||||
func (proxies *Proxies) Append(proxy Proxy) {
|
func (proxies *Proxies) Append(proxy ...Proxy) {
|
||||||
*proxies = append(*proxies, proxy)
|
*proxies = append(*proxies, proxy...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (proxies *Proxies) Parse(rawValue string) error {
|
func (proxies *Proxies) Parse(rawValue string) error {
|
||||||
|
@@ -63,7 +63,7 @@ func TestProxies_Append(t *testing.T) {
|
|||||||
assert.Equal(t, "http://proxy1.example.com:8080", (*proxies)[0].String())
|
assert.Equal(t, "http://proxy1.example.com:8080", (*proxies)[0].String())
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Append multiple proxies", func(t *testing.T) {
|
t.Run("Append multiple proxies sequentially", func(t *testing.T) {
|
||||||
proxies := &Proxies{}
|
proxies := &Proxies{}
|
||||||
|
|
||||||
url1, err := url.Parse("http://proxy1.example.com:8080")
|
url1, err := url.Parse("http://proxy1.example.com:8080")
|
||||||
@@ -83,6 +83,24 @@ func TestProxies_Append(t *testing.T) {
|
|||||||
assert.Equal(t, "https://proxy3.example.com:443", (*proxies)[2].String())
|
assert.Equal(t, "https://proxy3.example.com:443", (*proxies)[2].String())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append multiple proxies in single call", func(t *testing.T) {
|
||||||
|
proxies := &Proxies{}
|
||||||
|
|
||||||
|
url1, err := url.Parse("http://proxy1.example.com:8080")
|
||||||
|
require.NoError(t, err)
|
||||||
|
url2, err := url.Parse("http://proxy2.example.com:8081")
|
||||||
|
require.NoError(t, err)
|
||||||
|
url3, err := url.Parse("https://proxy3.example.com:443")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
proxies.Append(Proxy(*url1), Proxy(*url2), Proxy(*url3))
|
||||||
|
|
||||||
|
assert.Len(t, *proxies, 3)
|
||||||
|
assert.Equal(t, "http://proxy1.example.com:8080", (*proxies)[0].String())
|
||||||
|
assert.Equal(t, "http://proxy2.example.com:8081", (*proxies)[1].String())
|
||||||
|
assert.Equal(t, "https://proxy3.example.com:443", (*proxies)[2].String())
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Append to existing proxies", func(t *testing.T) {
|
t.Run("Append to existing proxies", func(t *testing.T) {
|
||||||
existingURL, err := url.Parse("http://existing.example.com:8080")
|
existingURL, err := url.Parse("http://existing.example.com:8080")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -98,6 +116,77 @@ func TestProxies_Append(t *testing.T) {
|
|||||||
assert.Equal(t, "http://existing.example.com:8080", (*proxies)[0].String())
|
assert.Equal(t, "http://existing.example.com:8080", (*proxies)[0].String())
|
||||||
assert.Equal(t, "http://new.example.com:8081", (*proxies)[1].String())
|
assert.Equal(t, "http://new.example.com:8081", (*proxies)[1].String())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Append multiple to existing proxies", func(t *testing.T) {
|
||||||
|
existingURL, err := url.Parse("http://existing.example.com:8080")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
proxies := &Proxies{Proxy(*existingURL)}
|
||||||
|
|
||||||
|
url1, err := url.Parse("http://new1.example.com:8081")
|
||||||
|
require.NoError(t, err)
|
||||||
|
url2, err := url.Parse("http://new2.example.com:8082")
|
||||||
|
require.NoError(t, err)
|
||||||
|
url3, err := url.Parse("https://new3.example.com:443")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
proxies.Append(Proxy(*url1), Proxy(*url2), Proxy(*url3))
|
||||||
|
|
||||||
|
assert.Len(t, *proxies, 4)
|
||||||
|
assert.Equal(t, "http://existing.example.com:8080", (*proxies)[0].String())
|
||||||
|
assert.Equal(t, "http://new1.example.com:8081", (*proxies)[1].String())
|
||||||
|
assert.Equal(t, "http://new2.example.com:8082", (*proxies)[2].String())
|
||||||
|
assert.Equal(t, "https://new3.example.com:443", (*proxies)[3].String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append no proxies", func(t *testing.T) {
|
||||||
|
existingURL, err := url.Parse("http://existing.example.com:8080")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
proxies := &Proxies{Proxy(*existingURL)}
|
||||||
|
proxies.Append()
|
||||||
|
|
||||||
|
assert.Len(t, *proxies, 1)
|
||||||
|
assert.Equal(t, "http://existing.example.com:8080", (*proxies)[0].String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append mixed proxies", func(t *testing.T) {
|
||||||
|
proxies := &Proxies{}
|
||||||
|
|
||||||
|
url1, err := url.Parse("http://proxy1.example.com:8080")
|
||||||
|
require.NoError(t, err)
|
||||||
|
url2, err := url.Parse("socks5://proxy2.example.com:1080")
|
||||||
|
require.NoError(t, err)
|
||||||
|
url3, err := url.Parse("https://proxy3.example.com:443")
|
||||||
|
require.NoError(t, err)
|
||||||
|
url4, err := url.Parse("http://proxy4.example.com:3128")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
proxies.Append(Proxy(*url1), Proxy(*url2))
|
||||||
|
proxies.Append(Proxy(*url3))
|
||||||
|
proxies.Append(Proxy(*url4))
|
||||||
|
|
||||||
|
assert.Len(t, *proxies, 4)
|
||||||
|
assert.Equal(t, "http://proxy1.example.com:8080", (*proxies)[0].String())
|
||||||
|
assert.Equal(t, "socks5://proxy2.example.com:1080", (*proxies)[1].String())
|
||||||
|
assert.Equal(t, "https://proxy3.example.com:443", (*proxies)[2].String())
|
||||||
|
assert.Equal(t, "http://proxy4.example.com:3128", (*proxies)[3].String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Append proxies with authentication", func(t *testing.T) {
|
||||||
|
proxies := &Proxies{}
|
||||||
|
|
||||||
|
url1, err := url.Parse("http://user1:pass1@proxy1.example.com:8080")
|
||||||
|
require.NoError(t, err)
|
||||||
|
url2, err := url.Parse("https://user2:pass2@proxy2.example.com:443")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
proxies.Append(Proxy(*url1), Proxy(*url2))
|
||||||
|
|
||||||
|
assert.Len(t, *proxies, 2)
|
||||||
|
assert.Equal(t, "http://user1:pass1@proxy1.example.com:8080", (*proxies)[0].String())
|
||||||
|
assert.Equal(t, "https://user2:pass2@proxy2.example.com:443", (*proxies)[1].String())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProxies_Parse(t *testing.T) {
|
func TestProxies_Parse(t *testing.T) {
|
||||||
|
111
pkg/utils/parse.go
Normal file
111
pkg/utils/parse.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseString attempts to parse the input string `s` into a value of the specified type T.
|
||||||
|
// If parsing the string `s` fails for a supported type, it returns the zero value of T
|
||||||
|
// and the parsing error.
|
||||||
|
// /nolint:forcetypeassert,wrapcheck
|
||||||
|
func ParseString[
|
||||||
|
T string | int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float64 | bool | time.Duration | url.URL,
|
||||||
|
](rawValue string) (T, error) {
|
||||||
|
var value T
|
||||||
|
|
||||||
|
switch any(value).(type) {
|
||||||
|
case int:
|
||||||
|
i, err := strconv.Atoi(rawValue)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(i).(T)
|
||||||
|
case int8:
|
||||||
|
i, err := strconv.ParseInt(rawValue, 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(int8(i)).(T)
|
||||||
|
case int16:
|
||||||
|
i, err := strconv.ParseInt(rawValue, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(int16(i)).(T)
|
||||||
|
case int32:
|
||||||
|
i, err := strconv.ParseInt(rawValue, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(int32(i)).(T)
|
||||||
|
case int64:
|
||||||
|
i, err := strconv.ParseInt(rawValue, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(i).(T)
|
||||||
|
case uint:
|
||||||
|
u, err := strconv.ParseUint(rawValue, 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(uint(u)).(T)
|
||||||
|
case uint8:
|
||||||
|
u, err := strconv.ParseUint(rawValue, 10, 8)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(uint8(u)).(T)
|
||||||
|
case uint16:
|
||||||
|
u, err := strconv.ParseUint(rawValue, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(uint16(u)).(T)
|
||||||
|
case uint32:
|
||||||
|
u, err := strconv.ParseUint(rawValue, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(uint32(u)).(T)
|
||||||
|
case uint64:
|
||||||
|
u, err := strconv.ParseUint(rawValue, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(u).(T)
|
||||||
|
case float64:
|
||||||
|
f, err := strconv.ParseFloat(rawValue, 64)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(f).(T)
|
||||||
|
case bool:
|
||||||
|
b, err := strconv.ParseBool(rawValue)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(b).(T)
|
||||||
|
case string:
|
||||||
|
value = any(rawValue).(T)
|
||||||
|
case time.Duration:
|
||||||
|
d, err := time.ParseDuration(rawValue)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(d).(T)
|
||||||
|
case url.URL:
|
||||||
|
u, err := url.Parse(rawValue)
|
||||||
|
if err != nil {
|
||||||
|
return value, err
|
||||||
|
}
|
||||||
|
value = any(*u).(T)
|
||||||
|
default:
|
||||||
|
return value, fmt.Errorf("unsupported type: %T", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value, nil
|
||||||
|
}
|
528
pkg/utils/parse_test.go
Normal file
528
pkg/utils/parse_test.go
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseString(t *testing.T) {
|
||||||
|
t.Run("ParseString to string", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{"empty string", "", ""},
|
||||||
|
{"simple string", "hello", "hello"},
|
||||||
|
{"string with spaces", "hello world", "hello world"},
|
||||||
|
{"numeric string", "123", "123"},
|
||||||
|
{"special characters", "!@#$%^&*()", "!@#$%^&*()"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[string](test.input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to int", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected int
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"positive int", "42", 42, false},
|
||||||
|
{"negative int", "-42", -42, false},
|
||||||
|
{"zero", "0", 0, false},
|
||||||
|
{"invalid int", "abc", 0, true},
|
||||||
|
{"float string", "3.14", 0, true},
|
||||||
|
{"empty string", "", 0, true},
|
||||||
|
{"overflow string", "99999999999999999999", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[int](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to int8", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected int8
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"valid int8", "127", 127, false},
|
||||||
|
{"min int8", "-128", -128, false},
|
||||||
|
{"overflow int8", "128", 0, true},
|
||||||
|
{"underflow int8", "-129", 0, true},
|
||||||
|
{"invalid", "abc", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[int8](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to int16", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected int16
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"valid int16", "32767", 32767, false},
|
||||||
|
{"min int16", "-32768", -32768, false},
|
||||||
|
{"overflow int16", "32768", 0, true},
|
||||||
|
{"underflow int16", "-32769", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[int16](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to int32", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected int32
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"valid int32", "2147483647", 2147483647, false},
|
||||||
|
{"min int32", "-2147483648", -2147483648, false},
|
||||||
|
{"overflow int32", "2147483648", 0, true},
|
||||||
|
{"underflow int32", "-2147483649", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[int32](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to int64", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected int64
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"valid int64", "9223372036854775807", 9223372036854775807, false},
|
||||||
|
{"min int64", "-9223372036854775808", -9223372036854775808, false},
|
||||||
|
{"large number", "123456789012345", 123456789012345, false},
|
||||||
|
{"invalid", "not a number", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[int64](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to uint", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected uint
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"valid uint", "42", 42, false},
|
||||||
|
{"zero", "0", 0, false},
|
||||||
|
{"large uint", "4294967295", 4294967295, false},
|
||||||
|
{"negative", "-1", 0, true},
|
||||||
|
{"invalid", "abc", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[uint](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to uint8", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected uint8
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"valid uint8", "255", 255, false},
|
||||||
|
{"min uint8", "0", 0, false},
|
||||||
|
{"overflow uint8", "256", 0, true},
|
||||||
|
{"negative", "-1", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[uint8](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to uint16", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected uint16
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"valid uint16", "65535", 65535, false},
|
||||||
|
{"min uint16", "0", 0, false},
|
||||||
|
{"overflow uint16", "65536", 0, true},
|
||||||
|
{"negative", "-1", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[uint16](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to uint32", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected uint32
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"valid uint32", "4294967295", 4294967295, false},
|
||||||
|
{"min uint32", "0", 0, false},
|
||||||
|
{"overflow uint32", "4294967296", 0, true},
|
||||||
|
{"negative", "-1", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[uint32](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to uint64", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected uint64
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"valid uint64", "18446744073709551615", 18446744073709551615, false},
|
||||||
|
{"min uint64", "0", 0, false},
|
||||||
|
{"large number", "123456789012345", 123456789012345, false},
|
||||||
|
{"negative", "-1", 0, true},
|
||||||
|
{"invalid", "not a number", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[uint64](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to float64", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected float64
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"integer", "42", 42.0, false},
|
||||||
|
{"decimal", "3.14159", 3.14159, false},
|
||||||
|
{"negative", "-2.5", -2.5, false},
|
||||||
|
{"scientific notation", "1.23e10", 1.23e10, false},
|
||||||
|
{"zero", "0", 0.0, false},
|
||||||
|
{"invalid", "not a number", 0, true},
|
||||||
|
{"empty", "", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[float64](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.InDelta(t, test.expected, result, 0.0001)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to bool", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected bool
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"true lowercase", "true", true, false},
|
||||||
|
{"True mixed case", "True", true, false},
|
||||||
|
{"TRUE uppercase", "TRUE", true, false},
|
||||||
|
{"1 as true", "1", true, false},
|
||||||
|
{"false lowercase", "false", false, false},
|
||||||
|
{"False mixed case", "False", false, false},
|
||||||
|
{"FALSE uppercase", "FALSE", false, false},
|
||||||
|
{"0 as false", "0", false, false},
|
||||||
|
{"invalid", "yes", false, true},
|
||||||
|
{"empty", "", false, true},
|
||||||
|
{"numeric non-binary", "2", false, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[bool](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to time.Duration", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected time.Duration
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{"seconds", "10s", 10 * time.Second, false},
|
||||||
|
{"minutes", "5m", 5 * time.Minute, false},
|
||||||
|
{"hours", "2h", 2 * time.Hour, false},
|
||||||
|
{"combined", "1h30m45s", time.Hour + 30*time.Minute + 45*time.Second, false},
|
||||||
|
{"milliseconds", "500ms", 500 * time.Millisecond, false},
|
||||||
|
{"microseconds", "100us", 100 * time.Microsecond, false},
|
||||||
|
{"nanoseconds", "50ns", 50 * time.Nanosecond, false},
|
||||||
|
{"negative", "-5s", -5 * time.Second, false},
|
||||||
|
{"invalid", "5x", 0, true},
|
||||||
|
{"empty", "", 0, true},
|
||||||
|
{"no unit", "100", 0, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[time.Duration](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, test.expected, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ParseString to url.URL", func(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
checkFunc func(t *testing.T, u url.URL)
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "http URL",
|
||||||
|
input: "http://example.com",
|
||||||
|
checkFunc: func(t *testing.T, u url.URL) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Equal(t, "http", u.Scheme)
|
||||||
|
assert.Equal(t, "example.com", u.Host)
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "https URL with path",
|
||||||
|
input: "https://example.com/path/to/resource",
|
||||||
|
checkFunc: func(t *testing.T, u url.URL) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Equal(t, "https", u.Scheme)
|
||||||
|
assert.Equal(t, "example.com", u.Host)
|
||||||
|
assert.Equal(t, "/path/to/resource", u.Path)
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL with query parameters",
|
||||||
|
input: "https://example.com/search?q=test&page=1",
|
||||||
|
checkFunc: func(t *testing.T, u url.URL) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Equal(t, "https", u.Scheme)
|
||||||
|
assert.Equal(t, "example.com", u.Host)
|
||||||
|
assert.Equal(t, "/search", u.Path)
|
||||||
|
assert.Equal(t, "q=test&page=1", u.RawQuery)
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL with port",
|
||||||
|
input: "http://localhost:8080/api",
|
||||||
|
checkFunc: func(t *testing.T, u url.URL) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Equal(t, "http", u.Scheme)
|
||||||
|
assert.Equal(t, "localhost:8080", u.Host)
|
||||||
|
assert.Equal(t, "/api", u.Path)
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "URL with fragment",
|
||||||
|
input: "https://example.com/page#section",
|
||||||
|
checkFunc: func(t *testing.T, u url.URL) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Equal(t, "https", u.Scheme)
|
||||||
|
assert.Equal(t, "example.com", u.Host)
|
||||||
|
assert.Equal(t, "/page", u.Path)
|
||||||
|
assert.Equal(t, "section", u.Fragment)
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "relative path",
|
||||||
|
input: "/path/to/resource",
|
||||||
|
checkFunc: func(t *testing.T, u url.URL) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Empty(t, u.Scheme)
|
||||||
|
assert.Empty(t, u.Host)
|
||||||
|
assert.Equal(t, "/path/to/resource", u.Path)
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty string",
|
||||||
|
input: "",
|
||||||
|
checkFunc: func(t *testing.T, u url.URL) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Empty(t, u.String())
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
result, err := ParseString[url.URL](test.input)
|
||||||
|
if test.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
if test.checkFunc != nil {
|
||||||
|
test.checkFunc(t, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Edge cases", func(t *testing.T) {
|
||||||
|
t.Run("whitespace handling for numeric types", func(t *testing.T) {
|
||||||
|
result, err := ParseString[int](" 42 ")
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, 0, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("leading zeros for int", func(t *testing.T) {
|
||||||
|
result, err := ParseString[int]("007")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 7, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("plus sign for positive numbers", func(t *testing.T) {
|
||||||
|
result, err := ParseString[int]("+42")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 42, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("case sensitivity for bool", func(t *testing.T) {
|
||||||
|
testCases := []string{"t", "T", "f", "F"}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
result, err := ParseString[bool](tc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
if tc == "t" || tc == "T" {
|
||||||
|
assert.True(t, result)
|
||||||
|
} else {
|
||||||
|
assert.False(t, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
Reference in New Issue
Block a user