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{ 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") }) }