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
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)
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