...

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

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

     1  package storetest
     2  
     3  import (
     4  	"reflect"
     5  	"testing"
     6  
     7  	"github.com/launchdarkly/go-sdk-common/v3/ldtime"
     8  	"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
     9  	"github.com/launchdarkly/go-server-sdk/v6/subsystems"
    10  	"github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoreimpl"
    11  	"github.com/launchdarkly/go-server-sdk/v6/testhelpers"
    12  
    13  	"github.com/launchdarkly/go-test-helpers/v3/testbox"
    14  
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  const fakeUserHash = "userhash"
    20  
    21  // BigSegmentStoreTestSuite provides a configurable test suite for all implementations of
    22  // BigSegmentStore.
    23  type BigSegmentStoreTestSuite struct {
    24  	storeFactoryFn func(string) subsystems.ComponentConfigurer[subsystems.BigSegmentStore]
    25  	clearDataFn    func(string) error
    26  	setMetadataFn  func(string, subsystems.BigSegmentStoreMetadata) error
    27  	setSegmentsFn  func(prefix, userHashKey string, included []string, excluded []string) error
    28  }
    29  
    30  // NewBigSegmentStoreTestSuite creates an BigSegmentStoreTestSuite for testing some
    31  // implementation of BigSegmentStore.
    32  //
    33  // The storeFactoryFn parameter is a function that takes a prefix string and returns a configured
    34  // factory for this data store type (for instance, ldredis.DataStore().Prefix(prefix)). If the
    35  // prefix string is "", it should use the default prefix defined by the data store implementation.
    36  // The factory must include any necessary configuration that may be appropriate for the test
    37  // environment (for instance, pointing it to a database instance that has been set up for the
    38  // tests).
    39  //
    40  // The clearDataFn parameter is a function that takes a prefix string and deletes any existing
    41  // data that may exist in the database corresponding to that prefix.
    42  //
    43  // The setMetadataFn and setSegmentsFn parameters are functions for populating the database. The
    44  // string slices passed to setSegmentsFn are lists of segment references in the same format used
    45  // by BigSegmentMembership, and should be used as-is by the store.
    46  func NewBigSegmentStoreTestSuite(
    47  	storeFactoryFn func(prefix string) subsystems.ComponentConfigurer[subsystems.BigSegmentStore],
    48  	clearDataFn func(prefix string) error,
    49  	setMetadataFn func(prefix string, metadata subsystems.BigSegmentStoreMetadata) error,
    50  	setSegmentsFn func(prefix string, userHashKey string, included []string, excluded []string) error,
    51  ) *BigSegmentStoreTestSuite {
    52  	return &BigSegmentStoreTestSuite{
    53  		storeFactoryFn: storeFactoryFn,
    54  		clearDataFn:    clearDataFn,
    55  		setMetadataFn:  setMetadataFn,
    56  		setSegmentsFn:  setSegmentsFn,
    57  	}
    58  }
    59  
    60  // Run runs the configured test suite.
    61  func (s *BigSegmentStoreTestSuite) Run(t *testing.T) {
    62  	s.runInternal(testbox.RealTest(t))
    63  }
    64  
    65  func (s *BigSegmentStoreTestSuite) runInternal(t testbox.TestingT) {
    66  	t.Run("GetMetadata", s.runMetadataTests)
    67  	t.Run("GetMembership", s.runMembershipTests)
    68  }
    69  
    70  func (s *BigSegmentStoreTestSuite) runMetadataTests(t testbox.TestingT) {
    71  	t.Run("valid value", func(t testbox.TestingT) {
    72  		expected := subsystems.BigSegmentStoreMetadata{LastUpToDate: ldtime.UnixMillisecondTime(1234567890)}
    73  
    74  		s.withStoreAndEmptyData(t, func(store subsystems.BigSegmentStore) {
    75  			require.NoError(t, s.setMetadataFn("", expected))
    76  
    77  			meta, err := store.GetMetadata()
    78  			require.NoError(t, err)
    79  			assert.Equal(t, expected, meta)
    80  		})
    81  	})
    82  
    83  	t.Run("no value", func(t testbox.TestingT) {
    84  		s.withStoreAndEmptyData(t, func(store subsystems.BigSegmentStore) {
    85  			meta, err := store.GetMetadata()
    86  			// The Big Segment store should not return a database error in this case; it should return
    87  			// a result with an unset (zero) LastUpToDate, meaning "the store has not been updated ever".
    88  			assert.Equal(t, ldtime.UnixMillisecondTime(0), meta.LastUpToDate)
    89  			assert.NoError(t, err)
    90  		})
    91  	})
    92  }
    93  
    94  func (s *BigSegmentStoreTestSuite) runMembershipTests(t testbox.TestingT) {
    95  	t.Run("not found", func(t testbox.TestingT) {
    96  		s.withStoreAndEmptyData(t, func(store subsystems.BigSegmentStore) {
    97  			um, err := store.GetMembership(fakeUserHash)
    98  			require.NoError(t, err)
    99  			assertEqualMembership(t, nil, nil, um)
   100  		})
   101  	})
   102  
   103  	t.Run("includes only", func(t testbox.TestingT) {
   104  		s.withStoreAndEmptyData(t, func(store subsystems.BigSegmentStore) {
   105  			require.NoError(t, s.setSegmentsFn("", fakeUserHash, []string{"key1", "key2"}, nil))
   106  
   107  			um, err := store.GetMembership(fakeUserHash)
   108  			require.NoError(t, err)
   109  			assertEqualMembership(t, []string{"key1", "key2"}, nil, um)
   110  		})
   111  	})
   112  
   113  	t.Run("excludes only", func(t testbox.TestingT) {
   114  		s.withStoreAndEmptyData(t, func(store subsystems.BigSegmentStore) {
   115  			require.NoError(t, s.setSegmentsFn("", fakeUserHash, nil, []string{"key1", "key2"}))
   116  
   117  			um, err := store.GetMembership(fakeUserHash)
   118  			require.NoError(t, err)
   119  			assertEqualMembership(t, nil, []string{"key1", "key2"}, um)
   120  		})
   121  	})
   122  
   123  	t.Run("includes and excludes", func(t testbox.TestingT) {
   124  		s.withStoreAndEmptyData(t, func(store subsystems.BigSegmentStore) {
   125  			require.NoError(t, s.setSegmentsFn("", fakeUserHash, []string{"key1", "key2"}, []string{"key2", "key3"}))
   126  			// key1 is included; key2 is included and excluded, therefore it's included; key3 is excluded
   127  
   128  			um, err := store.GetMembership(fakeUserHash)
   129  			require.NoError(t, err)
   130  			assertEqualMembership(t, []string{"key1", "key2"}, []string{"key3"}, um)
   131  		})
   132  	})
   133  }
   134  
   135  func (s *BigSegmentStoreTestSuite) withStoreAndEmptyData(
   136  	t testbox.TestingT,
   137  	action func(subsystems.BigSegmentStore),
   138  ) {
   139  	require.NoError(t, s.clearDataFn(""))
   140  
   141  	testhelpers.WithMockLoggingContext(t, func(context subsystems.ClientContext) {
   142  		store, err := s.storeFactoryFn("").Build(context)
   143  		require.NoError(t, err)
   144  		defer func() {
   145  			_ = store.Close()
   146  		}()
   147  
   148  		action(store)
   149  	})
   150  }
   151  
   152  func assertEqualMembership(
   153  	t assert.TestingT,
   154  	expectedIncludes []string,
   155  	expectedExcludes []string,
   156  	actual subsystems.BigSegmentMembership,
   157  ) {
   158  	// Most store implementations should use our helper types from ldstoreimpl. If they do, then we
   159  	// can do an exact equality test. If they don't, then we'll just check that they include/exclude
   160  	// the right keys (which isn't quite as good because we can't prove that they don't also have
   161  	// other unwanted keys).
   162  	expected := ldstoreimpl.NewBigSegmentMembershipFromSegmentRefs(expectedIncludes, expectedExcludes)
   163  	if reflect.TypeOf(actual) == reflect.TypeOf(expected) {
   164  		assert.Equal(t, expected, actual)
   165  	} else {
   166  		for _, inc := range expectedIncludes {
   167  			assert.Equal(t, ldvalue.NewOptionalBool(true), actual.CheckMembership(inc), "for key %q", inc)
   168  		}
   169  		for _, exc := range expectedIncludes {
   170  			assert.Equal(t, ldvalue.NewOptionalBool(false), actual.CheckMembership(exc), "for key %q", exc)
   171  		}
   172  		// here's a key we'll never use, just to make sure it's not answering "yes" to everything
   173  		assert.Equal(t, ldvalue.OptionalBool{}, actual.CheckMembership("unused-key"), `for key "unused-key"`)
   174  	}
   175  }
   176  

View as plain text