package config import ( "errors" "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{ Files: []types.ConfigFile{}, 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{ Files: []types.ConfigFile{}, 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.Merge(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: "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) }) 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{ Files: []types.ConfigFile{}, 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{ Files: []types.ConfigFile{}, URL: newURL, DodosCount: utils.ToPtr(uint(10)), } config.Merge(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{ Files: []types.ConfigFile{}, Method: utils.ToPtr("GET"), URL: originalURL, Timeout: &originalTimeout, DodosCount: utils.ToPtr(uint(1)), Yes: utils.ToPtr(false), SkipVerify: utils.ToPtr(false), } newConfig := &Config{ Files: []types.ConfigFile{}, Method: nil, URL: nil, Timeout: nil, DodosCount: nil, Yes: nil, SkipVerify: nil, } originalConfigCopy := *config config.Merge(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) { configFile, _ := types.ParseConfigFile("original.yml") config := &Config{ Files: []types.ConfigFile{*configFile}, 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{ Files: []types.ConfigFile{}, Params: types.Params{}, Headers: types.Headers{}, Cookies: types.Cookies{}, Bodies: types.Bodies{}, Proxies: types.Proxies{}, } config.Merge(newConfig) assert.Equal(t, []types.ConfigFile{*configFile}, config.Files, "Empty Files should not override") assert.Equal(t, types.Params{{Key: "original", Value: []string{"value"}}}, config.Params, "Empty Params should not override") assert.Equal(t, types.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 with Files field", func(t *testing.T) { configFile1, _ := types.ParseConfigFile("config1.yml") configFile2, _ := types.ParseConfigFile("config2.yaml") config := &Config{ Files: []types.ConfigFile{*configFile1}, Method: utils.ToPtr("GET"), Headers: types.Headers{{Key: "Original-Header", Value: []string{"original"}}}, } newConfig := &Config{ Files: []types.ConfigFile{*configFile2}, Method: utils.ToPtr("POST"), } config.Merge(newConfig) assert.Equal(t, "POST", *config.Method, "Method should be updated") assert.Equal(t, []types.ConfigFile{*configFile1, *configFile2}, config.Files, "Files should be appended") assert.Equal(t, types.Headers{{Key: "Original-Header", Value: []string{"original"}}}, config.Headers, "Headers should remain unchanged") }) t.Run("MergeConfig on empty original config", func(t *testing.T) { config := &Config{} newURL, _ := url.Parse("https://new.example.com") newTimeout := 10 * time.Second newDuration := 2 * time.Minute newConfig := &Config{ Files: []types.ConfigFile{}, 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.Merge(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{ Files: []types.ConfigFile{}, 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{ Files: []types.ConfigFile{}, 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{ Files: []types.ConfigFile{}, 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]) }) } 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") }) } func TestParseConfigFile(t *testing.T) { t.Run("parseConfigFile with maxDepth 0", func(t *testing.T) { // Create a mock config file configFile, _ := types.ParseConfigFile("test.yaml") // Since we can't actually test file reading without a real file, // we'll test the function's behavior with maxDepth config, err := parseConfigFile(*configFile, 0) // The function will return an error because the file doesn't exist require.Error(t, err) assert.Nil(t, config) }) t.Run("parseConfigFile returns ConfigFileReadError", func(t *testing.T) { configFile, _ := types.ParseConfigFile("/nonexistent/file.yaml") config, err := parseConfigFile(*configFile, 1) require.Error(t, err) assert.Nil(t, config) // Check if error is of type ConfigFileReadError var readErr types.ConfigFileReadError assert.ErrorAs(t, err, &readErr) }) } func TestPrintParseErrors(t *testing.T) { t.Run("printParseErrors with empty value", func(t *testing.T) { // This function prints to stdout, so we can't easily test its output // But we can test that it doesn't panic errors := []types.FieldParseError{ { Field: "test_field", Value: "", Err: errors.New("test error"), }, } // Should not panic assert.NotPanics(t, func() { printParseErrors("TEST", errors...) }) }) t.Run("printParseErrors with value", func(t *testing.T) { errors := []types.FieldParseError{ { Field: "test_field", Value: "test_value", Err: errors.New("test error"), }, } // Should not panic assert.NotPanics(t, func() { printParseErrors("TEST", errors...) }) }) t.Run("printParseErrors with multiple errors", func(t *testing.T) { errors := []types.FieldParseError{ { Field: "field1", Value: "value1", Err: errors.New("error1"), }, { Field: "field2", Value: "", Err: errors.New("error2"), }, } // Should not panic assert.NotPanics(t, func() { printParseErrors("TEST", errors...) }) }) } func TestValidate(t *testing.T) { t.Run("Valid config returns no error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") timeout := 30 * time.Second duration := 1 * time.Minute config := Config{ Method: utils.ToPtr("GET"), URL: testURL, Timeout: &timeout, DodosCount: utils.ToPtr(uint(5)), RequestCount: utils.ToPtr(uint(100)), Duration: &duration, Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() assert.NoError(t, err) }) t.Run("Missing Method returns validation error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") config := Config{ URL: testURL, DodosCount: utils.ToPtr(uint(5)), Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "method is required") }) t.Run("Missing URL returns validation error", func(t *testing.T) { config := Config{ Method: utils.ToPtr("GET"), DodosCount: utils.ToPtr(uint(5)), Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "URL is required") }) t.Run("Invalid URL scheme returns validation error", func(t *testing.T) { testURL, _ := url.Parse("ftp://example.com") config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(5)), Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "URL scheme must be one of") }) t.Run("Missing DodosCount returns validation error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") config := Config{ Method: utils.ToPtr("GET"), URL: testURL, Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "dodos count is required") }) t.Run("Zero DodosCount returns validation error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(0)), Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "dodos count must be greater than 0") }) t.Run("Missing both RequestCount and Duration returns validation error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(5)), Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "either request count or duration must be specified") }) t.Run("Both RequestCount and Duration zero returns validation error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") duration := time.Duration(0) config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(5)), RequestCount: utils.ToPtr(uint(0)), Duration: &duration, Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "both request count and duration cannot be zero") }) t.Run("Zero RequestCount only returns validation error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(5)), RequestCount: utils.ToPtr(uint(0)), Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "request count must be greater than 0") }) t.Run("Zero Duration only returns validation error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") duration := time.Duration(0) config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(5)), Duration: &duration, Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "duration must be greater than 0") }) t.Run("Missing Yes returns validation error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(5)), RequestCount: utils.ToPtr(uint(100)), SkipVerify: utils.ToPtr(false), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "yes field is required") }) t.Run("Missing SkipVerify returns validation error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(5)), RequestCount: utils.ToPtr(uint(100)), Yes: utils.ToPtr(true), } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "skip verify field is required") }) t.Run("Invalid proxy scheme returns validation error", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") proxyURL, _ := url.Parse("ftp://proxy.example.com:8080") config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(5)), RequestCount: utils.ToPtr(uint(100)), Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), Proxies: types.Proxies{types.Proxy(*proxyURL)}, } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Contains(t, err.Error(), "proxy scheme must be one of") assert.Contains(t, err.Error(), "Proxy[0]") }) t.Run("Multiple invalid proxies return validation errors", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") proxyURL1, _ := url.Parse("ftp://proxy1.example.com:8080") proxyURL2, _ := url.Parse("ldap://proxy2.example.com:389") config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(5)), RequestCount: utils.ToPtr(uint(100)), Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), Proxies: types.Proxies{types.Proxy(*proxyURL1), types.Proxy(*proxyURL2)}, } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Len(t, validationErr.Errors, 2) assert.Contains(t, err.Error(), "Proxy[0]") assert.Contains(t, err.Error(), "Proxy[1]") }) t.Run("Valid proxy schemes pass validation", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") proxyURL1, _ := url.Parse("http://proxy1.example.com:8080") proxyURL2, _ := url.Parse("socks5://proxy2.example.com:1080") proxyURL3, _ := url.Parse("socks5h://proxy3.example.com:1080") config := Config{ Method: utils.ToPtr("GET"), URL: testURL, DodosCount: utils.ToPtr(uint(5)), RequestCount: utils.ToPtr(uint(100)), Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), Proxies: types.Proxies{types.Proxy(*proxyURL1), types.Proxy(*proxyURL2), types.Proxy(*proxyURL3)}, } err := config.Validate() assert.NoError(t, err) }) t.Run("Multiple validation errors are collected", func(t *testing.T) { config := Config{ // Missing Method, URL, DodosCount, Yes, SkipVerify // Missing both RequestCount and Duration } err := config.Validate() require.Error(t, err) var validationErr types.FieldValidationErrors require.ErrorAs(t, err, &validationErr) assert.Len(t, validationErr.Errors, 6) // All required fields missing assert.Contains(t, err.Error(), "method is required") assert.Contains(t, err.Error(), "URL is required") assert.Contains(t, err.Error(), "dodos count is required") assert.Contains(t, err.Error(), "either request count or duration must be specified") assert.Contains(t, err.Error(), "yes field is required") assert.Contains(t, err.Error(), "skip verify field is required") }) t.Run("Valid config with Duration only passes validation", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") duration := 30 * time.Second config := Config{ Method: utils.ToPtr("POST"), URL: testURL, DodosCount: utils.ToPtr(uint(10)), Duration: &duration, Yes: utils.ToPtr(false), SkipVerify: utils.ToPtr(true), } err := config.Validate() assert.NoError(t, err) }) t.Run("Valid config with RequestCount only passes validation", func(t *testing.T) { testURL, _ := url.Parse("https://example.com") config := Config{ Method: utils.ToPtr("PUT"), URL: testURL, DodosCount: utils.ToPtr(uint(3)), RequestCount: utils.ToPtr(uint(50)), Yes: utils.ToPtr(true), SkipVerify: utils.ToPtr(false), } err := config.Validate() assert.NoError(t, err) }) }