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) assert.Equal(t, "://invalid-url", fieldErr.Errors[0].Value) }) 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) assert.Equal(t, "://invalid-proxy", fieldErr.Errors[0].Value) }) 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) assert.Equal(t, "://invalid", fieldErr.Errors[0].Value) }) 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 config-file flag valid YAML", func(t *testing.T) { parser := NewConfigCLIParser([]string{"dodo", "-f", "/path/to/config.yaml"}) config, err := parser.Parse() require.NoError(t, err) require.NotNil(t, config) assert.Len(t, config.Files, 1) }) t.Run("Parse with config-file flag using long form", func(t *testing.T) { parser := NewConfigCLIParser([]string{"dodo", "--config-file", "/path/to/config.yml"}) config, err := parser.Parse() require.NoError(t, err) require.NotNil(t, config) assert.Len(t, config.Files, 1) }) t.Run("Parse with config-file flag invalid extension", func(t *testing.T) { parser := NewConfigCLIParser([]string{"dodo", "-f", "/path/to/config"}) config, err := parser.Parse() assert.Nil(t, config) var fieldErr types.FieldParseErrors require.ErrorAs(t, err, &fieldErr) assert.Len(t, fieldErr.Errors, 1) assert.Equal(t, "config-file[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") }) t.Run("Parse with config-file flag unsupported file type", func(t *testing.T) { parser := NewConfigCLIParser([]string{"dodo", "-f", "/path/to/config.json"}) config, err := parser.Parse() assert.Nil(t, config) var fieldErr types.FieldParseErrors require.ErrorAs(t, err, &fieldErr) assert.Len(t, fieldErr.Errors, 1) assert.Equal(t, "config-file[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(), "not supported") }) t.Run("Parse with config-file flag remote URL", func(t *testing.T) { parser := NewConfigCLIParser([]string{"dodo", "-f", "https://example.com/config.yaml"}) config, err := parser.Parse() require.NoError(t, err) require.NotNil(t, config) 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) { 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", "-f", "/path/to/config.yaml", }) 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()) assert.Len(t, config.Files, 1) }) 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]"]) // 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]"]) }) } 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") assert.Contains(t, output, "-f, -config-file") // 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) }) }