package ldcomponents import ( "crypto/x509" "net/http" "net/http/httptest" "net/url" "os" "reflect" "testing" "time" "github.com/launchdarkly/go-server-sdk/v6/interfaces" "github.com/launchdarkly/go-server-sdk/v6/internal" "github.com/launchdarkly/go-server-sdk/v6/subsystems" helpers "github.com/launchdarkly/go-test-helpers/v3" "github.com/launchdarkly/go-test-helpers/v3/httphelpers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHTTPConfigurationBuilder(t *testing.T) { basicConfig := subsystems.BasicClientContext{SDKKey: "test-key"} t.Run("defaults", func(t *testing.T) { c, err := HTTPConfiguration().Build(basicConfig) require.NoError(t, err) headers := c.DefaultHeaders assert.Len(t, headers, 2) assert.Equal(t, "test-key", headers.Get("Authorization")) assert.Equal(t, "GoClient/"+internal.SDKVersion, headers.Get("User-Agent")) client := c.CreateHTTPClient() assert.Equal(t, DefaultConnectTimeout, client.Timeout) require.NotNil(t, client.Transport) transport := client.Transport.(*http.Transport) require.NotNil(t, transport) assert.Equal(t, reflect.ValueOf(http.ProxyFromEnvironment).Pointer(), reflect.ValueOf(transport.Proxy).Pointer()) assert.Equal(t, 100, transport.MaxIdleConns) assert.Equal(t, 90*time.Second, transport.IdleConnTimeout) assert.Equal(t, 10*time.Second, transport.TLSHandshakeTimeout) assert.Equal(t, 1*time.Second, transport.ExpectContinueTimeout) }) t.Run("CACert", func(t *testing.T) { httphelpers.WithSelfSignedServer(httphelpers.HandlerWithStatus(200), func(server *httptest.Server, certData []byte, certs *x509.CertPool) { _, err := HTTPConfiguration(). CACert(certData). Build(basicConfig) require.NoError(t, err) }) }) t.Run("CACert with invalid certificate", func(t *testing.T) { badCertData := []byte("no") _, err := HTTPConfiguration(). CACert(badCertData). Build(basicConfig) require.Error(t, err) }) t.Run("CACertFile", func(t *testing.T) { httphelpers.WithSelfSignedServer(httphelpers.HandlerWithStatus(200), func(server *httptest.Server, certData []byte, certs *x509.CertPool) { helpers.WithTempFileData(certData, func(filename string) { _, err := HTTPConfiguration(). CACertFile(filename). Build(basicConfig) require.NoError(t, err) }) }) }) t.Run("CACertFile with invalid certificate", func(t *testing.T) { badCertData := []byte("no") helpers.WithTempFileData(badCertData, func(filename string) { _, err := HTTPConfiguration(). CACertFile(filename). Build(basicConfig) require.Error(t, err) }) }) t.Run("CACertFile with missing file", func(t *testing.T) { helpers.WithTempFile(func(filename string) { _ = os.Remove(filename) _, err := HTTPConfiguration(). CACertFile(filename). Build(basicConfig) require.Error(t, err) }) }) t.Run("ConnectTimeout", func(t *testing.T) { timeout := 700 * time.Millisecond c1, err := HTTPConfiguration(). ConnectTimeout(timeout). Build(basicConfig) require.NoError(t, err) client1 := c1.CreateHTTPClient() assert.Equal(t, timeout, client1.Timeout) c2, err := HTTPConfiguration(). ConnectTimeout(-1 * time.Millisecond). Build(basicConfig) require.NoError(t, err) client2 := c2.CreateHTTPClient() assert.Equal(t, DefaultConnectTimeout, client2.Timeout) }) t.Run("HTTPClientFactory", func(t *testing.T) { hc := &http.Client{Timeout: time.Hour} c, err := HTTPConfiguration(). HTTPClientFactory(func() *http.Client { return hc }). Build(basicConfig) require.NoError(t, err) assert.Equal(t, hc, c.CreateHTTPClient()) }) t.Run("ProxyURL", func(t *testing.T) { // Create a fake proxy server - really it's just an embedded HTTP server that always // returns a 200 status, but the Go HTTP client doesn't know the difference. Seeing // a request arrive at our server, with an absolute URL of http://example/ - instead // of the request actually going to http://example/ - proves that the proxy setting // was respected. fakeTargetURL := "http://example/" handler, requestsCh := httphelpers.RecordingHandler(httphelpers.HandlerWithStatus(200)) httphelpers.WithServer(handler, func(server *httptest.Server) { c, err := HTTPConfiguration(). ProxyURL(server.URL). Build(basicConfig) require.NoError(t, err) client := c.CreateHTTPClient() resp, err := client.Get(fakeTargetURL) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) r := <-requestsCh assert.Equal(t, fakeTargetURL, r.Request.RequestURI) }) }) t.Run("ProxyURL with basicauth", func(t *testing.T) { // See comment in previous test fakeTargetURL := "http://example/" handler, requestsCh := httphelpers.RecordingHandler(httphelpers.HandlerWithStatus(200)) httphelpers.WithServer(handler, func(server *httptest.Server) { parsedURL, _ := url.Parse(server.URL) urlWithCredentials := "http://lucy:cat@" + parsedURL.Host c, err := HTTPConfiguration(). ProxyURL(urlWithCredentials). Build(basicConfig) require.NoError(t, err) client := c.CreateHTTPClient() resp, err := client.Get(fakeTargetURL) require.NoError(t, err) assert.Equal(t, 200, resp.StatusCode) r := <-requestsCh assert.Equal(t, fakeTargetURL, r.Request.RequestURI) assert.Equal(t, "Basic bHVjeTpjYXQ=", r.Request.Header.Get("Proxy-Authorization")) }) }) t.Run("ProxyURL with invalid URL", func(t *testing.T) { proxyURL := ":///" _, err := HTTPConfiguration(). ProxyURL(proxyURL). Build(basicConfig) require.Error(t, err) }) t.Run("Custom header set/get", func(t *testing.T) { c, err := HTTPConfiguration(). Header("Custom-Header", "foo"). Build(basicConfig) require.NoError(t, err) assert.Equal(t, "foo", c.DefaultHeaders.Get("Custom-Header")) }) t.Run("Repeat assignments of custom header take latest value", func(t *testing.T) { c, err := HTTPConfiguration(). Header("Custom-Header", "foo"). Header("Custom-Header", "bar"). Header("Custom-Header", "baz"). Build(basicConfig) require.NoError(t, err) assert.Equal(t, "baz", c.DefaultHeaders.Get("Custom-Header")) }) t.Run("Custom header values overwrite required headers", func(t *testing.T) { c, err := HTTPConfiguration(). Header("User-Agent", "foo"). Header("Authorization", "bar"). Build(basicConfig) require.NoError(t, err) assert.Equal(t, "foo", c.DefaultHeaders.Get("User-Agent")) assert.Equal(t, "bar", c.DefaultHeaders.Get("Authorization")) }) t.Run("User-Agent", func(t *testing.T) { c, err := HTTPConfiguration(). UserAgent("extra"). Build(basicConfig) require.NoError(t, err) assert.Equal(t, "GoClient/"+internal.SDKVersion+" extra", c.DefaultHeaders.Get("User-Agent")) }) t.Run("Wrapper", func(t *testing.T) { c1, err := HTTPConfiguration(). Wrapper("FancySDK", ""). Build(basicConfig) require.NoError(t, err) assert.Equal(t, "FancySDK", c1.DefaultHeaders.Get("X-LaunchDarkly-Wrapper")) c2, err := HTTPConfiguration(). Wrapper("FancySDK", "2.0"). Build(basicConfig) require.NoError(t, err) assert.Equal(t, "FancySDK/2.0", c2.DefaultHeaders.Get("X-LaunchDarkly-Wrapper")) }) t.Run("tags header", func(t *testing.T) { t.Run("no tags", func(t *testing.T) { c, err := HTTPConfiguration().Build(basicConfig) require.NoError(t, err) assert.Nil(t, c.DefaultHeaders.Values("X-LaunchDarkly-Tags")) }) t.Run("some tags", func(t *testing.T) { bc := basicConfig bc.ApplicationInfo = interfaces.ApplicationInfo{ApplicationID: "appid", ApplicationVersion: "appver"} c, err := HTTPConfiguration().Build(bc) require.NoError(t, err) assert.Equal(t, "application-id/appid application-version/appver", c.DefaultHeaders.Get("X-LaunchDarkly-Tags")) }) }) t.Run("nil safety", func(t *testing.T) { var b *HTTPConfigurationBuilder = nil b = b.ConnectTimeout(0).Header("a", "b").ProxyURL("c").Wrapper("d", "e") _ = b.DescribeConfiguration(subsystems.BasicClientContext{}) _, _ = b.Build(subsystems.BasicClientContext{}) }) }