3 Commits

Author SHA1 Message Date
1eb969480b update 'Merge' of the 'Config' 2025-09-04 23:13:05 +04:00
7e89fa174b update types 2025-09-04 23:00:57 +04:00
81383d1ea7 add env parser 2025-09-04 22:43:52 +04:00
20 changed files with 2072 additions and 54 deletions

View File

@@ -4,16 +4,27 @@ import (
"fmt"
"os"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/config/parser"
"github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils"
"github.com/jedib0t/go-pretty/v6/text"
"github.com/k0kubun/pp/v3"
)
func main() {
cliParser := config.NewConfigCLIParser(os.Args)
cfg, err := cliParser.Parse()
envParser := parser.NewConfigENVParser("DODO")
envConfig, err := envParser.Parse()
_ = utils.HandleErrorOrDie(err,
utils.OnCustomError(func(err types.FieldParseErrors) error {
printValidationErrors("ENV", err.Errors...)
fmt.Println()
os.Exit(1)
return nil
}),
)
cliParser := parser.NewConfigCLIParser(os.Args)
cliConf, err := cliParser.Parse()
_ = utils.HandleErrorOrDie(err,
utils.OnSentinelError(types.ErrCLINoArgs, func(err error) error {
cliParser.PrintHelp()
@@ -35,7 +46,9 @@ func main() {
}),
)
fmt.Println(cfg)
envConfig.Merge(cliConf)
pp.Println(cliConf) //nolint
pp.Println(envConfig) //nolint
}
func printValidationErrors(parserName string, errors ...types.FieldParseError) {

5
go.mod
View File

@@ -4,15 +4,18 @@ go 1.25
require (
github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/k0kubun/pp/v3 v3.5.0
github.com/stretchr/testify v1.10.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/text v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

11
go.sum
View File

@@ -2,6 +2,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/k0kubun/pp/v3 v3.5.0 h1:iYNlYA5HJAJvkD4ibuf9c8y6SHM0QFhaBuCqm1zHp0w=
github.com/k0kubun/pp/v3 v3.5.0/go.mod h1:5lzno5ZZeEeTV/Ky6vs3g6d1U3WarDrH8k240vMtGro=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -11,10 +17,11 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -49,7 +49,7 @@ func NewConfig() *Config {
return &Config{}
}
func (config *Config) MergeConfig(newConfig *Config) {
func (config *Config) Merge(newConfig *Config) {
config.Files = append(config.Files, newConfig.Files...)
if newConfig.Method != nil {
config.Method = newConfig.Method
@@ -76,19 +76,19 @@ func (config *Config) MergeConfig(newConfig *Config) {
config.SkipVerify = newConfig.SkipVerify
}
if len(newConfig.Params) != 0 {
config.Params = newConfig.Params
config.Params.Append(newConfig.Params...)
}
if len(newConfig.Headers) != 0 {
config.Headers = newConfig.Headers
config.Headers.Append(newConfig.Headers...)
}
if len(newConfig.Cookies) != 0 {
config.Cookies = newConfig.Cookies
config.Cookies.Append(newConfig.Cookies...)
}
if len(newConfig.Bodies) != 0 {
config.Bodies = newConfig.Bodies
config.Bodies.Append(newConfig.Bodies...)
}
if len(newConfig.Proxies) != 0 {
config.Proxies = newConfig.Proxies
config.Proxies.Append(newConfig.Proxies...)
}
}

View File

@@ -56,7 +56,7 @@ func TestMergeConfig(t *testing.T) {
Proxies: types.Proxies{},
}
config.MergeConfig(newConfig)
config.Merge(newConfig)
assert.Equal(t, "POST", *config.Method)
assert.Equal(t, newURL, config.URL)
@@ -66,10 +66,10 @@ func TestMergeConfig(t *testing.T) {
assert.Equal(t, newDuration, *config.Duration)
assert.True(t, *config.Yes)
assert.True(t, *config.SkipVerify)
assert.Equal(t, types.Params{{Key: "new", Value: []string{"value"}}}, config.Params)
assert.Equal(t, types.Headers{{Key: "New-Header", Value: []string{"new"}}}, config.Headers)
assert.Equal(t, types.Cookies{{Key: "newCookie", Value: []string{"newValue"}}}, config.Cookies)
assert.Equal(t, types.Bodies{types.Body("new body")}, config.Bodies)
assert.Equal(t, types.Params{{Key: "old", Value: []string{"value"}}, {Key: "new", Value: []string{"value"}}}, config.Params)
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: "oldCookie", Value: []string{"oldValue"}}, {Key: "newCookie", Value: []string{"newValue"}}}, config.Cookies)
assert.Equal(t, types.Bodies{types.Body("old body"), types.Body("new body")}, config.Bodies)
assert.Empty(t, config.Proxies)
})
@@ -93,7 +93,7 @@ func TestMergeConfig(t *testing.T) {
DodosCount: utils.ToPtr(uint(10)),
}
config.MergeConfig(newConfig)
config.Merge(newConfig)
assert.Equal(t, "GET", *config.Method, "Method should remain unchanged")
assert.Equal(t, newURL, config.URL, "URL should be updated")
@@ -127,7 +127,7 @@ func TestMergeConfig(t *testing.T) {
}
originalConfigCopy := *config
config.MergeConfig(newConfig)
config.Merge(newConfig)
assert.Equal(t, originalConfigCopy.Method, config.Method)
assert.Equal(t, originalConfigCopy.URL, config.URL)
@@ -157,7 +157,7 @@ func TestMergeConfig(t *testing.T) {
Proxies: types.Proxies{},
}
config.MergeConfig(newConfig)
config.Merge(newConfig)
assert.Equal(t, []types.ConfigFile{*configFile}, config.Files, "Empty Files should not override")
assert.Equal(t, types.Params{{Key: "original", Value: []string{"value"}}}, config.Params, "Empty Params should not override")
@@ -182,7 +182,7 @@ func TestMergeConfig(t *testing.T) {
Method: utils.ToPtr("POST"),
}
config.MergeConfig(newConfig)
config.Merge(newConfig)
assert.Equal(t, "POST", *config.Method, "Method should be updated")
assert.Equal(t, []types.ConfigFile{*configFile1, *configFile2}, config.Files, "Files should be appended")
@@ -213,7 +213,7 @@ func TestMergeConfig(t *testing.T) {
Proxies: types.Proxies{},
}
config.MergeConfig(newConfig)
config.Merge(newConfig)
assert.Equal(t, "POST", *config.Method)
assert.Equal(t, newURL, config.URL)
@@ -352,3 +352,176 @@ func TestSetDefaults(t *testing.T) {
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")
})
}

