...

Source file src/github.com/launchdarkly/go-server-sdk/v6/internal/datasource/requestor_test.go

Documentation: github.com/launchdarkly/go-server-sdk/v6/internal/datasource

     1  package datasource
     2  
     3  import (
     4  	"net/http"
     5  	"net/http/httptest"
     6  	"testing"
     7  
     8  	"github.com/launchdarkly/go-sdk-common/v3/ldlog"
     9  	"github.com/launchdarkly/go-sdk-common/v3/ldlogtest"
    10  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
    11  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
    12  	"github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest"
    13  	"github.com/launchdarkly/go-server-sdk/v6/subsystems"
    14  	"github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoretypes"
    15  	"github.com/launchdarkly/go-server-sdk/v6/testhelpers/ldservices"
    16  
    17  	"github.com/launchdarkly/go-test-helpers/v3/httphelpers"
    18  
    19  	"github.com/stretchr/testify/assert"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  // this mock is not used in the tests in this file; it's used in the polling/streaming data source tests
    24  type mockRequestor struct {
    25  	requestAllRespCh      chan mockRequestAllResponse
    26  	requestResourceRespCh chan mockRequestResourceResponse
    27  	pollsCh               chan struct{}
    28  	closerCh              chan struct{}
    29  }
    30  
    31  type mockRequestAllResponse struct {
    32  	data   []ldstoretypes.Collection
    33  	cached bool
    34  	err    error
    35  }
    36  
    37  type mockRequestResourceResponse struct {
    38  	item ldstoretypes.ItemDescriptor
    39  	err  error
    40  }
    41  
    42  func newMockRequestor() *mockRequestor {
    43  	return &mockRequestor{
    44  		requestAllRespCh: make(chan mockRequestAllResponse, 100),
    45  		pollsCh:          make(chan struct{}, 100),
    46  		closerCh:         make(chan struct{}),
    47  	}
    48  }
    49  
    50  func (r *mockRequestor) Close() {
    51  	close(r.closerCh)
    52  }
    53  
    54  func (r *mockRequestor) requestAll() ([]ldstoretypes.Collection, bool, error) {
    55  	select {
    56  	case resp := <-r.requestAllRespCh:
    57  		r.pollsCh <- struct{}{}
    58  		return resp.data, resp.cached, resp.err
    59  	case <-r.closerCh:
    60  		return nil, false, nil
    61  	}
    62  }
    63  
    64  func TestRequestorImplRequestAll(t *testing.T) {
    65  	t.Run("success", func(t *testing.T) {
    66  		flag := ldbuilders.NewFlagBuilder("flagkey").Version(1).SingleVariation(ldvalue.Bool(true)).Build()
    67  		segment := ldbuilders.NewSegmentBuilder("segmentkey").Version(1).Build()
    68  		expectedData := sharedtest.NewDataSetBuilder().Flags(flag).Segments(segment)
    69  		handler, requestsCh := httphelpers.RecordingHandler(
    70  			ldservices.ServerSidePollingServiceHandler(expectedData.ToServerSDKData()),
    71  		)
    72  		httphelpers.WithServer(handler, func(ts *httptest.Server) {
    73  			r := newRequestorImpl(basicClientContext(), nil, ts.URL)
    74  
    75  			data, cached, err := r.requestAll()
    76  
    77  			assert.NoError(t, err)
    78  			assert.False(t, cached)
    79  
    80  			assert.Equal(t, sharedtest.NormalizeDataSet(expectedData.Build()), sharedtest.NormalizeDataSet(data))
    81  
    82  			req := <-requestsCh
    83  			assert.Equal(t, "/sdk/latest-all", req.Request.URL.String())
    84  		})
    85  	})
    86  
    87  	t.Run("HTTP error response", func(t *testing.T) {
    88  		handler := httphelpers.HandlerWithStatus(500)
    89  		httphelpers.WithServer(handler, func(ts *httptest.Server) {
    90  			r := newRequestorImpl(basicClientContext(), nil, ts.URL)
    91  
    92  			data, cached, err := r.requestAll()
    93  
    94  			assert.Error(t, err)
    95  			if he, ok := err.(httpStatusError); assert.True(t, ok) {
    96  				assert.Equal(t, 500, he.Code)
    97  			}
    98  			assert.False(t, cached)
    99  			assert.Nil(t, data)
   100  		})
   101  	})
   102  
   103  	t.Run("network error", func(t *testing.T) {
   104  		var closedServerURL string
   105  		handler := httphelpers.HandlerWithJSONResponse(ldservices.NewServerSDKData(), nil)
   106  		httphelpers.WithServer(handler, func(ts *httptest.Server) {
   107  			closedServerURL = ts.URL
   108  		})
   109  		r := newRequestorImpl(basicClientContext(), nil, closedServerURL)
   110  
   111  		data, cached, err := r.requestAll()
   112  
   113  		assert.Error(t, err)
   114  		assert.False(t, cached)
   115  		assert.Nil(t, data)
   116  	})
   117  
   118  	t.Run("malformed data", func(t *testing.T) {
   119  		handler := httphelpers.HandlerWithResponse(200, nil, []byte("{"))
   120  		httphelpers.WithServer(handler, func(ts *httptest.Server) {
   121  			r := newRequestorImpl(basicClientContext(), nil, ts.URL)
   122  
   123  			data, cached, err := r.requestAll()
   124  
   125  			require.Error(t, err)
   126  			_, ok := err.(malformedJSONError)
   127  			assert.True(t, ok)
   128  			assert.False(t, cached)
   129  			assert.Nil(t, data)
   130  		})
   131  	})
   132  
   133  	t.Run("malformed base URI", func(t *testing.T) {
   134  		r := newRequestorImpl(basicClientContext(), nil, "::::")
   135  
   136  		data, cached, err := r.requestAll()
   137  
   138  		require.Error(t, err)
   139  		assert.Contains(t, err.Error(), "missing protocol scheme")
   140  		assert.False(t, cached)
   141  		assert.Nil(t, data)
   142  	})
   143  
   144  	t.Run("sends configured headers", func(t *testing.T) {
   145  		headers := make(http.Header)
   146  		headers.Set("my-header", "my-value")
   147  		handler, requestsCh := httphelpers.RecordingHandler(
   148  			httphelpers.HandlerWithJSONResponse(ldservices.NewServerSDKData(), nil),
   149  		)
   150  		httpConfig := subsystems.HTTPConfiguration{DefaultHeaders: headers}
   151  		context := sharedtest.NewTestContext(testSDKKey, &httpConfig, nil)
   152  
   153  		httphelpers.WithServer(handler, func(ts *httptest.Server) {
   154  			r := newRequestorImpl(context, nil, ts.URL)
   155  
   156  			_, _, err := r.requestAll()
   157  			assert.NoError(t, err)
   158  
   159  			req := <-requestsCh
   160  			assert.Equal(t, "my-value", req.Request.Header.Get("my-header"))
   161  		})
   162  	})
   163  
   164  	t.Run("logs debug message", func(t *testing.T) {
   165  		mockLog := ldlogtest.NewMockLog()
   166  		mockLog.Loggers.SetMinLevel(ldlog.Debug)
   167  		context := sharedtest.NewTestContext(testSDKKey, nil, &subsystems.LoggingConfiguration{Loggers: mockLog.Loggers})
   168  		handler := httphelpers.HandlerWithJSONResponse(ldservices.NewServerSDKData(), nil)
   169  
   170  		httphelpers.WithServer(handler, func(ts *httptest.Server) {
   171  			r := newRequestorImpl(context, nil, ts.URL)
   172  
   173  			_, _, err := r.requestAll()
   174  			assert.NoError(t, err)
   175  
   176  			assert.Equal(t, []string{"Polling LaunchDarkly for feature flag updates"},
   177  				mockLog.GetOutput(ldlog.Debug))
   178  		})
   179  	})
   180  }
   181  
   182  func TestRequestorImplCaching(t *testing.T) {
   183  	flag := ldbuilders.NewFlagBuilder("flagkey").Version(1).SingleVariation(ldvalue.Bool(true)).Build()
   184  	expectedData := sharedtest.NewDataSetBuilder().Flags(flag)
   185  	etag := "123"
   186  	handler, requestsCh := httphelpers.RecordingHandler(
   187  		httphelpers.SequentialHandler(
   188  			http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   189  				w.Header().Set("ETag", etag)
   190  				w.Header().Set("Cache-Control", "max-age=0")
   191  				ldservices.ServerSidePollingServiceHandler(expectedData.ToServerSDKData()).ServeHTTP(w, r)
   192  			}),
   193  			httphelpers.HandlerWithStatus(304),
   194  		),
   195  	)
   196  	httphelpers.WithServer(handler, func(ts *httptest.Server) {
   197  		r := newRequestorImpl(basicClientContext(), nil, ts.URL)
   198  
   199  		data1, cached1, err1 := r.requestAll()
   200  
   201  		assert.NoError(t, err1)
   202  		assert.False(t, cached1)
   203  		assert.Equal(t, sharedtest.NormalizeDataSet(expectedData.Build()), sharedtest.NormalizeDataSet(data1))
   204  
   205  		req1 := <-requestsCh
   206  		assert.Equal(t, "/sdk/latest-all", req1.Request.URL.String())
   207  		assert.Equal(t, "", req1.Request.Header.Get("If-None-Match"))
   208  
   209  		data2, cached2, err2 := r.requestAll()
   210  
   211  		assert.NoError(t, err2)
   212  		assert.True(t, cached2)
   213  		assert.Nil(t, data2) // for cached data, requestAll doesn't bother parsing the body
   214  
   215  		req2 := <-requestsCh
   216  		assert.Equal(t, "/sdk/latest-all", req2.Request.URL.String())
   217  		assert.Equal(t, etag, req2.Request.Header.Get("If-None-Match"))
   218  	})
   219  }
   220  
   221  func TestRequestorImplCanUseCustomHTTPClientFactory(t *testing.T) {
   222  	data := ldservices.NewServerSDKData().Flags(ldservices.FlagOrSegment("my-flag", 2))
   223  	pollHandler, requestsCh := httphelpers.RecordingHandler(ldservices.ServerSidePollingServiceHandler(data))
   224  	httpClientFactory := urlAppendingHTTPClientFactory("/transformed")
   225  	httpConfig := subsystems.HTTPConfiguration{CreateHTTPClient: httpClientFactory}
   226  	context := sharedtest.NewTestContext(testSDKKey, &httpConfig, nil)
   227  
   228  	httphelpers.WithServer(pollHandler, func(ts *httptest.Server) {
   229  		r := newRequestorImpl(context, nil, ts.URL)
   230  
   231  		_, _, _ = r.requestAll()
   232  
   233  		req := <-requestsCh
   234  
   235  		assert.Equal(t, "/sdk/latest-all/transformed", req.Request.URL.Path)
   236  	})
   237  }
   238  

View as plain text