package httpclient import ( "net/http" "net/url" "testing" "github.com/stretchr/testify/require" ) func TestIsHTTPStatusCodeRetryable(t *testing.T) { t.Run("429 is retryable", func(t *testing.T) { require.True(t, IsHTTPStatusCodeRetryable(429)) }) t.Run("4xx errors (except 429) are not retryable", func(t *testing.T) { require.False(t, IsHTTPStatusCodeRetryable(400)) require.False(t, IsHTTPStatusCodeRetryable(401)) require.False(t, IsHTTPStatusCodeRetryable(403)) require.False(t, IsHTTPStatusCodeRetryable(404)) require.False(t, IsHTTPStatusCodeRetryable(422)) }) t.Run("5xx errors are retryable", func(t *testing.T) { require.True(t, IsHTTPStatusCodeRetryable(500)) require.True(t, IsHTTPStatusCodeRetryable(502)) require.True(t, IsHTTPStatusCodeRetryable(503)) require.True(t, IsHTTPStatusCodeRetryable(504)) }) t.Run("non-error status codes are not retryable", func(t *testing.T) { require.False(t, IsHTTPStatusCodeRetryable(200)) require.False(t, IsHTTPStatusCodeRetryable(201)) require.False(t, IsHTTPStatusCodeRetryable(301)) require.False(t, IsHTTPStatusCodeRetryable(302)) }) } func TestMergeHTTPHeaders(t *testing.T) { // Register headers for testing append behavior RegisterMergeWithAppendHeaders("User-Agent", "Accept") tests := []struct { name string // description of this test case // Named input parameters for target function. dest http.Header src http.Header want http.Header }{ { name: "given src Authorization header, should skip sensitive header", dest: http.Header{ "Content-Type": []string{"application/json"}, }, src: http.Header{ "Content-Type": []string{"application/json"}, "Authorization": []string{"Bearer 123456"}, }, want: http.Header{ "Content-Type": []string{"application/json"}, }, }, { name: "given src User-Agent header, should merge them", dest: http.Header{ "Content-Type": []string{"application/json"}, }, src: http.Header{ "Content-Type": []string{"application/json"}, "User-Agent": []string{"Mozilla/5.0"}, }, want: http.Header{ "Content-Type": []string{"application/json"}, "User-Agent": []string{"Mozilla/5.0"}, }, }, { name: "should add non-duplicate values to existing headers", dest: http.Header{ "Content-Type": []string{"application/json"}, "Accept": []string{"application/json"}, }, src: http.Header{ "Accept": []string{"text/plain", "application/json"}, "User-Agent": []string{"Mozilla/5.0"}, }, want: http.Header{ "Content-Type": []string{"application/json"}, "Accept": []string{"application/json", "text/plain"}, "User-Agent": []string{"Mozilla/5.0"}, }, }, { name: "should add all non-duplicate values from multiple values", dest: http.Header{ "Accept": []string{"application/json"}, }, src: http.Header{ "Accept": []string{"text/plain", "application/xml", "text/html"}, }, want: http.Header{ "Accept": []string{"application/json", "text/plain", "application/xml", "text/html"}, }, }, { name: "should skip all duplicate values", dest: http.Header{ "Accept": []string{"application/json", "text/plain"}, }, src: http.Header{ "Accept": []string{"application/json", "text/plain"}, }, want: http.Header{ "Accept": []string{"application/json", "text/plain"}, }, }, { name: "should skip only duplicate values and add new ones", dest: http.Header{ "Accept": []string{"application/json", "text/plain"}, }, src: http.Header{ "Accept": []string{"text/plain", "application/xml", "application/json"}, }, want: http.Header{ "Accept": []string{"application/json", "text/plain", "application/xml"}, }, }, { name: "should block transport-managed headers and skip sensitive ones", dest: http.Header{ "Content-Type": []string{"application/json"}, }, src: http.Header{ "Authorization": []string{"Bearer token"}, "Api-Key": []string{"key123"}, "X-Api-Key": []string{"xkey456"}, "X-Api-Secret": []string{"secret789"}, "X-Api-Token": []string{"token000"}, "Content-Type": []string{"text/plain"}, "Content-Length": []string{"100"}, "Transfer-Encoding": []string{"chunked"}, "User-Agent": []string{"Test/1.0"}, }, want: http.Header{ "Content-Type": []string{"application/json"}, "User-Agent": []string{"Test/1.0"}, }, }, { name: "empty src headers should not change dest", dest: http.Header{ "Content-Type": []string{"application/json"}, }, src: http.Header{}, want: http.Header{ "Content-Type": []string{"application/json"}, }, }, { name: "empty dest headers should merge non-blocked src headers", dest: http.Header{}, src: http.Header{ "User-Agent": []string{"Test/1.0"}, "Accept": []string{"*/*"}, "Authorization": []string{"Bearer token"}, }, want: http.Header{ "User-Agent": []string{"Test/1.0"}, "Accept": []string{"*/*"}, }, }, { name: "should merge multiple custom headers", dest: http.Header{ "Content-Type": []string{"application/json"}, }, src: http.Header{ "X-Request-ID": []string{"req-123"}, "X-Trace-ID": []string{"trace-456"}, "User-Agent": []string{"Custom/1.0"}, "Accept-Encoding": []string{"gzip, deflate"}, }, want: http.Header{ "Content-Type": []string{"application/json"}, "X-Request-ID": []string{"req-123"}, "X-Trace-ID": []string{"trace-456"}, "User-Agent": []string{"Custom/1.0"}, }, }, { name: "should handle headers with multiple values", dest: http.Header{ "Content-Type": []string{"application/json"}, }, src: http.Header{ "Accept": []string{"application/json", "text/plain"}, }, want: http.Header{ "Content-Type": []string{"application/json"}, "Accept": []string{"application/json", "text/plain"}, }, }, { name: "should add non-duplicate values when header exists", dest: http.Header{ "User-Agent": []string{"AxonHub/1.0"}, }, src: http.Header{ "Content-Type": []string{"application/json"}, "User-Agent": []string{"Mozilla/5.0"}, "Accept": []string{"*/*"}, }, want: http.Header{ "User-Agent": []string{"AxonHub/1.0", "Mozilla/5.0"}, "Accept": []string{"*/*"}, }, }, { name: "should overwrite non-appendable headers", dest: http.Header{ "X-Custom-Header": []string{"old-value"}, }, src: http.Header{ "X-Custom-Header": []string{"new-value"}, }, want: http.Header{ "X-Custom-Header": []string{"new-value"}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := MergeHTTPHeaders(tt.dest, tt.src) require.Equal(t, tt.want, got) }) } } func TestRegisterAppendHeaders(t *testing.T) { RegisterMergeWithAppendHeaders("X-New-Append") dest := http.Header{"X-New-Append": []string{"old"}} src := http.Header{"X-New-Append": []string{"new"}} got := MergeHTTPHeaders(dest, src) require.Equal(t, []string{"old", "new"}, got["X-New-Append"]) } func TestMergeHTTPQuery(t *testing.T) { tests := []struct { name string dest url.Values src url.Values want url.Values }{ { name: "should merge new query parameters", dest: url.Values{"q": []string{"golang"}}, src: url.Values{"page": []string{"1"}}, want: url.Values{"q": []string{"golang"}, "page": []string{"1"}}, }, { name: "should not overwrite existing query parameters", dest: url.Values{"q": []string{"golang"}}, src: url.Values{"q": []string{"java"}}, want: url.Values{"q": []string{"golang"}}, }, { name: "should handle empty src", dest: url.Values{"q": []string{"golang"}}, src: nil, want: url.Values{"q": []string{"golang"}}, }, { name: "should handle empty dest", dest: nil, src: url.Values{"page": []string{"1"}}, want: url.Values{"page": []string{"1"}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := MergeHTTPQuery(tt.dest, tt.src) require.Equal(t, tt.want, got) }) } } func TestMergeInboundRequest(t *testing.T) { t.Run("should merge both headers and query", func(t *testing.T) { dest := &Request{ Headers: http.Header{"Content-Type": []string{"application/json"}}, Query: url.Values{"q": []string{"old"}}, } src := &Request{ Headers: http.Header{"User-Agent": []string{"Test"}}, Query: url.Values{"page": []string{"1"}}, } got := MergeInboundRequest(dest, src) require.Equal(t, "application/json", got.Headers.Get("Content-Type")) require.Equal(t, "Test", got.Headers.Get("User-Agent")) require.Equal(t, "old", got.Query.Get("q")) require.Equal(t, "1", got.Query.Get("page")) }) t.Run("should return dest if src is nil", func(t *testing.T) { dest := &Request{Headers: http.Header{"X-Test": []string{"val"}}} got := MergeInboundRequest(dest, nil) require.Equal(t, dest, got) }) }