...

Source file src/github.com/launchdarkly/go-server-sdk/v6/testhelpers/storetest/persistent_data_store_test_suite.go

Documentation: github.com/launchdarkly/go-server-sdk/v6/testhelpers/storetest

     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  	// This allows for the fact that a PersistentDataStore may not be able to get the item version without
    36  	// deserializing it, so we allow the version to be zero.
    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  	// As above, the PersistentDataStore may not have separate access to the version and deleted state;
    49  	// PersistentDataStoreWrapper compensates for this when it deserializes the item.
    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  // PersistentDataStoreTestSuite provides a configurable test suite for all implementations of
    61  // PersistentDataStore.
    62  //
    63  // In order to be testable with this tool, a data store implementation must have the following
    64  // characteristics:
    65  //
    66  // 1. It has some notion of a "prefix" string that can be used to distinguish between different
    67  // SDK instances using the same underlying database.
    68  //
    69  // 2. Two instances of the same data store type with the same configuration, and the same prefix,
    70  // should be able to see each other's data.
    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  // NewPersistentDataStoreTestSuite creates a PersistentDataStoreTestSuite for testing some
    81  // implementation of PersistentDataStore.
    82  //
    83  // The storeFactoryFn parameter is a function that takes a prefix string and returns a configured
    84  // factory for this data store type (for instance, ldconsul.DataStore().Prefix(prefix)). If the
    85  // prefix string is "", it should use the default prefix defined by the data store implementation.
    86  // The factory must include any necessary configuration that may be appropriate for the test
    87  // environment (for instance, pointing it to a database instance that has been set up for the
    88  // tests).
    89  //
    90  // The clearDataFn parameter is a function that takes a prefix string and deletes any existing
    91  // data that may exist in the database corresponding to that prefix.
    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  // ErrorStoreFactory enables a test of error handling. The provided errorStoreFactory is expected to
   104  // produce a data store instance whose operations should all fail and return an error. The errorValidator
   105  // function, if any, will be called to verify that it is the expected error.
   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  // ConcurrentModificationHook enables tests of concurrent modification behavior, for store
   116  // implementations that support testing this.
   117  //
   118  // The hook parameter is a function which, when called with a store instance and another function as
   119  // parameters, will modify the store instance so that it will call the latter function synchronously
   120  // during each Upsert operation - after the old value has been read, but before the new one has been
   121  // written.
   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  // Run runs the configured test suite.
   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 { // PersistentDataStoreTestSuiteTest can disable these
   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  			// The store should always be available during this test suite
   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  	// We are ignoring the error here because the store might have been configured to deliberately
   165  	// cause an error, for tests that validate error handling.
   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  		// Insert the one with the higher version first, so we can verify that the version-checking logic
   540  		// is definitely looking in the right namespace
   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() //nolint:errcheck
   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  	// This is a basic smoke test to verify that the data store component behaves correctly within an
   694  	// SDK client instance.
   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() //nolint:errcheck
   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) // otherUser is now matched by the segment
   773  	})
   774  
   775  	t.Run("delete segment", func(t testbox.TestingT) {
   776  		// deleting the segment should cause the flag that uses it to stop matching
   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  		// deleting the flag should cause the flag to become unknown
   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