...

Source file src/github.com/launchdarkly/go-server-sdk/v6/ldclient_listeners_test.go

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

     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  // This file contains tests for all of the event broadcaster/listener functionality in the client, plus
    24  // related methods for looking at the same kinds of status values that can be broadcast to listeners.
    25  // It uses mock implementations of the data source and data store, so that it is only the status
    26  // monitoring mechanisms that are being tested, not the status behavior of specific real components.
    27  //
    28  // Parts of this functionality are also covered by lower-level component tests like
    29  // DataSourceUpdateSinkImplTest. However, the tests here verify that the client is wiring the components
    30  // together correctly so that they work from an application's point of view.
    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) // just verifying that the remove method works
   104  			th.AssertChannelClosed(t, ch2, time.Millisecond)
   105  
   106  			th.AssertNoMoreValues(t, ch1, timeout)
   107  			th.AssertNoMoreValues(t, ch3, timeout)
   108  
   109  			// make the flag true for the first user only, and broadcast a flag change event
   110  			p.testData.Update(p.testData.Flag(flagKey).VariationForUser(user.Key(), true))
   111  
   112  			// ch1 receives a value change event
   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  			// ch3 doesn't receive one, because the flag's value hasn't changed for otherUser
   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