View File

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

View File

@@ -1,4 +1,4 @@
package config
package parser
import (
"errors"
@@ -8,6 +8,7 @@ import (
"strings"
"time"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils"
)
@@ -52,6 +53,8 @@ Flags:
-x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080")
-skip-verify bool Skip SSL/TLS certificate verification (default %v)`
var _ IParser = ConfigCLIParser{}
type ConfigCLIParser struct {
args []string
}
@@ -79,13 +82,13 @@ func (arg *stringSliceArg) Set(value string) error {
// - types.ErrCLINoArgs
// - types.CLIUnexpectedArgsError
// - types.FieldParseErrors
func (parser *ConfigCLIParser) Parse() (*Config, error) {
func (parser ConfigCLIParser) Parse() (*config.Config, error) {
flagSet := flag.NewFlagSet("dodo", flag.ExitOnError)
flagSet.Usage = func() { parser.PrintHelp() }
var (
config = &Config{}
config = &config.Config{}
configFiles = stringSliceArg{}
yes bool
skipVerify bool
@@ -259,14 +262,14 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
return config, nil
}
func (parser *ConfigCLIParser) PrintHelp() {
func (parser ConfigCLIParser) PrintHelp() {
fmt.Printf(
cliUsageText+"\n",
Defaults.Yes,
Defaults.DodosCount,
Defaults.RequestTimeout,
Defaults.Method,
Defaults.SkipVerify,
config.Defaults.Yes,
config.Defaults.DodosCount,
config.Defaults.RequestTimeout,
config.Defaults.Method,
config.Defaults.SkipVerify,
)
}

View File

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

236
pkg/config/parser/env.go Normal file
View 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))
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ func (body Body) String() string {
type Bodies []Body
func (bodies *Bodies) Append(body Body) {
*bodies = append(*bodies, body)
func (bodies *Bodies) Append(body ...Body) {
*bodies = append(*bodies, body...)
}
func (bodies *Bodies) Parse(rawValues ...string) {

View File

@@ -49,6 +49,16 @@ func TestBodies_Append(t *testing.T) {
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) {
bodies := &Bodies{Body("existing")}
bodies.Append(Body("new"))
@@ -58,6 +68,17 @@ func TestBodies_Append(t *testing.T) {
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) {
bodies := &Bodies{}
bodies.Append(Body(""))
@@ -65,6 +86,29 @@ func TestBodies_Append(t *testing.T) {
assert.Len(t, *bodies, 1)
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) {

View File

@@ -15,11 +15,13 @@ func (cookies Cookies) GetValue(key string) *[]string {
return nil
}
func (cookies *Cookies) Append(cookie Cookie) {
if item := cookies.GetValue(cookie.Key); item != nil {
*item = append(*item, cookie.Value...)
} else {
*cookies = append(*cookies, cookie)
func (cookies *Cookies) Append(cookie ...Cookie) {
for _, c := range cookie {
if item := cookies.GetValue(c.Key); item != nil {
*item = append(*item, c.Value...)
} else {
*cookies = append(*cookies, c)
}
}
}

View File

@@ -88,6 +88,39 @@ func TestCookies_Append(t *testing.T) {
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) {
cookies := &Cookies{
{Key: "tags", Value: []string{"tag1"}},
@@ -105,6 +138,88 @@ func TestCookies_Append(t *testing.T) {
assert.Len(t, *cookies, 1)
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) {

View File

@@ -25,11 +25,13 @@ func (headers Headers) GetValue(key string) *[]string {
return nil
}
func (headers *Headers) Append(header Header) {
if item := headers.GetValue(header.Key); item != nil {
*item = append(*item, header.Value...)
} else {
*headers = append(*headers, header)
func (headers *Headers) Append(header ...Header) {
for _, h := range header {
if item := headers.GetValue(h.Key); item != nil {
*item = append(*item, h.Value...)
} else {
*headers = append(*headers, h)
}
}
}

View File

@@ -125,6 +125,39 @@ func TestHeaders_Append(t *testing.T) {
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) {
headers := &Headers{
{Key: "Accept-Language", Value: []string{"en"}},
@@ -142,6 +175,78 @@ func TestHeaders_Append(t *testing.T) {
assert.Len(t, *headers, 1)
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) {

View File

@@ -15,11 +15,13 @@ func (params Params) GetValue(key string) *[]string {
return nil
}
func (params *Params) Append(param Param) {
if item := params.GetValue(param.Key); item != nil {
*item = append(*item, param.Value...)
} else {
*params = append(*params, param)
func (params *Params) Append(param ...Param) {
for _, p := range param {
if item := params.GetValue(p.Key); item != nil {
*item = append(*item, p.Value...)
} else {
*params = append(*params, p)
}
}
}

View File

@@ -88,6 +88,39 @@ func TestParams_Append(t *testing.T) {
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) {
params := &Params{
{Key: "colors", Value: []string{"red"}},
@@ -105,6 +138,60 @@ func TestParams_Append(t *testing.T) {
assert.Len(t, *params, 1)
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) {

View File

@@ -13,8 +13,8 @@ func (proxy Proxy) String() string {
type Proxies []Proxy
func (proxies *Proxies) Append(proxy Proxy) {
*proxies = append(*proxies, proxy)
func (proxies *Proxies) Append(proxy ...Proxy) {
*proxies = append(*proxies, proxy...)
}
func (proxies *Proxies) Parse(rawValue string) error {

View File

@@ -63,7 +63,7 @@ func TestProxies_Append(t *testing.T) {
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{}
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())
})
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) {
existingURL, err := url.Parse("http://existing.example.com:8080")
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://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) {