1 package ldclient
2
3 import (
4 "crypto/x509"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9 "time"
10
11 "github.com/launchdarkly/go-sdk-common/v3/ldlog"
12 "github.com/launchdarkly/go-sdk-common/v3/ldlogtest"
13 "github.com/launchdarkly/go-sdk-common/v3/lduser"
14 "github.com/launchdarkly/go-sdk-common/v3/ldvalue"
15 "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
16 "github.com/launchdarkly/go-server-sdk/v6/interfaces"
17 "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest"
18 "github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
19 "github.com/launchdarkly/go-server-sdk/v6/testhelpers/ldservices"
20
21 "github.com/launchdarkly/go-test-helpers/v3/httphelpers"
22
23 "github.com/stretchr/testify/assert"
24 "github.com/stretchr/testify/require"
25 )
26
27 const (
28 initializationFailedErrorMessage = "LaunchDarkly client initialization failed"
29 pollingModeWarningMessage = "You should only disable the streaming API if instructed to do so by LaunchDarkly support"
30 )
31
32 var (
33 alwaysTrueFlag = ldbuilders.NewFlagBuilder("always-true-flag").SingleVariation(ldvalue.Bool(true)).Build()
34 testUser = lduser.NewUser("test-user-key")
35 )
36
37
38
39
40
41
42 func assertNoMoreRequests(t *testing.T, requestsCh <-chan httphelpers.HTTPRequestInfo) {
43 assert.Equal(t, 0, len(requestsCh))
44 }
45
46 func TestDefaultDataSourceIsStreaming(t *testing.T) {
47 data := ldservices.NewServerSDKData().Flags(&alwaysTrueFlag)
48 streamHandler, _ := ldservices.ServerSideStreamingServiceHandler(data.ToPutEvent())
49 httphelpers.WithServer(streamHandler, func(streamServer *httptest.Server) {
50 logCapture := ldlogtest.NewMockLog()
51 defer logCapture.DumpIfTestFailed(t)
52
53 config := Config{
54 Events: ldcomponents.NoEvents(),
55 Logging: ldcomponents.Logging().Loggers(logCapture.Loggers),
56 ServiceEndpoints: interfaces.ServiceEndpoints{Streaming: streamServer.URL},
57 }
58
59 client, err := MakeCustomClient(testSdkKey, config, time.Second*5)
60 require.NoError(t, err)
61 defer client.Close()
62
63 assert.Equal(t, string(interfaces.DataSourceStateValid), string(client.GetDataSourceStatusProvider().GetStatus().State))
64
65 value, _ := client.BoolVariation(alwaysTrueFlag.Key, testUser, false)
66 assert.True(t, value)
67 })
68 }
69
70 func TestClientStartsInStreamingMode(t *testing.T) {
71 data := ldservices.NewServerSDKData().Flags(&alwaysTrueFlag)
72 streamHandler, _ := ldservices.ServerSideStreamingServiceHandler(data.ToPutEvent())
73 handler, requestsCh := httphelpers.RecordingHandler(streamHandler)
74 httphelpers.WithServer(handler, func(streamServer *httptest.Server) {
75 logCapture := ldlogtest.NewMockLog()
76 defer logCapture.DumpIfTestFailed(t)
77
78 config := Config{
79 Events: ldcomponents.NoEvents(),
80 Logging: ldcomponents.Logging().Loggers(logCapture.Loggers),
81 ServiceEndpoints: interfaces.ServiceEndpoints{Streaming: streamServer.URL},
82 }
83
84 client, err := MakeCustomClient(testSdkKey, config, time.Second*5)
85 require.NoError(t, err)
86 defer client.Close()
87
88 assert.Equal(t, string(interfaces.DataSourceStateValid), string(client.GetDataSourceStatusProvider().GetStatus().State))
89
90 value, _ := client.BoolVariation(alwaysTrueFlag.Key, testUser, false)
91 assert.True(t, value)
92
93 r := <-requestsCh
94 assert.Equal(t, testSdkKey, r.Request.Header.Get("Authorization"))
95 assertNoMoreRequests(t, requestsCh)
96
97 assert.Len(t, logCapture.GetOutput(ldlog.Error), 0)
98 assert.Len(t, logCapture.GetOutput(ldlog.Warn), 0)
99 })
100 }
101
102 func TestClientFailsToStartInStreamingModeWith401Error(t *testing.T) {
103 handler, requestsCh := httphelpers.RecordingHandler(httphelpers.HandlerWithStatus(401))
104 httphelpers.WithServer(handler, func(streamServer *httptest.Server) {
105 logCapture := ldlogtest.NewMockLog()
106
107 config := Config{
108 Events: ldcomponents.NoEvents(),
109 Logging: ldcomponents.Logging().Loggers(logCapture.Loggers),
110 ServiceEndpoints: interfaces.ServiceEndpoints{Streaming: streamServer.URL},
111 }
112
113 client, err := MakeCustomClient(testSdkKey, config, time.Second*5)
114 require.Error(t, err)
115 require.NotNil(t, client)
116 defer client.Close()
117
118 assert.Equal(t, initializationFailedErrorMessage, err.Error())
119
120 assert.Equal(t, string(interfaces.DataSourceStateOff), string(client.GetDataSourceStatusProvider().GetStatus().State))
121
122 value, _ := client.BoolVariation(alwaysTrueFlag.Key, testUser, false)
123 assert.False(t, value)
124
125 r := <-requestsCh
126 assert.Equal(t, testSdkKey, r.Request.Header.Get("Authorization"))
127 assertNoMoreRequests(t, requestsCh)
128
129 expectedError := "Error in stream connection (giving up permanently): HTTP error 401 (invalid SDK key)"
130 assert.Equal(t, []string{expectedError}, logCapture.GetOutput(ldlog.Error))
131 assert.Equal(t, []string{initializationFailedErrorMessage}, logCapture.GetOutput(ldlog.Warn))
132 })
133 }
134
135 func TestClientRetriesConnectionInStreamingModeWithNonFatalError(t *testing.T) {
136 data := ldservices.NewServerSDKData().Flags(&alwaysTrueFlag)
137 streamHandler, _ := ldservices.ServerSideStreamingServiceHandler(data.ToPutEvent())
138 failThenSucceedHandler := httphelpers.SequentialHandler(httphelpers.HandlerWithStatus(503), streamHandler)
139 handler, requestsCh := httphelpers.RecordingHandler(failThenSucceedHandler)
140 httphelpers.WithServer(handler, func(streamServer *httptest.Server) {
141 logCapture := ldlogtest.NewMockLog()
142
143 config := Config{
144 Events: ldcomponents.NoEvents(),
145 Logging: ldcomponents.Logging().Loggers(logCapture.Loggers),
146 ServiceEndpoints: interfaces.ServiceEndpoints{Streaming: streamServer.URL},
147 }
148
149 client, err := MakeCustomClient(testSdkKey, config, time.Second*5)
150 require.NoError(t, err)
151 defer client.Close()
152
153 assert.Equal(t, string(interfaces.DataSourceStateValid), string(client.GetDataSourceStatusProvider().GetStatus().State))
154
155 value, _ := client.BoolVariation(alwaysTrueFlag.Key, testUser, false)
156 assert.True(t, value)
157
158 r0 := <-requestsCh
159 assert.Equal(t, testSdkKey, r0.Request.Header.Get("Authorization"))
160 r1 := <-requestsCh
161 assert.Equal(t, testSdkKey, r1.Request.Header.Get("Authorization"))
162 assertNoMoreRequests(t, requestsCh)
163
164 expectedWarning := "Error in stream connection (will retry): HTTP error 503"
165 assert.Equal(t, []string{expectedWarning}, logCapture.GetOutput(ldlog.Warn))
166 assert.Len(t, logCapture.GetOutput(ldlog.Error), 0)
167 })
168 }
169
170 func TestClientStartsInPollingMode(t *testing.T) {
171 data := ldservices.NewServerSDKData().Flags(&alwaysTrueFlag)
172 pollHandler, requestsCh := httphelpers.RecordingHandler(ldservices.ServerSidePollingServiceHandler(data))
173 httphelpers.WithServer(pollHandler, func(pollServer *httptest.Server) {
174 logCapture := ldlogtest.NewMockLog()
175
176 config := Config{
177 DataSource: ldcomponents.PollingDataSource(),
178 Events: ldcomponents.NoEvents(),
179 Logging: ldcomponents.Logging().Loggers(logCapture.Loggers),
180 ServiceEndpoints: interfaces.ServiceEndpoints{Polling: pollServer.URL},
181 }
182
183 client, err := MakeCustomClient(testSdkKey, config, time.Second*5)
184 require.NoError(t, err)
185 defer client.Close()
186
187 assert.Equal(t, string(interfaces.DataSourceStateValid), string(client.GetDataSourceStatusProvider().GetStatus().State))
188
189 value, _ := client.BoolVariation(alwaysTrueFlag.Key, testUser, false)
190 assert.True(t, value)
191
192 r := <-requestsCh
193 assert.Equal(t, testSdkKey, r.Request.Header.Get("Authorization"))
194 assertNoMoreRequests(t, requestsCh)
195
196 assert.Len(t, logCapture.GetOutput(ldlog.Error), 0)
197 assert.Equal(t, []string{pollingModeWarningMessage}, logCapture.GetOutput(ldlog.Warn))
198 })
199 }
200
201 func TestClientFailsToStartInPollingModeWith401Error(t *testing.T) {
202 handler, requestsCh := httphelpers.RecordingHandler(httphelpers.HandlerWithStatus(401))
203 httphelpers.WithServer(handler, func(pollServer *httptest.Server) {
204 logCapture := ldlogtest.NewMockLog()
205
206 config := Config{
207 DataSource: ldcomponents.PollingDataSource(),
208 Events: ldcomponents.NoEvents(),
209 Logging: ldcomponents.Logging().Loggers(logCapture.Loggers),
210 ServiceEndpoints: interfaces.ServiceEndpoints{Polling: pollServer.URL},
211 }
212
213 client, err := MakeCustomClient(testSdkKey, config, time.Second*5)
214 require.Error(t, err)
215 require.NotNil(t, client)
216 defer client.Close()
217
218 assert.Equal(t, initializationFailedErrorMessage, err.Error())
219
220 assert.Equal(t, string(interfaces.DataSourceStateOff), string(client.GetDataSourceStatusProvider().GetStatus().State))
221
222 value, _ := client.BoolVariation(alwaysTrueFlag.Key, testUser, false)
223 assert.False(t, value)
224
225 r := <-requestsCh
226 assert.Equal(t, testSdkKey, r.Request.Header.Get("Authorization"))
227 assertNoMoreRequests(t, requestsCh)
228
229 expectedError := "Error on polling request (giving up permanently): HTTP error 401 (invalid SDK key)"
230 assert.Equal(t, []string{expectedError}, logCapture.GetOutput(ldlog.Error))
231 assert.Equal(t, []string{pollingModeWarningMessage, initializationFailedErrorMessage}, logCapture.GetOutput(ldlog.Warn))
232 })
233 }
234
235 func TestClientSendsEventWithoutDiagnostics(t *testing.T) {
236 eventsHandler, eventRequestsCh := httphelpers.RecordingHandler(ldservices.ServerSideEventsServiceHandler())
237 httphelpers.WithServer(eventsHandler, func(eventsServer *httptest.Server) {
238 data := ldservices.NewServerSDKData().Flags(&alwaysTrueFlag)
239 streamHandler, _ := ldservices.ServerSideStreamingServiceHandler(data.ToPutEvent())
240 httphelpers.WithServer(streamHandler, func(streamServer *httptest.Server) {
241 logCapture := ldlogtest.NewMockLog()
242
243 config := Config{
244 DiagnosticOptOut: true,
245 Logging: ldcomponents.Logging().Loggers(logCapture.Loggers),
246 ServiceEndpoints: interfaces.ServiceEndpoints{Streaming: streamServer.URL, Events: eventsServer.URL},
247 }
248
249 client, err := MakeCustomClient(testSdkKey, config, time.Second*5)
250 require.NoError(t, err)
251 defer client.Close()
252
253 client.Identify(testUser)
254 client.Flush()
255
256 r := <-eventRequestsCh
257 assert.Equal(t, testSdkKey, r.Request.Header.Get("Authorization"))
258 assert.Equal(t, "/bulk", r.Request.URL.Path)
259 assertNoMoreRequests(t, eventRequestsCh)
260
261 var jsonValue ldvalue.Value
262 err = json.Unmarshal(r.Body, &jsonValue)
263 assert.NoError(t, err)
264 assert.Equal(t, ldvalue.String("identify"), jsonValue.GetByIndex(0).GetByKey("kind"))
265 })
266 })
267 }
268
269 func TestClientSendsDiagnostics(t *testing.T) {
270 eventsHandler, eventRequestsCh := httphelpers.RecordingHandler(ldservices.ServerSideEventsServiceHandler())
271 httphelpers.WithServer(eventsHandler, func(eventsServer *httptest.Server) {
272 data := ldservices.NewServerSDKData().Flags(&alwaysTrueFlag)
273 streamHandler, _ := ldservices.ServerSideStreamingServiceHandler(data.ToPutEvent())
274 httphelpers.WithServer(streamHandler, func(streamServer *httptest.Server) {
275 config := Config{
276 Logging: ldcomponents.Logging().Loggers(sharedtest.NewTestLoggers()),
277 ServiceEndpoints: interfaces.ServiceEndpoints{Streaming: streamServer.URL, Events: eventsServer.URL},
278 }
279
280 client, err := MakeCustomClient(testSdkKey, config, time.Second*5)
281 require.NoError(t, err)
282 defer client.Close()
283
284 r := <-eventRequestsCh
285 assert.Equal(t, testSdkKey, r.Request.Header.Get("Authorization"))
286 assert.Equal(t, "/diagnostic", r.Request.URL.Path)
287 var jsonValue ldvalue.Value
288 err = json.Unmarshal(r.Body, &jsonValue)
289 assert.NoError(t, err)
290 assert.Equal(t, ldvalue.String("diagnostic-init"), jsonValue.GetByKey("kind"))
291 })
292 })
293 }
294
295 func TestClientUsesCustomTLSConfiguration(t *testing.T) {
296 data := ldservices.NewServerSDKData().Flags(&alwaysTrueFlag)
297 streamHandler, _ := ldservices.ServerSideStreamingServiceHandler(data.ToPutEvent())
298
299 httphelpers.WithSelfSignedServer(streamHandler, func(server *httptest.Server, certData []byte, certs *x509.CertPool) {
300 config := Config{
301 Events: ldcomponents.NoEvents(),
302 HTTP: ldcomponents.HTTPConfiguration().CACert(certData),
303 Logging: ldcomponents.Logging().Loggers(sharedtest.NewTestLoggers()),
304 ServiceEndpoints: interfaces.ServiceEndpoints{Streaming: server.URL},
305 }
306
307 client, err := MakeCustomClient(testSdkKey, config, time.Second*5)
308 require.NoError(t, err)
309 defer client.Close()
310
311 value, _ := client.BoolVariation(alwaysTrueFlag.Key, testUser, false)
312 assert.True(t, value)
313 })
314 }
315
316 func TestClientStartupTimesOut(t *testing.T) {
317 data := ldservices.NewServerSDKData().Flags(&alwaysTrueFlag)
318 streamHandler, _ := ldservices.ServerSideStreamingServiceHandler(data.ToPutEvent())
319 slowHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
320 time.Sleep(300 * time.Millisecond)
321 streamHandler.ServeHTTP(w, r)
322 })
323
324 httphelpers.WithServer(slowHandler, func(streamServer *httptest.Server) {
325 logCapture := ldlogtest.NewMockLog()
326
327 config := Config{
328 Events: ldcomponents.NoEvents(),
329 Logging: ldcomponents.Logging().Loggers(logCapture.Loggers),
330 ServiceEndpoints: interfaces.ServiceEndpoints{Streaming: streamServer.URL},
331 }
332
333 client, err := MakeCustomClient(testSdkKey, config, time.Millisecond*100)
334 require.Error(t, err)
335 require.NotNil(t, client)
336 defer client.Close()
337
338 assert.Equal(t, "timeout encountered waiting for LaunchDarkly client initialization", err.Error())
339
340 value, _ := client.BoolVariation(alwaysTrueFlag.Key, testUser, false)
341 assert.False(t, value)
342
343 assert.Equal(t, []string{"Timeout encountered waiting for LaunchDarkly client initialization"}, logCapture.GetOutput(ldlog.Warn))
344 assert.Len(t, logCapture.GetOutput(ldlog.Error), 0)
345 })
346 }
347
View as plain text