mirror of
https://github.com/aykhans/dodo.git
synced 2025-09-03 18:03:34 +00:00
Here we go again...
This commit is contained in:
250
pkg/config/cli.go
Normal file
250
pkg/config/cli.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aykhans/dodo/pkg/types"
|
||||
"github.com/aykhans/dodo/pkg/utils"
|
||||
)
|
||||
|
||||
const cliUsageText = `Usage:
|
||||
dodo [flags]
|
||||
|
||||
Examples:
|
||||
|
||||
Simple usage:
|
||||
dodo -u https://example.com -o 1m
|
||||
|
||||
Usage with config file:
|
||||
dodo -f /path/to/config/file/config.json
|
||||
|
||||
Usage with all flags:
|
||||
dodo -f /path/to/config/file/config.json \
|
||||
-u https://example.com -m POST \
|
||||
-d 10 -r 1000 -o 3m -t 3s \
|
||||
-b "body1" -body "body2" \
|
||||
-H "header1:value1" -header "header2:value2" \
|
||||
-p "param1=value1" -param "param2=value2" \
|
||||
-c "cookie1=value1" -cookie "cookie2=value2" \
|
||||
-x "http://proxy.example.com:8080" -proxy "socks5://proxy2.example.com:8080" \
|
||||
-skip-verify -y
|
||||
|
||||
Flags:
|
||||
-h, -help help for dodo
|
||||
-v, -version version for dodo
|
||||
-y, -yes bool Answer yes to all questions (default %v)
|
||||
-f, -config-file string Path to the local config file or http(s) URL of the config file
|
||||
-d, -dodos uint Number of dodos(threads) (default %d)
|
||||
-r, -requests uint Number of total requests
|
||||
-o, -duration Time Maximum duration for the test (e.g. 30s, 1m, 5h)
|
||||
-t, -timeout Time Timeout for each request (e.g. 400ms, 15s, 1m10s) (default %v)
|
||||
-u, -url string URL for stress testing
|
||||
-m, -method string HTTP Method for the request (default %s)
|
||||
-b, -body [string] Body for the request (e.g. "body text")
|
||||
-p, -param [string] Parameter for the request (e.g. "key1=value1")
|
||||
-H, -header [string] Header for the request (e.g. "key1: value1")
|
||||
-c, -cookie [string] Cookie for the request (e.g. "key1=value1")
|
||||
-x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080")
|
||||
-skip-verify bool Skip SSL/TLS certificate verification (default %v)`
|
||||
|
||||
type ConfigCLIParser struct {
|
||||
args []string
|
||||
}
|
||||
|
||||
func NewConfigCLIParser(args []string) *ConfigCLIParser {
|
||||
if args == nil {
|
||||
args = []string{}
|
||||
}
|
||||
return &ConfigCLIParser{args}
|
||||
}
|
||||
|
||||
type stringSliceArg []string
|
||||
|
||||
func (arg *stringSliceArg) String() string {
|
||||
return strings.Join(*arg, ",")
|
||||
}
|
||||
|
||||
func (arg *stringSliceArg) Set(value string) error {
|
||||
*arg = append(*arg, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse parses command-line arguments into a Config object.
|
||||
// It can return the following errors:
|
||||
// - types.ErrCLINoArgs
|
||||
// - types.CLIUnexpectedArgsError
|
||||
// - types.FieldParseErrors
|
||||
func (parser *ConfigCLIParser) Parse() (*Config, error) {
|
||||
flagSet := flag.NewFlagSet("dodo", flag.ExitOnError)
|
||||
|
||||
flagSet.Usage = func() { parser.PrintHelp() }
|
||||
|
||||
var (
|
||||
config = &Config{}
|
||||
yes bool
|
||||
skipVerify bool
|
||||
method string
|
||||
urlInput string
|
||||
dodosCount uint
|
||||
requestCount uint
|
||||
duration time.Duration
|
||||
timeout time.Duration
|
||||
params = stringSliceArg{}
|
||||
headers = stringSliceArg{}
|
||||
cookies = stringSliceArg{}
|
||||
bodies = stringSliceArg{}
|
||||
proxies = stringSliceArg{}
|
||||
)
|
||||
|
||||
{
|
||||
flagSet.BoolVar(&yes, "yes", false, "Answer yes to all questions")
|
||||
flagSet.BoolVar(&yes, "y", false, "Answer yes to all questions")
|
||||
|
||||
flagSet.BoolVar(&skipVerify, "skip-verify", false, "Skip SSL/TLS certificate verification")
|
||||
|
||||
flagSet.StringVar(&method, "method", "", "HTTP Method")
|
||||
flagSet.StringVar(&method, "m", "", "HTTP Method")
|
||||
|
||||
flagSet.StringVar(&urlInput, "url", "", "URL to send the request")
|
||||
flagSet.StringVar(&urlInput, "u", "", "URL to send the request")
|
||||
|
||||
flagSet.UintVar(&dodosCount, "dodos", 0, "Number of dodos(threads)")
|
||||
flagSet.UintVar(&dodosCount, "d", 0, "Number of dodos(threads)")
|
||||
|
||||
flagSet.UintVar(&requestCount, "requests", 0, "Number of total requests")
|
||||
flagSet.UintVar(&requestCount, "r", 0, "Number of total requests")
|
||||
|
||||
flagSet.DurationVar(&duration, "duration", 0, "Maximum duration of the test")
|
||||
flagSet.DurationVar(&duration, "o", 0, "Maximum duration of the test")
|
||||
|
||||
flagSet.DurationVar(&timeout, "timeout", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
|
||||
flagSet.DurationVar(&timeout, "t", 0, "Timeout for each request (e.g. 400ms, 15s, 1m10s)")
|
||||
|
||||
flagSet.Var(¶ms, "param", "URL parameter to send with the request")
|
||||
flagSet.Var(¶ms, "p", "URL parameter to send with the request")
|
||||
|
||||
flagSet.Var(&headers, "header", "Header to send with the request")
|
||||
flagSet.Var(&headers, "H", "Header to send with the request")
|
||||
|
||||
flagSet.Var(&cookies, "cookie", "Cookie to send with the request")
|
||||
flagSet.Var(&cookies, "c", "Cookie to send with the request")
|
||||
|
||||
flagSet.Var(&bodies, "body", "Body to send with the request")
|
||||
flagSet.Var(&bodies, "b", "Body to send with the request")
|
||||
|
||||
flagSet.Var(&proxies, "proxy", "Proxy to use for the request")
|
||||
flagSet.Var(&proxies, "x", "Proxy to use for the request")
|
||||
}
|
||||
|
||||
// Parse the specific arguments provided to the parser, skipping the program name.
|
||||
if err := flagSet.Parse(parser.args[1:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Check if no flags were set and no non-flag arguments were provided.
|
||||
// This covers cases where `dodo` is run without any meaningful arguments.
|
||||
if flagSet.NFlag() == 0 && len(flagSet.Args()) == 0 {
|
||||
return nil, types.ErrCLINoArgs
|
||||
}
|
||||
|
||||
// Check for any unexpected non-flag arguments remaining after parsing.
|
||||
if args := flagSet.Args(); len(args) > 0 {
|
||||
return nil, types.NewCLIUnexpectedArgsError(args)
|
||||
}
|
||||
|
||||
var fieldParseErrors []types.FieldParseError
|
||||
// Iterate over flags that were explicitly set on the command line.
|
||||
flagSet.Visit(func(flagVar *flag.Flag) {
|
||||
switch flagVar.Name {
|
||||
case "yes", "y":
|
||||
config.Yes = utils.ToPtr(yes)
|
||||
case "skip-verify":
|
||||
config.SkipVerify = utils.ToPtr(skipVerify)
|
||||
case "method", "m":
|
||||
config.Method = utils.ToPtr(method)
|
||||
case "url", "u":
|
||||
urlParsed, err := url.Parse(urlInput)
|
||||
if err != nil {
|
||||
fieldParseErrors = append(fieldParseErrors, *types.NewFieldParseError("url", err))
|
||||
} else {
|
||||
config.URL = urlParsed
|
||||
}
|
||||
case "dodos", "d":
|
||||
config.DodosCount = utils.ToPtr(dodosCount)
|
||||
case "requests", "r":
|
||||
config.RequestCount = utils.ToPtr(requestCount)
|
||||
case "duration", "o":
|
||||
config.Duration = utils.ToPtr(duration)
|
||||
case "timeout", "t":
|
||||
config.Timeout = utils.ToPtr(timeout)
|
||||
case "param", "p":
|
||||
config.Params.Parse(params...)
|
||||
case "header", "H":
|
||||
config.Headers.Parse(headers...)
|
||||
case "cookie", "c":
|
||||
config.Cookies.Parse(cookies...)
|
||||
case "body", "b":
|
||||
config.Bodies.Parse(bodies...)
|
||||
case "proxy", "x":
|
||||
for i, proxy := range proxies {
|
||||
err := config.Proxies.Parse(proxy)
|
||||
if err != nil {
|
||||
fieldParseErrors = append(
|
||||
fieldParseErrors,
|
||||
*types.NewFieldParseError(fmt.Sprintf("proxy[%d]", i), err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if len(fieldParseErrors) > 0 {
|
||||
return nil, types.NewFieldParseErrors(fieldParseErrors)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (parser *ConfigCLIParser) PrintHelp() {
|
||||
fmt.Printf(
|
||||
cliUsageText+"\n",
|
||||
Defaults.Yes,
|
||||
Defaults.DodosCount,
|
||||
Defaults.RequestTimeout,
|
||||
Defaults.Method,
|
||||
Defaults.SkipVerify,
|
||||
)
|
||||
}
|
||||
|
||||
// CLIYesOrNoReader reads a yes or no answer from the command line.
|
||||
// It prompts the user with the given message and default value,
|
||||
// and returns true if the user answers "y" or "Y", and false otherwise.
|
||||
// If there is an error while reading the input, it returns false.
|
||||
// If the user simply presses enter without providing any input,
|
||||
// it returns the default value specified by the `def` parameter.
|
||||
func CLIYesOrNoReader(message string, def bool) bool {
|
||||
var answer string
|
||||
defaultMessage := "Y/n"
|
||||
|
||||
if !def {
|
||||
defaultMessage = "y/N"
|
||||
}
|
||||
|
||||
fmt.Printf("%s [%s]: ", message, defaultMessage)
|
||||
if _, err := fmt.Scanln(&answer); err != nil {
|
||||
if err.Error() == "unexpected newline" {
|
||||
return def
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if answer == "" {
|
||||
return def
|
||||
}
|
||||
|
||||
return answer == "y" || answer == "Y"
|
||||
}
|
679
pkg/config/cli_test.go
Normal file
679
pkg/config/cli_test.go
Normal file
@@ -0,0 +1,679 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aykhans/dodo/pkg/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewConfigCLIParser(t *testing.T) {
|
||||
t.Run("NewConfigCLIParser with valid args", func(t *testing.T) {
|
||||
args := []string{"dodo", "-u", "https://example.com"}
|
||||
parser := NewConfigCLIParser(args)
|
||||
|
||||
require.NotNil(t, parser)
|
||||
assert.Equal(t, args, parser.args)
|
||||
})
|
||||
|
||||
t.Run("NewConfigCLIParser with nil args", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser(nil)
|
||||
|
||||
require.NotNil(t, parser)
|
||||
assert.Equal(t, []string{}, parser.args)
|
||||
})
|
||||
|
||||
t.Run("NewConfigCLIParser with empty args", func(t *testing.T) {
|
||||
args := []string{}
|
||||
parser := NewConfigCLIParser(args)
|
||||
|
||||
require.NotNil(t, parser)
|
||||
assert.Equal(t, args, parser.args)
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringSliceArg(t *testing.T) {
|
||||
t.Run("stringSliceArg String method", func(t *testing.T) {
|
||||
arg := stringSliceArg{"value1", "value2", "value3"}
|
||||
assert.Equal(t, "value1,value2,value3", arg.String())
|
||||
})
|
||||
|
||||
t.Run("stringSliceArg String with empty slice", func(t *testing.T) {
|
||||
arg := stringSliceArg{}
|
||||
assert.Empty(t, arg.String())
|
||||
})
|
||||
|
||||
t.Run("stringSliceArg String with single value", func(t *testing.T) {
|
||||
arg := stringSliceArg{"single"}
|
||||
assert.Equal(t, "single", arg.String())
|
||||
})
|
||||
|
||||
t.Run("stringSliceArg Set method", func(t *testing.T) {
|
||||
arg := &stringSliceArg{}
|
||||
|
||||
err := arg.Set("first")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, stringSliceArg{"first"}, *arg)
|
||||
|
||||
err = arg.Set("second")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, stringSliceArg{"first", "second"}, *arg)
|
||||
})
|
||||
|
||||
t.Run("stringSliceArg Set with empty string", func(t *testing.T) {
|
||||
arg := &stringSliceArg{}
|
||||
|
||||
err := arg.Set("")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, stringSliceArg{""}, *arg)
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigCLIParser_Parse(t *testing.T) {
|
||||
t.Run("Parse with no arguments returns ErrCLINoArgs", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
assert.Nil(t, config)
|
||||
require.ErrorIs(t, err, types.ErrCLINoArgs)
|
||||
})
|
||||
|
||||
t.Run("Parse with unexpected arguments returns CLIUnexpectedArgsError", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "unexpected", "args"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
assert.Nil(t, config)
|
||||
var cliErr types.CLIUnexpectedArgsError
|
||||
require.ErrorAs(t, err, &cliErr)
|
||||
assert.Equal(t, []string{"unexpected", "args"}, cliErr.Args)
|
||||
})
|
||||
|
||||
t.Run("Parse with valid URL", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-u", "https://example.com"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.URL)
|
||||
assert.Equal(t, "https://example.com", config.URL.String())
|
||||
})
|
||||
|
||||
t.Run("Parse with invalid URL returns FieldParseErrors", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-u", "://invalid-url"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
assert.Nil(t, config)
|
||||
var fieldErr types.FieldParseErrors
|
||||
require.ErrorAs(t, err, &fieldErr)
|
||||
assert.Len(t, fieldErr.Errors, 1)
|
||||
assert.Equal(t, "url", fieldErr.Errors[0].Field)
|
||||
})
|
||||
|
||||
t.Run("Parse with method flag", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-m", "POST"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Method)
|
||||
assert.Equal(t, "POST", *config.Method)
|
||||
})
|
||||
|
||||
t.Run("Parse with yes flag", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-y"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Yes)
|
||||
assert.True(t, *config.Yes)
|
||||
})
|
||||
|
||||
t.Run("Parse with skip-verify flag", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-skip-verify"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.SkipVerify)
|
||||
assert.True(t, *config.SkipVerify)
|
||||
})
|
||||
|
||||
t.Run("Parse with dodos count", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-d", "5"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.DodosCount)
|
||||
assert.Equal(t, uint(5), *config.DodosCount)
|
||||
})
|
||||
|
||||
t.Run("Parse with request count", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-r", "1000"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.RequestCount)
|
||||
assert.Equal(t, uint(1000), *config.RequestCount)
|
||||
})
|
||||
|
||||
t.Run("Parse with duration", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-o", "5m"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Duration)
|
||||
assert.Equal(t, 5*time.Minute, *config.Duration)
|
||||
})
|
||||
|
||||
t.Run("Parse with timeout", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-t", "30s"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Timeout)
|
||||
assert.Equal(t, 30*time.Second, *config.Timeout)
|
||||
})
|
||||
|
||||
t.Run("Parse with parameters", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-p", "key1=value1", "-p", "key2=value2"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
assert.Len(t, config.Params, 2)
|
||||
assert.Equal(t, "key1", config.Params[0].Key)
|
||||
assert.Equal(t, []string{"value1"}, config.Params[0].Value)
|
||||
assert.Equal(t, "key2", config.Params[1].Key)
|
||||
assert.Equal(t, []string{"value2"}, config.Params[1].Value)
|
||||
})
|
||||
|
||||
t.Run("Parse with headers", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-H", "Content-Type: application/json", "-H", "Authorization: Bearer token"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
assert.Len(t, config.Headers, 2)
|
||||
assert.Equal(t, "Content-Type", config.Headers[0].Key)
|
||||
assert.Equal(t, []string{"application/json"}, config.Headers[0].Value)
|
||||
assert.Equal(t, "Authorization", config.Headers[1].Key)
|
||||
assert.Equal(t, []string{"Bearer token"}, config.Headers[1].Value)
|
||||
})
|
||||
|
||||
t.Run("Parse with cookies", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-c", "session=abc123", "-c", "user=john"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
assert.Len(t, config.Cookies, 2)
|
||||
assert.Equal(t, "session", config.Cookies[0].Key)
|
||||
assert.Equal(t, []string{"abc123"}, config.Cookies[0].Value)
|
||||
assert.Equal(t, "user", config.Cookies[1].Key)
|
||||
assert.Equal(t, []string{"john"}, config.Cookies[1].Value)
|
||||
})
|
||||
|
||||
t.Run("Parse with bodies", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-b", "body1", "-b", "body2"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
assert.Len(t, config.Bodies, 2)
|
||||
assert.Equal(t, types.Body("body1"), config.Bodies[0])
|
||||
assert.Equal(t, types.Body("body2"), config.Bodies[1])
|
||||
})
|
||||
|
||||
t.Run("Parse with valid proxies", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-x", "http://proxy1.example.com:8080", "-x", "socks5://proxy2.example.com:1080"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
assert.Len(t, config.Proxies, 2)
|
||||
assert.Equal(t, "http://proxy1.example.com:8080", config.Proxies[0].String())
|
||||
assert.Equal(t, "socks5://proxy2.example.com:1080", config.Proxies[1].String())
|
||||
})
|
||||
|
||||
t.Run("Parse with invalid proxy returns FieldParseErrors", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-x", "://invalid-proxy"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
assert.Nil(t, config)
|
||||
var fieldErr types.FieldParseErrors
|
||||
require.ErrorAs(t, err, &fieldErr)
|
||||
assert.Len(t, fieldErr.Errors, 1)
|
||||
assert.Equal(t, "proxy[0]", fieldErr.Errors[0].Field)
|
||||
})
|
||||
|
||||
t.Run("Parse with mixed valid and invalid proxies", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-x", "http://valid.example.com:8080", "-x", "://invalid"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
assert.Nil(t, config)
|
||||
var fieldErr types.FieldParseErrors
|
||||
require.ErrorAs(t, err, &fieldErr)
|
||||
assert.Len(t, fieldErr.Errors, 1)
|
||||
assert.Equal(t, "proxy[1]", fieldErr.Errors[0].Field)
|
||||
})
|
||||
|
||||
t.Run("Parse with long flag names", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{
|
||||
"dodo",
|
||||
"--url", "https://example.com",
|
||||
"--method", "POST",
|
||||
"--yes",
|
||||
"--skip-verify",
|
||||
"--dodos", "3",
|
||||
"--requests", "500",
|
||||
"--duration", "1m",
|
||||
"--timeout", "10s",
|
||||
})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
assert.Equal(t, "https://example.com", config.URL.String())
|
||||
assert.Equal(t, "POST", *config.Method)
|
||||
assert.True(t, *config.Yes)
|
||||
assert.True(t, *config.SkipVerify)
|
||||
assert.Equal(t, uint(3), *config.DodosCount)
|
||||
assert.Equal(t, uint(500), *config.RequestCount)
|
||||
assert.Equal(t, time.Minute, *config.Duration)
|
||||
assert.Equal(t, 10*time.Second, *config.Timeout)
|
||||
})
|
||||
|
||||
t.Run("Parse with all flags combined", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{
|
||||
"dodo",
|
||||
"-u", "https://api.example.com/test",
|
||||
"-m", "PUT",
|
||||
"-y",
|
||||
"-skip-verify",
|
||||
"-d", "10",
|
||||
"-r", "2000",
|
||||
"-o", "30m",
|
||||
"-t", "5s",
|
||||
"-p", "apikey=123",
|
||||
"-H", "Content-Type: application/json",
|
||||
"-c", "session=token123",
|
||||
"-b", `{"data": "test"}`,
|
||||
"-x", "http://proxy.example.com:3128",
|
||||
})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
// Verify all fields are set correctly
|
||||
assert.Equal(t, "https://api.example.com/test", config.URL.String())
|
||||
assert.Equal(t, "PUT", *config.Method)
|
||||
assert.True(t, *config.Yes)
|
||||
assert.True(t, *config.SkipVerify)
|
||||
assert.Equal(t, uint(10), *config.DodosCount)
|
||||
assert.Equal(t, uint(2000), *config.RequestCount)
|
||||
assert.Equal(t, 30*time.Minute, *config.Duration)
|
||||
assert.Equal(t, 5*time.Second, *config.Timeout)
|
||||
|
||||
assert.Len(t, config.Params, 1)
|
||||
assert.Equal(t, "apikey", config.Params[0].Key)
|
||||
|
||||
assert.Len(t, config.Headers, 1)
|
||||
assert.Equal(t, "Content-Type", config.Headers[0].Key)
|
||||
|
||||
assert.Len(t, config.Cookies, 1)
|
||||
assert.Equal(t, "session", config.Cookies[0].Key)
|
||||
|
||||
assert.Len(t, config.Bodies, 1)
|
||||
assert.Equal(t, types.Body(`{"data": "test"}`), config.Bodies[0]) //nolint:testifylint
|
||||
|
||||
assert.Len(t, config.Proxies, 1)
|
||||
assert.Equal(t, "http://proxy.example.com:3128", config.Proxies[0].String())
|
||||
})
|
||||
|
||||
t.Run("Parse with multiple field parse errors", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{
|
||||
"dodo",
|
||||
"-u", "://invalid-url",
|
||||
"-x", "://invalid-proxy1",
|
||||
"-x", "://invalid-proxy2",
|
||||
})
|
||||
config, err := parser.Parse()
|
||||
|
||||
assert.Nil(t, config)
|
||||
var fieldErr types.FieldParseErrors
|
||||
require.ErrorAs(t, err, &fieldErr)
|
||||
assert.Len(t, fieldErr.Errors, 3)
|
||||
|
||||
// Check error fields
|
||||
fields := make(map[string]bool)
|
||||
for _, parseErr := range fieldErr.Errors {
|
||||
fields[parseErr.Field] = true
|
||||
}
|
||||
assert.True(t, fields["url"])
|
||||
assert.True(t, fields["proxy[0]"])
|
||||
assert.True(t, fields["proxy[1]"])
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigCLIParser_PrintHelp(t *testing.T) {
|
||||
t.Run("PrintHelp outputs expected content", func(t *testing.T) {
|
||||
// Capture stdout
|
||||
oldStdout := os.Stdout
|
||||
reader, writer, _ := os.Pipe()
|
||||
os.Stdout = writer
|
||||
|
||||
parser := NewConfigCLIParser([]string{"dodo"})
|
||||
parser.PrintHelp()
|
||||
|
||||
// Restore stdout and read output
|
||||
writer.Close()
|
||||
os.Stdout = oldStdout
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, reader)
|
||||
output := buf.String()
|
||||
|
||||
// Verify help text contains expected elements
|
||||
assert.Contains(t, output, "Usage:")
|
||||
assert.Contains(t, output, "dodo [flags]")
|
||||
assert.Contains(t, output, "Examples:")
|
||||
assert.Contains(t, output, "Flags:")
|
||||
assert.Contains(t, output, "-h, -help")
|
||||
assert.Contains(t, output, "-v, -version")
|
||||
assert.Contains(t, output, "-u, -url")
|
||||
assert.Contains(t, output, "-m, -method")
|
||||
assert.Contains(t, output, "-d, -dodos")
|
||||
assert.Contains(t, output, "-r, -requests")
|
||||
assert.Contains(t, output, "-t, -timeout")
|
||||
assert.Contains(t, output, "-b, -body")
|
||||
assert.Contains(t, output, "-H, -header")
|
||||
assert.Contains(t, output, "-p, -param")
|
||||
assert.Contains(t, output, "-c, -cookie")
|
||||
assert.Contains(t, output, "-x, -proxy")
|
||||
assert.Contains(t, output, "-skip-verify")
|
||||
assert.Contains(t, output, "-y, -yes")
|
||||
|
||||
// Verify default values are included
|
||||
assert.Contains(t, output, Defaults.Method)
|
||||
assert.Contains(t, output, "1") // DodosCount default
|
||||
assert.Contains(t, output, "10s") // RequestTimeout default
|
||||
assert.Contains(t, output, "false") // Yes default
|
||||
assert.Contains(t, output, "false") // SkipVerify default
|
||||
})
|
||||
}
|
||||
|
||||
func TestCLIYesOrNoReader(t *testing.T) {
|
||||
t.Run("CLIYesOrNoReader with 'y' input returns true", func(t *testing.T) {
|
||||
// Redirect stdin
|
||||
oldStdin := os.Stdin
|
||||
reader, writer, _ := os.Pipe()
|
||||
os.Stdin = reader
|
||||
|
||||
// Write input and close writer
|
||||
writer.WriteString("y\n")
|
||||
writer.Close()
|
||||
|
||||
result := CLIYesOrNoReader("Test question", false)
|
||||
|
||||
// Restore stdin
|
||||
os.Stdin = oldStdin
|
||||
|
||||
assert.True(t, result)
|
||||
})
|
||||
|
||||
t.Run("CLIYesOrNoReader with 'Y' input returns true", func(t *testing.T) {
|
||||
// Redirect stdin
|
||||
oldStdin := os.Stdin
|
||||
reader, writer, _ := os.Pipe()
|
||||
os.Stdin = reader
|
||||
|
||||
// Write input and close writer
|
||||
writer.WriteString("Y\n")
|
||||
writer.Close()
|
||||
|
||||
result := CLIYesOrNoReader("Test question", false)
|
||||
|
||||
// Restore stdin
|
||||
os.Stdin = oldStdin
|
||||
|
||||
assert.True(t, result)
|
||||
})
|
||||
|
||||
t.Run("CLIYesOrNoReader with 'n' input returns false", func(t *testing.T) {
|
||||
// Redirect stdin
|
||||
oldStdin := os.Stdin
|
||||
reader, writer, _ := os.Pipe()
|
||||
os.Stdin = reader
|
||||
|
||||
// Write input and close writer
|
||||
writer.WriteString("n\n")
|
||||
writer.Close()
|
||||
|
||||
result := CLIYesOrNoReader("Test question", true)
|
||||
|
||||
// Restore stdin
|
||||
os.Stdin = oldStdin
|
||||
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("CLIYesOrNoReader with empty input returns default", func(t *testing.T) {
|
||||
// Redirect stdin
|
||||
oldStdin := os.Stdin
|
||||
reader, writer, _ := os.Pipe()
|
||||
os.Stdin = reader
|
||||
|
||||
// Write just newline and close writer
|
||||
writer.WriteString("\n")
|
||||
writer.Close()
|
||||
|
||||
// Test with default true
|
||||
result := CLIYesOrNoReader("Test question", true)
|
||||
os.Stdin = oldStdin
|
||||
assert.True(t, result)
|
||||
})
|
||||
|
||||
t.Run("CLIYesOrNoReader with empty input returns default false", func(t *testing.T) {
|
||||
// Redirect stdin
|
||||
oldStdin := os.Stdin
|
||||
reader, writer, _ := os.Pipe()
|
||||
os.Stdin = reader
|
||||
|
||||
// Write just newline and close writer
|
||||
writer.WriteString("\n")
|
||||
writer.Close()
|
||||
|
||||
// Test with default false
|
||||
result := CLIYesOrNoReader("Test question", false)
|
||||
os.Stdin = oldStdin
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("CLIYesOrNoReader with other input returns false", func(t *testing.T) {
|
||||
// Redirect stdin
|
||||
oldStdin := os.Stdin
|
||||
reader, writer, _ := os.Pipe()
|
||||
os.Stdin = reader
|
||||
|
||||
// Write other input and close writer
|
||||
writer.WriteString("maybe\n")
|
||||
writer.Close()
|
||||
|
||||
result := CLIYesOrNoReader("Test question", true)
|
||||
|
||||
// Restore stdin
|
||||
os.Stdin = oldStdin
|
||||
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("CLIYesOrNoReader message format with default true", func(t *testing.T) {
|
||||
// Capture stdout to verify message format
|
||||
oldStdout := os.Stdout
|
||||
stdoutReader, stdoutWriter, _ := os.Pipe()
|
||||
os.Stdout = stdoutWriter
|
||||
|
||||
// Redirect stdin
|
||||
oldStdin := os.Stdin
|
||||
stdinReader, stdinWriter, _ := os.Pipe()
|
||||
os.Stdin = stdinReader
|
||||
|
||||
// Write input and close writer
|
||||
stdinWriter.WriteString("y\n")
|
||||
stdinWriter.Close()
|
||||
|
||||
CLIYesOrNoReader("Continue?", true)
|
||||
|
||||
// Restore stdin and stdout
|
||||
os.Stdin = oldStdin
|
||||
stdoutWriter.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
// Read output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, stdoutReader)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "Continue? [Y/n]:")
|
||||
})
|
||||
|
||||
t.Run("CLIYesOrNoReader message format with default false", func(t *testing.T) {
|
||||
// Capture stdout to verify message format
|
||||
oldStdout := os.Stdout
|
||||
stdoutReader, stdoutWriter, _ := os.Pipe()
|
||||
os.Stdout = stdoutWriter
|
||||
|
||||
// Redirect stdin
|
||||
oldStdin := os.Stdin
|
||||
stdinReader, stdinWriter, _ := os.Pipe()
|
||||
os.Stdin = stdinReader
|
||||
|
||||
// Write input and close writer
|
||||
stdinWriter.WriteString("n\n")
|
||||
stdinWriter.Close()
|
||||
|
||||
CLIYesOrNoReader("Delete files?", false)
|
||||
|
||||
// Restore stdin and stdout
|
||||
os.Stdin = oldStdin
|
||||
stdoutWriter.Close()
|
||||
os.Stdout = oldStdout
|
||||
|
||||
// Read output
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, stdoutReader)
|
||||
output := buf.String()
|
||||
|
||||
assert.Contains(t, output, "Delete files? [y/N]:")
|
||||
})
|
||||
}
|
||||
|
||||
func TestConfigCLIParser_EdgeCases(t *testing.T) {
|
||||
t.Run("Parse with zero duration", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-o", "0s"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Duration)
|
||||
assert.Equal(t, time.Duration(0), *config.Duration)
|
||||
})
|
||||
|
||||
t.Run("Parse with zero timeout", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-t", "0s"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.Timeout)
|
||||
assert.Equal(t, time.Duration(0), *config.Timeout)
|
||||
})
|
||||
|
||||
t.Run("Parse with zero dodos count", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-d", "0"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.DodosCount)
|
||||
assert.Equal(t, uint(0), *config.DodosCount)
|
||||
})
|
||||
|
||||
t.Run("Parse with zero request count", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-r", "0"})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.RequestCount)
|
||||
assert.Equal(t, uint(0), *config.RequestCount)
|
||||
})
|
||||
|
||||
t.Run("Parse with empty string values", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{
|
||||
"dodo",
|
||||
"-m", "",
|
||||
"-p", "",
|
||||
"-H", "",
|
||||
"-c", "",
|
||||
"-b", "",
|
||||
})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
assert.Empty(t, *config.Method)
|
||||
assert.Len(t, config.Params, 1)
|
||||
assert.Empty(t, config.Params[0].Key)
|
||||
assert.Len(t, config.Headers, 1)
|
||||
assert.Empty(t, config.Headers[0].Key)
|
||||
assert.Len(t, config.Cookies, 1)
|
||||
assert.Empty(t, config.Cookies[0].Key)
|
||||
assert.Len(t, config.Bodies, 1)
|
||||
assert.Equal(t, types.Body(""), config.Bodies[0])
|
||||
})
|
||||
|
||||
t.Run("Parse with complex URL", func(t *testing.T) {
|
||||
complexURL := "https://user:pass@api.example.com:8080/v1/endpoint?param=value&other=test#fragment"
|
||||
parser := NewConfigCLIParser([]string{"dodo", "-u", complexURL})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
require.NotNil(t, config.URL)
|
||||
|
||||
parsedURL, parseErr := url.Parse(complexURL)
|
||||
require.NoError(t, parseErr)
|
||||
assert.Equal(t, parsedURL, config.URL)
|
||||
})
|
||||
|
||||
t.Run("Parse with repeated same flags overrides previous values", func(t *testing.T) {
|
||||
parser := NewConfigCLIParser([]string{
|
||||
"dodo",
|
||||
"-m", "GET",
|
||||
"-m", "POST", // This should override the previous
|
||||
"-d", "1",
|
||||
"-d", "5", // This should override the previous
|
||||
})
|
||||
config, err := parser.Parse()
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, config)
|
||||
|
||||
assert.Equal(t, "POST", *config.Method)
|
||||
assert.Equal(t, uint(5), *config.DodosCount)
|
||||
})
|
||||
}
|
112
pkg/config/config.go
Normal file
112
pkg/config/config.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/aykhans/dodo/pkg/types"
|
||||
"github.com/aykhans/dodo/pkg/utils"
|
||||
)
|
||||
|
||||
const VERSION string = "1.0.0"
|
||||
|
||||
var Defaults = struct {
|
||||
UserAgent string
|
||||
Method string
|
||||
RequestTimeout time.Duration
|
||||
DodosCount uint
|
||||
Yes bool
|
||||
SkipVerify bool
|
||||
}{
|
||||
UserAgent: "dodo/" + VERSION,
|
||||
Method: "GET",
|
||||
RequestTimeout: time.Second * 10,
|
||||
DodosCount: 1,
|
||||
Yes: false,
|
||||
SkipVerify: false,
|
||||
}
|
||||
|
||||
var SupportedProxySchemes []string = []string{"http", "socks5", "socks5h"}
|
||||
|
||||
type Config struct {
|
||||
Method *string
|
||||
URL *url.URL
|
||||
Timeout *time.Duration
|
||||
DodosCount *uint
|
||||
RequestCount *uint
|
||||
Duration *time.Duration
|
||||
Yes *bool
|
||||
SkipVerify *bool
|
||||
Params types.Params
|
||||
Headers types.Headers
|
||||
Cookies types.Cookies
|
||||
Bodies types.Bodies
|
||||
Proxies types.Proxies
|
||||
}
|
||||
|
||||
func NewConfig() *Config {
|
||||
return &Config{}
|
||||
}
|
||||
|
||||
func (config *Config) MergeConfig(newConfig *Config) {
|
||||
if newConfig.Method != nil {
|
||||
config.Method = newConfig.Method
|
||||
}
|
||||
if newConfig.URL != nil {
|
||||
config.URL = newConfig.URL
|
||||
}
|
||||
if newConfig.Timeout != nil {
|
||||
config.Timeout = newConfig.Timeout
|
||||
}
|
||||
if newConfig.DodosCount != nil {
|
||||
config.DodosCount = newConfig.DodosCount
|
||||
}
|
||||
if newConfig.RequestCount != nil {
|
||||
config.RequestCount = newConfig.RequestCount
|
||||
}
|
||||
if newConfig.Duration != nil {
|
||||
config.Duration = newConfig.Duration
|
||||
}
|
||||
if newConfig.Yes != nil {
|
||||
config.Yes = newConfig.Yes
|
||||
}
|
||||
if newConfig.SkipVerify != nil {
|
||||
config.SkipVerify = newConfig.SkipVerify
|
||||
}
|
||||
if len(newConfig.Params) != 0 {
|
||||
config.Params = newConfig.Params
|
||||
}
|
||||
if len(newConfig.Headers) != 0 {
|
||||
config.Headers = newConfig.Headers
|
||||
}
|
||||
if len(newConfig.Cookies) != 0 {
|
||||
config.Cookies = newConfig.Cookies
|
||||
}
|
||||
if len(newConfig.Bodies) != 0 {
|
||||
config.Bodies = newConfig.Bodies
|
||||
}
|
||||
if len(newConfig.Proxies) != 0 {
|
||||
config.Proxies = newConfig.Proxies
|
||||
}
|
||||
}
|
||||
|
||||
func (config *Config) SetDefaults() {
|
||||
if config.Method == nil {
|
||||
config.Method = utils.ToPtr(Defaults.Method)
|
||||
}
|
||||
if config.Timeout == nil {
|
||||
config.Timeout = &Defaults.RequestTimeout
|
||||
}
|
||||
if config.DodosCount == nil {
|
||||
config.DodosCount = utils.ToPtr(Defaults.DodosCount)
|
||||
}
|
||||
if config.Yes == nil {
|
||||
config.Yes = utils.ToPtr(Defaults.Yes)
|
||||
}
|
||||
if config.SkipVerify == nil {
|
||||
config.SkipVerify = utils.ToPtr(Defaults.SkipVerify)
|
||||
}
|
||||
if !config.Headers.Has("User-Agent") {
|
||||
config.Headers = append(config.Headers, types.Header{Key: "User-Agent", Value: []string{Defaults.UserAgent}})
|
||||
}
|
||||
}
|
318
pkg/config/config_test.go
Normal file
318
pkg/config/config_test.go
Normal file
@@ -0,0 +1,318 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aykhans/dodo/pkg/types"
|
||||
"github.com/aykhans/dodo/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMergeConfig(t *testing.T) {
|
||||
t.Run("MergeConfig with all fields from new config", func(t *testing.T) {
|
||||
originalURL, _ := url.Parse("https://original.example.com")
|
||||
newURL, _ := url.Parse("https://new.example.com")
|
||||
|
||||
originalTimeout := 5 * time.Second
|
||||
newTimeout := 10 * time.Second
|
||||
|
||||
originalDuration := 1 * time.Minute
|
||||
newDuration := 2 * time.Minute
|
||||
|
||||
config := &Config{
|
||||
Method: utils.ToPtr("GET"),
|
||||
URL: originalURL,
|
||||
Timeout: &originalTimeout,
|
||||
DodosCount: utils.ToPtr(uint(1)),
|
||||
RequestCount: utils.ToPtr(uint(10)),
|
||||
Duration: &originalDuration,
|
||||
Yes: utils.ToPtr(false),
|
||||
SkipVerify: utils.ToPtr(false),
|
||||
Params: types.Params{{Key: "old", Value: []string{"value"}}},
|
||||
Headers: types.Headers{{Key: "Old-Header", Value: []string{"old"}}},
|
||||
Cookies: types.Cookies{{Key: "oldCookie", Value: []string{"oldValue"}}},
|
||||
Bodies: types.Bodies{types.Body("old body")},
|
||||
Proxies: types.Proxies{},
|
||||
}
|
||||
|
||||
newConfig := &Config{
|
||||
Method: utils.ToPtr("POST"),
|
||||
URL: newURL,
|
||||
Timeout: &newTimeout,
|
||||
DodosCount: utils.ToPtr(uint(5)),
|
||||
RequestCount: utils.ToPtr(uint(20)),
|
||||
Duration: &newDuration,
|
||||
Yes: utils.ToPtr(true),
|
||||
SkipVerify: utils.ToPtr(true),
|
||||
Params: types.Params{{Key: "new", Value: []string{"value"}}},
|
||||
Headers: types.Headers{{Key: "New-Header", Value: []string{"new"}}},
|
||||
Cookies: types.Cookies{{Key: "newCookie", Value: []string{"newValue"}}},
|
||||
Bodies: types.Bodies{types.Body("new body")},
|
||||
Proxies: types.Proxies{},
|
||||
}
|
||||
|
||||
config.MergeConfig(newConfig)
|
||||
|
||||
assert.Equal(t, "POST", *config.Method)
|
||||
assert.Equal(t, newURL, config.URL)
|
||||
assert.Equal(t, newTimeout, *config.Timeout)
|
||||
assert.Equal(t, uint(5), *config.DodosCount)
|
||||
assert.Equal(t, uint(20), *config.RequestCount)
|
||||
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.Empty(t, config.Proxies)
|
||||
})
|
||||
|
||||
t.Run("MergeConfig with partial fields from new config", func(t *testing.T) {
|
||||
originalURL, _ := url.Parse("https://original.example.com")
|
||||
originalTimeout := 5 * time.Second
|
||||
|
||||
config := &Config{
|
||||
Method: utils.ToPtr("GET"),
|
||||
URL: originalURL,
|
||||
Timeout: &originalTimeout,
|
||||
DodosCount: utils.ToPtr(uint(1)),
|
||||
Headers: types.Headers{{Key: "Original-Header", Value: []string{"original"}}},
|
||||
}
|
||||
|
||||
newURL, _ := url.Parse("https://new.example.com")
|
||||
newConfig := &Config{
|
||||
URL: newURL,
|
||||
DodosCount: utils.ToPtr(uint(10)),
|
||||
}
|
||||
|
||||
config.MergeConfig(newConfig)
|
||||
|
||||
assert.Equal(t, "GET", *config.Method, "Method should remain unchanged")
|
||||
assert.Equal(t, newURL, config.URL, "URL should be updated")
|
||||
assert.Equal(t, originalTimeout, *config.Timeout, "Timeout should remain unchanged")
|
||||
assert.Equal(t, uint(10), *config.DodosCount, "DodosCount should be updated")
|
||||
assert.Equal(t, types.Headers{{Key: "Original-Header", Value: []string{"original"}}}, config.Headers, "Headers should remain unchanged")
|
||||
})
|
||||
|
||||
t.Run("MergeConfig with nil new config fields", func(t *testing.T) {
|
||||
originalURL, _ := url.Parse("https://original.example.com")
|
||||
originalTimeout := 5 * time.Second
|
||||
|
||||
config := &Config{
|
||||
Method: utils.ToPtr("GET"),
|
||||
URL: originalURL,
|
||||
Timeout: &originalTimeout,
|
||||
DodosCount: utils.ToPtr(uint(1)),
|
||||
Yes: utils.ToPtr(false),
|
||||
SkipVerify: utils.ToPtr(false),
|
||||
}
|
||||
|
||||
newConfig := &Config{
|
||||
Method: nil,
|
||||
URL: nil,
|
||||
Timeout: nil,
|
||||
DodosCount: nil,
|
||||
Yes: nil,
|
||||
SkipVerify: nil,
|
||||
}
|
||||
|
||||
originalConfigCopy := *config
|
||||
config.MergeConfig(newConfig)
|
||||
|
||||
assert.Equal(t, originalConfigCopy.Method, config.Method)
|
||||
assert.Equal(t, originalConfigCopy.URL, config.URL)
|
||||
assert.Equal(t, originalConfigCopy.Timeout, config.Timeout)
|
||||
assert.Equal(t, originalConfigCopy.DodosCount, config.DodosCount)
|
||||
assert.Equal(t, originalConfigCopy.Yes, config.Yes)
|
||||
assert.Equal(t, originalConfigCopy.SkipVerify, config.SkipVerify)
|
||||
})
|
||||
|
||||
t.Run("MergeConfig with empty slices", func(t *testing.T) {
|
||||
config := &Config{
|
||||
Params: types.Params{{Key: "original", Value: []string{"value"}}},
|
||||
Headers: types.Headers{{Key: "Original-Header", Value: []string{"original"}}},
|
||||
Cookies: types.Cookies{{Key: "originalCookie", Value: []string{"originalValue"}}},
|
||||
Bodies: types.Bodies{types.Body("original body")},
|
||||
Proxies: types.Proxies{},
|
||||
}
|
||||
|
||||
newConfig := &Config{
|
||||
Params: types.Params{},
|
||||
Headers: types.Headers{},
|
||||
Cookies: types.Cookies{},
|
||||
Bodies: types.Bodies{},
|
||||
Proxies: types.Proxies{},
|
||||
}
|
||||
|
||||
config.MergeConfig(newConfig)
|
||||
|
||||
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.Cookies{{Key: "originalCookie", Value: []string{"originalValue"}}}, config.Cookies, "Empty Cookies should not override")
|
||||
assert.Equal(t, types.Bodies{types.Body("original body")}, config.Bodies, "Empty Bodies should not override")
|
||||
assert.Equal(t, types.Proxies{}, config.Proxies, "Empty Proxies should not override")
|
||||
})
|
||||
|
||||
t.Run("MergeConfig on empty original config", func(t *testing.T) {
|
||||
config := &Config{}
|
||||
|
||||
newURL, _ := url.Parse("https://new.example.com")
|
||||
newTimeout := 10 * time.Second
|
||||
newDuration := 2 * time.Minute
|
||||
|
||||
newConfig := &Config{
|
||||
Method: utils.ToPtr("POST"),
|
||||
URL: newURL,
|
||||
Timeout: &newTimeout,
|
||||
DodosCount: utils.ToPtr(uint(5)),
|
||||
RequestCount: utils.ToPtr(uint(20)),
|
||||
Duration: &newDuration,
|
||||
Yes: utils.ToPtr(true),
|
||||
SkipVerify: utils.ToPtr(true),
|
||||
Params: types.Params{{Key: "new", Value: []string{"value"}}},
|
||||
Headers: types.Headers{{Key: "New-Header", Value: []string{"new"}}},
|
||||
Cookies: types.Cookies{{Key: "newCookie", Value: []string{"newValue"}}},
|
||||
Bodies: types.Bodies{types.Body("new body")},
|
||||
Proxies: types.Proxies{},
|
||||
}
|
||||
|
||||
config.MergeConfig(newConfig)
|
||||
|
||||
assert.Equal(t, "POST", *config.Method)
|
||||
assert.Equal(t, newURL, config.URL)
|
||||
assert.Equal(t, newTimeout, *config.Timeout)
|
||||
assert.Equal(t, uint(5), *config.DodosCount)
|
||||
assert.Equal(t, uint(20), *config.RequestCount)
|
||||
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.Empty(t, config.Proxies)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetDefaults(t *testing.T) {
|
||||
t.Run("SetDefaults on empty config", func(t *testing.T) {
|
||||
config := &Config{}
|
||||
config.SetDefaults()
|
||||
|
||||
require.NotNil(t, config.Method)
|
||||
assert.Equal(t, Defaults.Method, *config.Method)
|
||||
|
||||
require.NotNil(t, config.Timeout)
|
||||
assert.Equal(t, Defaults.RequestTimeout, *config.Timeout)
|
||||
|
||||
require.NotNil(t, config.DodosCount)
|
||||
assert.Equal(t, Defaults.DodosCount, *config.DodosCount)
|
||||
|
||||
require.NotNil(t, config.Yes)
|
||||
assert.Equal(t, Defaults.Yes, *config.Yes)
|
||||
|
||||
require.NotNil(t, config.SkipVerify)
|
||||
assert.Equal(t, Defaults.SkipVerify, *config.SkipVerify)
|
||||
|
||||
assert.True(t, config.Headers.Has("User-Agent"))
|
||||
assert.Equal(t, Defaults.UserAgent, config.Headers[0].Value[0])
|
||||
})
|
||||
|
||||
t.Run("SetDefaults preserves existing values", func(t *testing.T) {
|
||||
customTimeout := 30 * time.Second
|
||||
config := &Config{
|
||||
Method: utils.ToPtr("POST"),
|
||||
Timeout: &customTimeout,
|
||||
DodosCount: utils.ToPtr(uint(10)),
|
||||
Yes: utils.ToPtr(true),
|
||||
SkipVerify: utils.ToPtr(true),
|
||||
Headers: types.Headers{{Key: "User-Agent", Value: []string{"custom-agent"}}},
|
||||
}
|
||||
|
||||
config.SetDefaults()
|
||||
|
||||
assert.Equal(t, "POST", *config.Method, "Method should not be overridden")
|
||||
assert.Equal(t, customTimeout, *config.Timeout, "Timeout should not be overridden")
|
||||
assert.Equal(t, uint(10), *config.DodosCount, "DodosCount should not be overridden")
|
||||
assert.True(t, *config.Yes, "Yes should not be overridden")
|
||||
assert.True(t, *config.SkipVerify, "SkipVerify should not be overridden")
|
||||
assert.Equal(t, "custom-agent", config.Headers[0].Value[0], "User-Agent should not be overridden")
|
||||
assert.Len(t, config.Headers, 1, "Should not add duplicate User-Agent")
|
||||
})
|
||||
|
||||
t.Run("SetDefaults adds User-Agent when missing", func(t *testing.T) {
|
||||
config := &Config{
|
||||
Headers: types.Headers{{Key: "Content-Type", Value: []string{"application/json"}}},
|
||||
}
|
||||
|
||||
config.SetDefaults()
|
||||
|
||||
assert.Len(t, config.Headers, 2)
|
||||
assert.True(t, config.Headers.Has("User-Agent"))
|
||||
assert.True(t, config.Headers.Has("Content-Type"))
|
||||
|
||||
var userAgentFound bool
|
||||
for _, h := range config.Headers {
|
||||
if h.Key == "User-Agent" {
|
||||
userAgentFound = true
|
||||
assert.Equal(t, Defaults.UserAgent, h.Value[0])
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(t, userAgentFound, "User-Agent header should be added")
|
||||
})
|
||||
|
||||
t.Run("SetDefaults with partial config", func(t *testing.T) {
|
||||
config := &Config{
|
||||
Method: utils.ToPtr("PUT"),
|
||||
Yes: utils.ToPtr(true),
|
||||
}
|
||||
|
||||
config.SetDefaults()
|
||||
|
||||
assert.Equal(t, "PUT", *config.Method, "Existing Method should be preserved")
|
||||
assert.True(t, *config.Yes, "Existing Yes should be preserved")
|
||||
|
||||
require.NotNil(t, config.Timeout)
|
||||
assert.Equal(t, Defaults.RequestTimeout, *config.Timeout, "Timeout should be set to default")
|
||||
|
||||
require.NotNil(t, config.DodosCount)
|
||||
assert.Equal(t, Defaults.DodosCount, *config.DodosCount, "DodosCount should be set to default")
|
||||
|
||||
require.NotNil(t, config.SkipVerify)
|
||||
assert.Equal(t, Defaults.SkipVerify, *config.SkipVerify, "SkipVerify should be set to default")
|
||||
|
||||
assert.True(t, config.Headers.Has("User-Agent"))
|
||||
})
|
||||
|
||||
t.Run("SetDefaults idempotent", func(t *testing.T) {
|
||||
config := &Config{}
|
||||
|
||||
config.SetDefaults()
|
||||
firstCallHeaders := len(config.Headers)
|
||||
firstCallMethod := *config.Method
|
||||
firstCallTimeout := *config.Timeout
|
||||
|
||||
config.SetDefaults()
|
||||
|
||||
assert.Len(t, config.Headers, firstCallHeaders, "Headers count should not change on second call")
|
||||
assert.Equal(t, firstCallMethod, *config.Method, "Method should not change on second call")
|
||||
assert.Equal(t, firstCallTimeout, *config.Timeout, "Timeout should not change on second call")
|
||||
})
|
||||
|
||||
t.Run("SetDefaults with empty Headers initializes correctly", func(t *testing.T) {
|
||||
config := &Config{
|
||||
Headers: types.Headers{},
|
||||
}
|
||||
|
||||
config.SetDefaults()
|
||||
|
||||
assert.Len(t, config.Headers, 1)
|
||||
assert.Equal(t, "User-Agent", config.Headers[0].Key)
|
||||
assert.Equal(t, Defaults.UserAgent, config.Headers[0].Value[0])
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user