1 package ldclient
2
3 import (
4 "errors"
5 "testing"
6 "time"
7
8 "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest/mocks"
9
10 "github.com/launchdarkly/go-sdk-common/v3/lduser"
11 "github.com/launchdarkly/go-sdk-common/v3/ldvalue"
12 "github.com/launchdarkly/go-server-sdk/v6/interfaces"
13 "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest"
14 "github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
15 "github.com/launchdarkly/go-server-sdk/v6/subsystems"
16 "github.com/launchdarkly/go-server-sdk/v6/testhelpers/ldtestdata"
17
18 th "github.com/launchdarkly/go-test-helpers/v3"
19
20 "github.com/stretchr/testify/assert"
21 )
22
23
24
25
26
27
28
29
30
31
32 type clientListenersTestParams struct {
33 client *LDClient
34 testData *ldtestdata.TestDataSource
35 dataStoreUpdates subsystems.DataStoreUpdateSink
36 }
37
38 func clientListenersTest(action func(clientListenersTestParams)) {
39 clientListenersTestWithConfig(nil, action)
40 }
41
42 func clientListenersTestWithConfig(configAction func(*Config), action func(clientListenersTestParams)) {
43 testData := ldtestdata.DataSource()
44 capturingStoreConfigurer := &mocks.ComponentConfigurerThatCapturesClientContext[subsystems.DataStore]{
45 Configurer: ldcomponents.PersistentDataStore(
46 mocks.SingleComponentConfigurer[subsystems.PersistentDataStore]{Instance: mocks.NewMockPersistentDataStore()},
47 ),
48 }
49 config := Config{
50 DataSource: testData,
51 DataStore: capturingStoreConfigurer,
52 Events: ldcomponents.NoEvents(),
53 Logging: ldcomponents.Logging().Loggers(sharedtest.NewTestLoggers()),
54 }
55 if configAction != nil {
56 configAction(&config)
57 }
58 client, _ := MakeCustomClient(testSdkKey, config, 5*time.Second)
59 defer client.Close()
60 action(clientListenersTestParams{client, testData, capturingStoreConfigurer.ReceivedClientContext.GetDataStoreUpdateSink()})
61 }
62
63 func TestFlagTracker(t *testing.T) {
64 flagKey := "important-flag"
65 timeout := time.Millisecond * 100
66
67 t.Run("sends flag change events", func(t *testing.T) {
68 clientListenersTest(func(p clientListenersTestParams) {
69 p.testData.Update(p.testData.Flag(flagKey))
70
71 ch1 := p.client.GetFlagTracker().AddFlagChangeListener()
72 ch2 := p.client.GetFlagTracker().AddFlagChangeListener()
73
74 th.AssertNoMoreValues(t, ch1, timeout)
75 th.AssertNoMoreValues(t, ch2, timeout)
76
77 p.testData.Update(p.testData.Flag(flagKey))
78
79 sharedtest.ExpectFlagChangeEvents(t, ch1, flagKey)
80 sharedtest.ExpectFlagChangeEvents(t, ch2, flagKey)
81
82 p.client.GetFlagTracker().RemoveFlagChangeListener(ch1)
83 th.AssertChannelClosed(t, ch1, time.Millisecond)
84
85 p.testData.Update(p.testData.Flag(flagKey))
86
87 sharedtest.ExpectFlagChangeEvents(t, ch2, flagKey)
88 })
89 })
90
91 t.Run("sends flag value change events", func(t *testing.T) {
92 flagKey := "important-flag"
93 user := lduser.NewUser("important-user")
94 otherUser := lduser.NewUser("unimportant-user")
95
96 clientListenersTest(func(p clientListenersTestParams) {
97 p.testData.Update(p.testData.Flag(flagKey).VariationForAll(false))
98
99 ch1 := p.client.GetFlagTracker().AddFlagValueChangeListener(flagKey, user, ldvalue.Null())
100 ch2 := p.client.GetFlagTracker().AddFlagValueChangeListener(flagKey, user, ldvalue.Null())
101 ch3 := p.client.GetFlagTracker().AddFlagValueChangeListener(flagKey, otherUser, ldvalue.Null())
102
103 p.client.GetFlagTracker().RemoveFlagValueChangeListener(ch2)
104 th.AssertChannelClosed(t, ch2, time.Millisecond)
105
106 th.AssertNoMoreValues(t, ch1, timeout)
107 th.AssertNoMoreValues(t, ch3, timeout)
108
109
110 p.testData.Update(p.testData.Flag(flagKey).VariationForUser(user.Key(), true))
111
112
113 event1 := <-ch1
114 assert.Equal(t, flagKey, event1.Key)
115 assert.Equal(t, ldvalue.Bool(false), event1.OldValue)
116 assert.Equal(t, ldvalue.Bool(true), event1.NewValue)
117
118
119 th.AssertNoMoreValues(t, ch3, timeout)
120 })
121 })
122 }
123
124 func TestDataSourceStatusProvider(t *testing.T) {
125 t.Run("returns latest status", func(t *testing.T) {
126 timeBeforeStarting := time.Now()
127 clientListenersTest(func(p clientListenersTestParams) {
128 initialStatus := p.client.GetDataSourceStatusProvider().GetStatus()
129 assert.Equal(t, interfaces.DataSourceStateValid, initialStatus.State)
130 assert.False(t, initialStatus.StateSince.Before(timeBeforeStarting))
131 assert.Equal(t, interfaces.DataSourceErrorInfo{}, initialStatus.LastError)
132
133 errorInfo := interfaces.DataSourceErrorInfo{
134 Kind: interfaces.DataSourceErrorKindErrorResponse,
135 StatusCode: 401,
136 Time: time.Now(),
137 }
138 p.testData.UpdateStatus(interfaces.DataSourceStateOff, errorInfo)
139
140 newStatus := p.client.GetDataSourceStatusProvider().GetStatus()
141 assert.Equal(t, interfaces.DataSourceStateOff, newStatus.State)
142 assert.False(t, newStatus.StateSince.Before(errorInfo.Time))
143 assert.Equal(t, errorInfo, newStatus.LastError)
144 })
145 })
146
147 t.Run("sends status updates", func(t *testing.T) {
148 clientListenersTest(func(p clientListenersTestParams) {
149 statusCh := p.client.GetDataSourceStatusProvider().AddStatusListener()
150
151 errorInfo := interfaces.DataSourceErrorInfo{
152 Kind: interfaces.DataSourceErrorKindErrorResponse,
153 StatusCode: 401,
154 Time: time.Now(),
155 }
156 p.testData.UpdateStatus(interfaces.DataSourceStateOff, errorInfo)
157
158 newStatus := <-statusCh
159 assert.Equal(t, interfaces.DataSourceStateOff, newStatus.State)
160 assert.False(t, newStatus.StateSince.Before(errorInfo.Time))
161 assert.Equal(t, errorInfo, newStatus.LastError)
162 })
163 })
164 }
165
166 func TestDataStoreStatusProvider(t *testing.T) {
167 t.Run("returns latest status", func(t *testing.T) {
168 clientListenersTest(func(p clientListenersTestParams) {
169 originalStatus := interfaces.DataStoreStatus{Available: true}
170 newStatus := interfaces.DataStoreStatus{Available: false}
171
172 assert.Equal(t, originalStatus, p.client.GetDataStoreStatusProvider().GetStatus())
173
174 p.dataStoreUpdates.UpdateStatus(newStatus)
175
176 assert.Equal(t, newStatus, p.client.GetDataStoreStatusProvider().GetStatus())
177 })
178 })
179
180 t.Run("sends status updates", func(t *testing.T) {
181 clientListenersTest(func(p clientListenersTestParams) {
182 newStatus := interfaces.DataStoreStatus{Available: false}
183 statusCh := p.client.GetDataStoreStatusProvider().AddStatusListener()
184
185 p.dataStoreUpdates.UpdateStatus(newStatus)
186
187 s := th.RequireValue(t, statusCh, time.Second*2, "timed out waiting for new status")
188 assert.Equal(t, newStatus, s)
189 })
190 })
191 }
192
193 func TestBigSegmentsStoreStatusProvider(t *testing.T) {
194 t.Run("returns unavailable status when not configured", func(t *testing.T) {
195 clientListenersTest(func(p clientListenersTestParams) {
196 assert.Equal(t, interfaces.BigSegmentStoreStatus{},
197 p.client.GetBigSegmentStoreStatusProvider().GetStatus())
198 })
199 })
200
201 t.Run("sends status updates", func(t *testing.T) {
202 store := &mocks.MockBigSegmentStore{}
203 store.TestSetMetadataToCurrentTime()
204 storeFactory := mocks.SingleComponentConfigurer[subsystems.BigSegmentStore]{Instance: store}
205 clientListenersTestWithConfig(
206 func(c *Config) {
207 c.BigSegments = ldcomponents.BigSegments(storeFactory).StatusPollInterval(time.Millisecond * 10)
208 },
209 func(p clientListenersTestParams) {
210 statusCh := p.client.GetBigSegmentStoreStatusProvider().AddStatusListener()
211
212 mocks.ExpectBigSegmentStoreStatus(
213 t,
214 statusCh,
215 p.client.GetBigSegmentStoreStatusProvider().GetStatus,
216 time.Second,
217 interfaces.BigSegmentStoreStatus{Available: true},
218 )
219
220 store.TestSetMetadataState(subsystems.BigSegmentStoreMetadata{}, errors.New("failing"))
221
222 mocks.ExpectBigSegmentStoreStatus(
223 t,
224 statusCh,
225 p.client.GetBigSegmentStoreStatusProvider().GetStatus,
226 time.Second,
227 interfaces.BigSegmentStoreStatus{Available: false},
228 )
229 })
230 })
231 }
232
View as plain text