1 package storetest
2
3 import (
4 "testing"
5 "time"
6
7 "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest/mocks"
8
9 "github.com/launchdarkly/go-sdk-common/v3/ldcontext"
10 "github.com/launchdarkly/go-sdk-common/v3/ldlog"
11 "github.com/launchdarkly/go-sdk-common/v3/ldlogtest"
12 "github.com/launchdarkly/go-sdk-common/v3/ldreason"
13 "github.com/launchdarkly/go-sdk-common/v3/ldvalue"
14 "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
15 "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
16 ld "github.com/launchdarkly/go-server-sdk/v6"
17 "github.com/launchdarkly/go-server-sdk/v6/internal/datakinds"
18 sh "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest"
19 "github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
20 ssys "github.com/launchdarkly/go-server-sdk/v6/subsystems"
21 st "github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoretypes"
22 "github.com/launchdarkly/go-server-sdk/v6/testhelpers"
23
24 "github.com/launchdarkly/go-test-helpers/v3/testbox"
25
26 "github.com/stretchr/testify/assert"
27 "github.com/stretchr/testify/require"
28 )
29
30 func assertEqualsSerializedItem(
31 t assert.TestingT,
32 item mocks.MockDataItem,
33 serializedItemDesc st.SerializedItemDescriptor,
34 ) {
35
36
37 assert.Equal(t, item.ToSerializedItemDescriptor().SerializedItem, serializedItemDesc.SerializedItem)
38 if serializedItemDesc.Version != 0 {
39 assert.Equal(t, item.Version, serializedItemDesc.Version)
40 }
41 }
42
43 func assertEqualsDeletedItem(
44 t assert.TestingT,
45 expected st.SerializedItemDescriptor,
46 actual st.SerializedItemDescriptor,
47 ) {
48
49
50 if actual.SerializedItem == nil {
51 assert.True(t, actual.Deleted)
52 assert.Equal(t, expected.Version, actual.Version)
53 } else {
54 itemDesc, err := mocks.MockData.Deserialize(actual.SerializedItem)
55 assert.NoError(t, err)
56 assert.Equal(t, st.ItemDescriptor{Version: expected.Version}, itemDesc)
57 }
58 }
59
60
61
62
63
64
65
66
67
68
69
70
71 type PersistentDataStoreTestSuite struct {
72 storeFactoryFn func(string) ssys.ComponentConfigurer[ssys.PersistentDataStore]
73 clearDataFn func(string) error
74 errorStoreFactory ssys.ComponentConfigurer[ssys.PersistentDataStore]
75 errorValidator func(assert.TestingT, error)
76 concurrentModificationHookFn func(store ssys.PersistentDataStore, hook func())
77 includeBaseTests bool
78 }
79
80
81
82
83
84
85
86
87
88
89
90
91
92 func NewPersistentDataStoreTestSuite(
93 storeFactoryFn func(prefix string) ssys.ComponentConfigurer[ssys.PersistentDataStore],
94 clearDataFn func(prefix string) error,
95 ) *PersistentDataStoreTestSuite {
96 return &PersistentDataStoreTestSuite{
97 storeFactoryFn: storeFactoryFn,
98 clearDataFn: clearDataFn,
99 includeBaseTests: true,
100 }
101 }
102
103
104
105
106 func (s *PersistentDataStoreTestSuite) ErrorStoreFactory(
107 errorStoreFactory ssys.ComponentConfigurer[ssys.PersistentDataStore],
108 errorValidator func(assert.TestingT, error),
109 ) *PersistentDataStoreTestSuite {
110 s.errorStoreFactory = errorStoreFactory
111 s.errorValidator = errorValidator
112 return s
113 }
114
115
116
117
118
119
120
121
122 func (s *PersistentDataStoreTestSuite) ConcurrentModificationHook(
123 setHookFn func(store ssys.PersistentDataStore, hook func()),
124 ) *PersistentDataStoreTestSuite {
125 s.concurrentModificationHookFn = setHookFn
126 return s
127 }
128
129
130 func (s *PersistentDataStoreTestSuite) Run(t *testing.T) {
131 s.runInternal(testbox.RealTest(t))
132 }
133
134 func (s *PersistentDataStoreTestSuite) runInternal(t testbox.TestingT) {
135 if s.includeBaseTests {
136 t.Run("Init", s.runInitTests)
137 t.Run("Get", s.runGetTests)
138 t.Run("Upsert", s.runUpsertTests)
139 t.Run("Delete", s.runDeleteTests)
140
141 t.Run("IsStoreAvailable", func(t testbox.TestingT) {
142
143 s.withDefaultStore(t, func(store ssys.PersistentDataStore) {
144 assert.True(t, store.IsStoreAvailable())
145 })
146 })
147 }
148
149 t.Run("error returns", s.runErrorTests)
150 t.Run("prefix independence", s.runPrefixIndependenceTests)
151 t.Run("concurrent modification", s.runConcurrentModificationTests)
152
153 if s.includeBaseTests {
154 t.Run("LDClient end-to-end tests", s.runLDClientEndToEndTests)
155 }
156 }
157
158 func (s *PersistentDataStoreTestSuite) clearData(t require.TestingT, prefix string) {
159 require.NoError(t, s.clearDataFn(prefix))
160 }
161
162 func (s *PersistentDataStoreTestSuite) initWithEmptyData(store ssys.PersistentDataStore) {
163 _ = store.Init(mocks.MakeSerializedMockDataSet())
164
165
166 }
167
168 func (s *PersistentDataStoreTestSuite) withStore(
169 t testbox.TestingT,
170 prefix string,
171 action func(ssys.PersistentDataStore),
172 ) {
173 testhelpers.WithMockLoggingContext(t, func(context ssys.ClientContext) {
174 store, err := s.storeFactoryFn(prefix).Build(context)
175 require.NoError(t, err)
176 defer func() {
177 _ = store.Close()
178 }()
179 action(store)
180 })
181 }
182
183 func (s *PersistentDataStoreTestSuite) withDefaultStore(
184 t testbox.TestingT,
185 action func(ssys.PersistentDataStore),
186 ) {
187 s.withStore(t, "", action)
188 }
189
190 func (s *PersistentDataStoreTestSuite) withDefaultInitedStore(
191 t testbox.TestingT,
192 action func(ssys.PersistentDataStore),
193 ) {
194 s.clearData(t, "")
195 s.withDefaultStore(t, func(store ssys.PersistentDataStore) {
196 s.initWithEmptyData(store)
197 action(store)
198 })
199 }
200
201 func (s *PersistentDataStoreTestSuite) runInitTests(t testbox.TestingT) {
202 t.Run("store initialized after init", func(t testbox.TestingT) {
203 s.clearData(t, "")
204 s.withDefaultStore(t, func(store ssys.PersistentDataStore) {
205 item1 := mocks.MockDataItem{Key: "feature"}
206 allData := mocks.MakeSerializedMockDataSet(item1)
207 require.NoError(t, store.Init(allData))
208
209 assert.True(t, store.IsInitialized())
210 })
211 })
212
213 t.Run("completely replaces previous data", func(t testbox.TestingT) {
214 s.clearData(t, "")
215 s.withDefaultStore(t, func(store ssys.PersistentDataStore) {
216 item1 := mocks.MockDataItem{Key: "first", Version: 1}
217 item2 := mocks.MockDataItem{Key: "second", Version: 1}
218 otherItem1 := mocks.MockDataItem{Key: "first", Version: 1, IsOtherKind: true}
219 allData := mocks.MakeSerializedMockDataSet(item1, item2, otherItem1)
220 require.NoError(t, store.Init(allData))
221
222 items, err := store.GetAll(mocks.MockData)
223 require.NoError(t, err)
224 assert.Len(t, items, 2)
225 assertEqualsSerializedItem(t, item1, itemDescriptorsToMap(items)[item1.Key])
226 assertEqualsSerializedItem(t, item2, itemDescriptorsToMap(items)[item2.Key])
227
228 otherItems, err := store.GetAll(mocks.MockOtherData)
229 require.NoError(t, err)
230 assert.Len(t, otherItems, 1)
231 assertEqualsSerializedItem(t, otherItem1, itemDescriptorsToMap(otherItems)[otherItem1.Key])
232
233 otherItem2 := mocks.MockDataItem{Key: "second", Version: 1, IsOtherKind: true}
234 allData = mocks.MakeSerializedMockDataSet(item1, otherItem2)
235 require.NoError(t, store.Init(allData))
236
237 items, err = store.GetAll(mocks.MockData)
238 require.NoError(t, err)
239 assert.Len(t, items, 1)
240 assertEqualsSerializedItem(t, item1, itemDescriptorsToMap(items)[item1.Key])
241
242 otherItems, err = store.GetAll(mocks.MockOtherData)
243 require.NoError(t, err)
244 assert.Len(t, otherItems, 1)
245 assertEqualsSerializedItem(t, otherItem2, itemDescriptorsToMap(otherItems)[otherItem2.Key])
246 })
247 })
248
249 t.Run("one instance can detect if another instance has initialized the store", func(t testbox.TestingT) {
250 s.clearData(t, "")
251 s.withDefaultStore(t, func(store1 ssys.PersistentDataStore) {
252 s.withDefaultStore(t, func(store2 ssys.PersistentDataStore) {
253 assert.False(t, store1.IsInitialized())
254
255 s.initWithEmptyData(store2)
256
257 assert.True(t, store1.IsInitialized())
258 })
259 })
260 })
261 }
262
263 func (s *PersistentDataStoreTestSuite) runGetTests(t testbox.TestingT) {
264 t.Run("existing item", func(t testbox.TestingT) {
265 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
266 item1 := mocks.MockDataItem{Key: "feature"}
267 updated, err := store.Upsert(mocks.MockData, item1.Key, item1.ToSerializedItemDescriptor())
268 assert.NoError(t, err)
269 assert.True(t, updated)
270
271 result, err := store.Get(mocks.MockData, item1.Key)
272 assert.NoError(t, err)
273 assertEqualsSerializedItem(t, item1, result)
274 })
275 })
276
277 t.Run("nonexisting item", func(t testbox.TestingT) {
278 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
279 result, err := store.Get(mocks.MockData, "no")
280 assert.NoError(t, err)
281 assert.Equal(t, -1, result.Version)
282 assert.Nil(t, result.SerializedItem)
283 })
284 })
285
286 t.Run("all items", func(t testbox.TestingT) {
287 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
288 result, err := store.GetAll(mocks.MockData)
289 assert.NoError(t, err)
290 assert.Len(t, result, 0)
291
292 item1 := mocks.MockDataItem{Key: "first", Version: 1}
293 item2 := mocks.MockDataItem{Key: "second", Version: 1}
294 otherItem1 := mocks.MockDataItem{Key: "first", Version: 1, IsOtherKind: true}
295 _, err = store.Upsert(mocks.MockData, item1.Key, item1.ToSerializedItemDescriptor())
296 assert.NoError(t, err)
297 _, err = store.Upsert(mocks.MockData, item2.Key, item2.ToSerializedItemDescriptor())
298 assert.NoError(t, err)
299 _, err = store.Upsert(mocks.MockOtherData, otherItem1.Key, otherItem1.ToSerializedItemDescriptor())
300 assert.NoError(t, err)
301
302 result, err = store.GetAll(mocks.MockData)
303 assert.NoError(t, err)
304 assert.Len(t, result, 2)
305 assertEqualsSerializedItem(t, item1, itemDescriptorsToMap(result)[item1.Key])
306 assertEqualsSerializedItem(t, item2, itemDescriptorsToMap(result)[item2.Key])
307 })
308 })
309 }
310
311 func (s *PersistentDataStoreTestSuite) runUpsertTests(t testbox.TestingT) {
312 item1 := mocks.MockDataItem{Key: "feature", Version: 10, Name: "original"}
313
314 setupItem1 := func(t testbox.TestingT, store ssys.PersistentDataStore) {
315 updated, err := store.Upsert(mocks.MockData, item1.Key, item1.ToSerializedItemDescriptor())
316 assert.NoError(t, err)
317 assert.True(t, updated)
318 }
319
320 t.Run("newer version", func(t testbox.TestingT) {
321 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
322 setupItem1(t, store)
323
324 item1a := mocks.MockDataItem{Key: "feature", Version: item1.Version + 1, Name: "updated"}
325 updated, err := store.Upsert(mocks.MockData, item1.Key, item1a.ToSerializedItemDescriptor())
326 assert.NoError(t, err)
327 assert.True(t, updated)
328
329 result, err := store.Get(mocks.MockData, item1.Key)
330 assert.NoError(t, err)
331 assertEqualsSerializedItem(t, item1a, result)
332 })
333 })
334
335 t.Run("older version", func(t testbox.TestingT) {
336 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
337 setupItem1(t, store)
338
339 item1a := mocks.MockDataItem{Key: "feature", Version: item1.Version - 1, Name: "updated"}
340 updated, err := store.Upsert(mocks.MockData, item1.Key, item1a.ToSerializedItemDescriptor())
341 assert.NoError(t, err)
342 assert.False(t, updated)
343
344 result, err := store.Get(mocks.MockData, item1.Key)
345 assert.NoError(t, err)
346 assertEqualsSerializedItem(t, item1, result)
347 })
348 })
349
350 t.Run("same version", func(t testbox.TestingT) {
351 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
352 setupItem1(t, store)
353
354 item1a := mocks.MockDataItem{Key: "feature", Version: item1.Version, Name: "updated"}
355 updated, err := store.Upsert(mocks.MockData, item1.Key, item1a.ToSerializedItemDescriptor())
356 assert.NoError(t, err)
357 assert.False(t, updated)
358
359 result, err := store.Get(mocks.MockData, item1.Key)
360 assert.NoError(t, err)
361 assertEqualsSerializedItem(t, item1, result)
362 })
363 })
364 }
365
366 func (s *PersistentDataStoreTestSuite) runDeleteTests(t testbox.TestingT) {
367 t.Run("newer version", func(t testbox.TestingT) {
368 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
369 item1 := mocks.MockDataItem{Key: "feature", Version: 10}
370 updated, err := store.Upsert(mocks.MockData, item1.Key, item1.ToSerializedItemDescriptor())
371 assert.NoError(t, err)
372 assert.True(t, updated)
373
374 deletedItem := mocks.MockDataItem{Key: item1.Key, Version: item1.Version + 1, Deleted: true}
375 updated, err = store.Upsert(mocks.MockData, item1.Key, deletedItem.ToSerializedItemDescriptor())
376 assert.NoError(t, err)
377 assert.True(t, updated)
378
379 result, err := store.Get(mocks.MockData, item1.Key)
380 assert.NoError(t, err)
381 assertEqualsDeletedItem(t, deletedItem.ToSerializedItemDescriptor(), result)
382 })
383 })
384
385 t.Run("older version", func(t testbox.TestingT) {
386 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
387 item1 := mocks.MockDataItem{Key: "feature", Version: 10}
388 updated, err := store.Upsert(mocks.MockData, item1.Key, item1.ToSerializedItemDescriptor())
389 assert.NoError(t, err)
390 assert.True(t, updated)
391
392 deletedItem := mocks.MockDataItem{Key: item1.Key, Version: item1.Version - 1, Deleted: true}
393 updated, err = store.Upsert(mocks.MockData, item1.Key, deletedItem.ToSerializedItemDescriptor())
394 assert.NoError(t, err)
395 assert.False(t, updated)
396
397 result, err := store.Get(mocks.MockData, item1.Key)
398 assert.NoError(t, err)
399 assertEqualsSerializedItem(t, item1, result)
400 })
401 })
402
403 t.Run("same version", func(t testbox.TestingT) {
404 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
405 item1 := mocks.MockDataItem{Key: "feature", Version: 10}
406 updated, err := store.Upsert(mocks.MockData, item1.Key, item1.ToSerializedItemDescriptor())
407 assert.NoError(t, err)
408 assert.True(t, updated)
409
410 deletedItem := mocks.MockDataItem{Key: item1.Key, Version: item1.Version, Deleted: true}
411 updated, err = store.Upsert(mocks.MockData, item1.Key, deletedItem.ToSerializedItemDescriptor())
412 assert.NoError(t, err)
413 assert.False(t, updated)
414
415 result, err := store.Get(mocks.MockData, item1.Key)
416 assert.NoError(t, err)
417 assertEqualsSerializedItem(t, item1, result)
418 })
419 })
420
421 t.Run("unknown item", func(t testbox.TestingT) {
422 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
423 deletedItem := mocks.MockDataItem{Key: "feature", Version: 1, Deleted: true}
424 updated, err := store.Upsert(mocks.MockData, deletedItem.Key, deletedItem.ToSerializedItemDescriptor())
425 assert.NoError(t, err)
426 assert.True(t, updated)
427
428 result, err := store.Get(mocks.MockData, deletedItem.Key)
429 assert.NoError(t, err)
430 assertEqualsDeletedItem(t, deletedItem.ToSerializedItemDescriptor(), result)
431 })
432 })
433
434 t.Run("upsert older version after delete", func(t testbox.TestingT) {
435 s.withDefaultInitedStore(t, func(store ssys.PersistentDataStore) {
436 item1 := mocks.MockDataItem{Key: "feature", Version: 10}
437 updated, err := store.Upsert(mocks.MockData, item1.Key, item1.ToSerializedItemDescriptor())
438 assert.NoError(t, err)
439 assert.True(t, updated)
440
441 deletedItem := mocks.MockDataItem{Key: item1.Key, Version: item1.Version + 1, Deleted: true}
442 updated, err = store.Upsert(mocks.MockData, item1.Key, deletedItem.ToSerializedItemDescriptor())
443 assert.NoError(t, err)
444 assert.True(t, updated)
445
446 updated, err = store.Upsert(mocks.MockData, item1.Key, item1.ToSerializedItemDescriptor())
447 assert.NoError(t, err)
448 assert.False(t, updated)
449
450 result, err := store.Get(mocks.MockData, item1.Key)
451 assert.NoError(t, err)
452 assertEqualsDeletedItem(t, deletedItem.ToSerializedItemDescriptor(), result)
453 })
454 })
455 }
456
457 func (s *PersistentDataStoreTestSuite) runPrefixIndependenceTests(t testbox.TestingT) {
458 runWithPrefixes := func(
459 t testbox.TestingT,
460 name string,
461 test func(testbox.TestingT, ssys.PersistentDataStore, ssys.PersistentDataStore),
462 ) {
463 prefix1 := "testprefix1"
464 prefix2 := "testprefix2"
465 s.clearData(t, prefix1)
466 s.clearData(t, prefix2)
467
468 s.withStore(t, prefix1, func(store1 ssys.PersistentDataStore) {
469 s.withStore(t, prefix2, func(store2 ssys.PersistentDataStore) {
470 t.Run(name, func(t testbox.TestingT) {
471 test(t, store1, store2)
472 })
473 })
474 })
475 }
476
477 runWithPrefixes(t, "Init", func(t testbox.TestingT, store1 ssys.PersistentDataStore, store2 ssys.PersistentDataStore) {
478 assert.False(t, store1.IsInitialized())
479 assert.False(t, store2.IsInitialized())
480
481 item1a := mocks.MockDataItem{Key: "flag-a", Version: 1}
482 item1b := mocks.MockDataItem{Key: "flag-b", Version: 1}
483 item2a := mocks.MockDataItem{Key: "flag-a", Version: 2}
484 item2c := mocks.MockDataItem{Key: "flag-c", Version: 2}
485
486 data1 := mocks.MakeSerializedMockDataSet(item1a, item1b)
487 data2 := mocks.MakeSerializedMockDataSet(item2a, item2c)
488
489 err := store1.Init(data1)
490 require.NoError(t, err)
491
492 assert.True(t, store1.IsInitialized())
493 assert.False(t, store2.IsInitialized())
494
495 err = store2.Init(data2)
496 require.NoError(t, err)
497
498 assert.True(t, store1.IsInitialized())
499 assert.True(t, store2.IsInitialized())
500
501 newItems1, err := store1.GetAll(mocks.MockData)
502 require.NoError(t, err)
503 assert.Len(t, newItems1, 2)
504 assertEqualsSerializedItem(t, item1a, itemDescriptorsToMap(newItems1)[item1a.Key])
505 assertEqualsSerializedItem(t, item1b, itemDescriptorsToMap(newItems1)[item1b.Key])
506
507 newItem1a, err := store1.Get(mocks.MockData, item1a.Key)
508 require.NoError(t, err)
509 assertEqualsSerializedItem(t, item1a, newItem1a)
510
511 newItem1b, err := store1.Get(mocks.MockData, item1b.Key)
512 require.NoError(t, err)
513 assertEqualsSerializedItem(t, item1b, newItem1b)
514
515 newItems2, err := store2.GetAll(mocks.MockData)
516 require.NoError(t, err)
517 assert.Len(t, newItems2, 2)
518 assertEqualsSerializedItem(t, item2a, itemDescriptorsToMap(newItems2)[item2a.Key])
519 assertEqualsSerializedItem(t, item2c, itemDescriptorsToMap(newItems2)[item2c.Key])
520
521 newItem2a, err := store2.Get(mocks.MockData, item2a.Key)
522 require.NoError(t, err)
523 assertEqualsSerializedItem(t, item2a, newItem2a)
524
525 newItem2c, err := store2.Get(mocks.MockData, item2c.Key)
526 require.NoError(t, err)
527 assertEqualsSerializedItem(t, item2c, newItem2c)
528 })
529
530 runWithPrefixes(t, "Upsert/Delete", func(t testbox.TestingT, store1 ssys.PersistentDataStore,
531 store2 ssys.PersistentDataStore) {
532 assert.False(t, store1.IsInitialized())
533 assert.False(t, store2.IsInitialized())
534
535 key := "flag"
536 item1 := mocks.MockDataItem{Key: key, Version: 1}
537 item2 := mocks.MockDataItem{Key: key, Version: 2}
538
539
540
541 updated, err := store2.Upsert(mocks.MockData, item2.Key, item2.ToSerializedItemDescriptor())
542 require.NoError(t, err)
543 assert.True(t, updated)
544 _, err = store1.Upsert(mocks.MockData, item1.Key, item1.ToSerializedItemDescriptor())
545 require.NoError(t, err)
546 assert.True(t, updated)
547
548 newItem1, err := store1.Get(mocks.MockData, key)
549 require.NoError(t, err)
550 assertEqualsSerializedItem(t, item1, newItem1)
551
552 newItem2, err := store2.Get(mocks.MockData, key)
553 require.NoError(t, err)
554 assertEqualsSerializedItem(t, item2, newItem2)
555
556 updated, err = store1.Upsert(mocks.MockData, key, item2.ToSerializedItemDescriptor())
557 require.NoError(t, err)
558 assert.True(t, updated)
559
560 newItem1a, err := store1.Get(mocks.MockData, key)
561 require.NoError(t, err)
562 assertEqualsSerializedItem(t, item2, newItem1a)
563 })
564 }
565
566 func (s *PersistentDataStoreTestSuite) runErrorTests(t testbox.TestingT) {
567 if s.errorStoreFactory == nil {
568 t.Skip("not implemented for this store type")
569 return
570 }
571 errorValidator := s.errorValidator
572 if errorValidator == nil {
573 errorValidator = func(assert.TestingT, error) {}
574 }
575
576 store, err := s.errorStoreFactory.Build(sh.NewSimpleTestContext(""))
577 require.NoError(t, err)
578 defer store.Close()
579
580 t.Run("Init", func(t testbox.TestingT) {
581 allData := []st.SerializedCollection{
582 {Kind: datakinds.Features},
583 {Kind: datakinds.Segments},
584 }
585 err := store.Init(allData)
586 require.Error(t, err)
587 errorValidator(t, err)
588 })
589
590 t.Run("Get", func(t testbox.TestingT) {
591 _, err := store.Get(datakinds.Features, "key")
592 require.Error(t, err)
593 errorValidator(t, err)
594 })
595
596 t.Run("GetAll", func(t testbox.TestingT) {
597 _, err := store.GetAll(datakinds.Features)
598 require.Error(t, err)
599 errorValidator(t, err)
600 })
601
602 t.Run("Upsert", func(t testbox.TestingT) {
603 desc := sh.FlagDescriptor(ldbuilders.NewFlagBuilder("key").Build())
604 sdesc := st.SerializedItemDescriptor{
605 Version: 1,
606 SerializedItem: datakinds.Features.Serialize(desc),
607 }
608 _, err := store.Upsert(datakinds.Features, "key", sdesc)
609 require.Error(t, err)
610 errorValidator(t, err)
611 })
612
613 t.Run("IsInitialized", func(t testbox.TestingT) {
614 assert.False(t, store.IsInitialized())
615 })
616 }
617
618 func (s *PersistentDataStoreTestSuite) runConcurrentModificationTests(t testbox.TestingT) {
619 if s.concurrentModificationHookFn == nil {
620 t.Skip("not implemented for this store type")
621 return
622 }
623
624 key := "foo"
625
626 makeItemWithVersion := func(version int) mocks.MockDataItem {
627 return mocks.MockDataItem{Key: key, Version: version}
628 }
629
630 s.clearData(t, "")
631 s.withStore(t, "", func(store1 ssys.PersistentDataStore) {
632 s.withStore(t, "", func(store2 ssys.PersistentDataStore) {
633 setupStore1 := func(initialVersion int) {
634 allData := mocks.MakeSerializedMockDataSet(makeItemWithVersion(initialVersion))
635 require.NoError(t, store1.Init(allData))
636 }
637
638 setupConcurrentModifierToWriteVersions := func(versionsToWrite ...int) {
639 i := 0
640 s.concurrentModificationHookFn(store1, func() {
641 if i < len(versionsToWrite) {
642 newItem := makeItemWithVersion(versionsToWrite[i])
643 _, err := store2.Upsert(mocks.MockData, key, newItem.ToSerializedItemDescriptor())
644 require.NoError(t, err)
645 i++
646 }
647 })
648 }
649
650 t.Run("upsert race condition against external client with lower version", func(t testbox.TestingT) {
651 setupStore1(1)
652 setupConcurrentModifierToWriteVersions(2, 3, 4)
653
654 _, err := store1.Upsert(mocks.MockData, key, makeItemWithVersion(10).ToSerializedItemDescriptor())
655 assert.NoError(t, err)
656
657 var result st.SerializedItemDescriptor
658 result, err = store1.Get(mocks.MockData, key)
659 assert.NoError(t, err)
660 assertEqualsSerializedItem(t, makeItemWithVersion(10), result)
661 })
662
663 t.Run("upsert race condition against external client with higher version", func(t testbox.TestingT) {
664 setupStore1(1)
665 setupConcurrentModifierToWriteVersions(3)
666
667 updated, err := store1.Upsert(mocks.MockData, key, makeItemWithVersion(2).ToSerializedItemDescriptor())
668 assert.NoError(t, err)
669 assert.False(t, updated)
670
671 var result st.SerializedItemDescriptor
672 result, err = store1.Get(mocks.MockData, key)
673 assert.NoError(t, err)
674 assertEqualsSerializedItem(t, makeItemWithVersion(3), result)
675 })
676 })
677 })
678 }
679
680 func itemDescriptorsToMap(
681 items []st.KeyedSerializedItemDescriptor,
682 ) map[string]st.SerializedItemDescriptor {
683 ret := make(map[string]st.SerializedItemDescriptor)
684 for _, item := range items {
685 ret[item.Key] = item.Item
686 }
687 return ret
688 }
689
690 func (s *PersistentDataStoreTestSuite) runLDClientEndToEndTests(t testbox.TestingT) {
691 dataStoreFactory := s.storeFactoryFn("ldclient")
692
693
694
695
696 flagKey, segmentKey, userKey, otherUserKey := "flagkey", "segmentkey", "userkey", "otheruser"
697 goodValue1, goodValue2, badValue := ldvalue.String("good"), ldvalue.String("better"), ldvalue.String("bad")
698 goodVariation1, goodVariation2, badVariation := 0, 1, 2
699 user, otherUser := ldcontext.New(userKey), ldcontext.New(otherUserKey)
700
701 makeFlagThatReturnsVariationForSegmentMatch := func(version int, variation int) ldmodel.FeatureFlag {
702 return ldbuilders.NewFlagBuilder(flagKey).Version(version).
703 On(true).
704 Variations(goodValue1, goodValue2, badValue).
705 FallthroughVariation(badVariation).
706 AddRule(ldbuilders.NewRuleBuilder().Variation(variation).Clauses(
707 ldbuilders.Clause("", ldmodel.OperatorSegmentMatch, ldvalue.String(segmentKey)),
708 )).
709 Build()
710 }
711 makeSegmentThatMatchesUserKeys := func(version int, keys ...string) ldmodel.Segment {
712 return ldbuilders.NewSegmentBuilder(segmentKey).Version(version).
713 Included(keys...).
714 Build()
715 }
716 flag := makeFlagThatReturnsVariationForSegmentMatch(1, goodVariation1)
717 segment := makeSegmentThatMatchesUserKeys(1, userKey)
718
719 data := []st.Collection{
720 {Kind: datakinds.Features, Items: []st.KeyedItemDescriptor{
721 {Key: flagKey, Item: sh.FlagDescriptor(flag)},
722 }},
723 {Kind: datakinds.Segments, Items: []st.KeyedItemDescriptor{
724 {Key: segmentKey, Item: sh.SegmentDescriptor(segment)},
725 }},
726 }
727 dataSourceConfigurer := &mocks.ComponentConfigurerThatCapturesClientContext[ssys.DataSource]{
728 Configurer: &mocks.DataSourceFactoryWithData{Data: data},
729 }
730 mockLog := ldlogtest.NewMockLog()
731 config := ld.Config{
732 DataStore: ldcomponents.PersistentDataStore(dataStoreFactory).NoCaching(),
733 DataSource: dataSourceConfigurer,
734 Events: ldcomponents.NoEvents(),
735 Logging: ldcomponents.Logging().Loggers(mockLog.Loggers),
736 }
737
738 client, err := ld.MakeCustomClient("sdk-key", config, 5*time.Second)
739 require.NoError(t, err)
740 defer client.Close()
741 dataSourceUpdateSink := dataSourceConfigurer.ReceivedClientContext.GetDataSourceUpdateSink()
742
743 flagShouldHaveValueForUser := func(u ldcontext.Context, expectedValue ldvalue.Value) {
744 value, err := client.JSONVariation(flagKey, u, ldvalue.Null())
745 assert.NoError(t, err)
746 assert.Equal(t, expectedValue, value)
747 }
748
749 t.Run("get flag", func(t testbox.TestingT) {
750 flagShouldHaveValueForUser(user, goodValue1)
751 flagShouldHaveValueForUser(otherUser, badValue)
752 })
753
754 t.Run("get all flags", func(t testbox.TestingT) {
755 state := client.AllFlagsState(user)
756 assert.Equal(t, map[string]ldvalue.Value{flagKey: goodValue1}, state.ToValuesMap())
757 })
758
759 t.Run("update flag", func(t testbox.TestingT) {
760 flagv2 := makeFlagThatReturnsVariationForSegmentMatch(2, goodVariation2)
761 dataSourceUpdateSink.Upsert(datakinds.Features, flagKey,
762 sh.FlagDescriptor(flagv2))
763
764 flagShouldHaveValueForUser(user, goodValue2)
765 flagShouldHaveValueForUser(otherUser, badValue)
766 })
767
768 t.Run("update segment", func(t testbox.TestingT) {
769 segmentv2 := makeSegmentThatMatchesUserKeys(2, userKey, otherUserKey)
770 dataSourceUpdateSink.Upsert(datakinds.Segments, segmentKey,
771 sh.SegmentDescriptor(segmentv2))
772 flagShouldHaveValueForUser(otherUser, goodValue2)
773 })
774
775 t.Run("delete segment", func(t testbox.TestingT) {
776
777 dataSourceUpdateSink.Upsert(datakinds.Segments, segmentKey,
778 st.ItemDescriptor{Version: 3, Item: nil})
779 flagShouldHaveValueForUser(user, badValue)
780 })
781
782 t.Run("delete flag", func(t testbox.TestingT) {
783
784 dataSourceUpdateSink.Upsert(datakinds.Features, flagKey,
785 st.ItemDescriptor{Version: 3, Item: nil})
786 value, detail, err := client.JSONVariationDetail(flagKey, user, ldvalue.Null())
787 assert.Error(t, err)
788 assert.Equal(t, ldvalue.Null(), value)
789 assert.Equal(t, ldreason.EvalErrorFlagNotFound, detail.Reason.GetErrorKind())
790 })
791
792 t.Run("no errors are logged", func(t testbox.TestingT) {
793 assert.Len(t, mockLog.GetOutput(ldlog.Error), 0)
794 })
795 }
796
View as plain text