diff --git a/pkg/config/polling_manager.go b/pkg/config/polling_manager.go index 9d1806e64..07911dbe7 100644 --- a/pkg/config/polling_manager.go +++ b/pkg/config/polling_manager.go @@ -95,11 +95,12 @@ func WithInitialDatafile(datafile []byte) OptionFunc { } } -// SyncConfig gets current datafile and updates projectConfig -func (cm *PollingProjectConfigManager) SyncConfig(datafile []byte) { +// SyncConfig downloads datafile and updates projectConfig +func (cm *PollingProjectConfigManager) SyncConfig() { var e error var code int var respHeaders http.Header + var datafile []byte closeMutex := func(e error) { cm.err = e @@ -107,40 +108,34 @@ func (cm *PollingProjectConfigManager) SyncConfig(datafile []byte) { } url := fmt.Sprintf(cm.datafileURLTemplate, cm.sdkKey) - if len(datafile) == 0 { - if cm.lastModified != "" { - lastModifiedHeader := utils.Header{Name: ModifiedSince, Value: cm.lastModified} - datafile, respHeaders, code, e = cm.requester.Get(url, lastModifiedHeader) - } else { - datafile, respHeaders, code, e = cm.requester.Get(url) - } - - if e != nil { - msg := "unable to fetch fresh datafile" - cmLogger.Warning(msg) - cm.configLock.Lock() - closeMutex(errors.New(fmt.Sprintf("%s, reason (http status code): %s", msg, e.Error()))) - return - } - - if code == http.StatusNotModified { - cmLogger.Debug("The datafile was not modified and won't be downloaded again") - return - } + if cm.lastModified != "" { + lastModifiedHeader := utils.Header{Name: ModifiedSince, Value: cm.lastModified} + datafile, respHeaders, code, e = cm.requester.Get(url, lastModifiedHeader) + } else { + datafile, respHeaders, code, e = cm.requester.Get(url) + } - // Save last-modified date from response header - lastModified := respHeaders.Get(LastModified) - if lastModified != "" { - cm.configLock.Lock() - cm.lastModified = lastModified - cm.configLock.Unlock() - } + if e != nil { + msg := "unable to fetch fresh datafile" + cmLogger.Warning(msg) + cm.configLock.Lock() + closeMutex(errors.New(fmt.Sprintf("%s, reason (http status code): %s", msg, e.Error()))) + return } - projectConfig, err := datafileprojectconfig.NewDatafileProjectConfig(datafile) + if code == http.StatusNotModified { + cmLogger.Debug("The datafile was not modified and won't be downloaded again") + return + } + // Save last-modified date from response header cm.configLock.Lock() + lastModified := respHeaders.Get(LastModified) + if lastModified != "" { + cm.lastModified = lastModified + } + projectConfig, err := datafileprojectconfig.NewDatafileProjectConfig(datafile) if err != nil { cmLogger.Warning("failed to create project config") closeMutex(errors.New("unable to parse datafile")) @@ -156,21 +151,11 @@ func (cm *PollingProjectConfigManager) SyncConfig(datafile []byte) { closeMutex(nil) return } - cmLogger.Debug(fmt.Sprintf("New datafile set with revision: %s. Old revision: %s", projectConfig.GetRevision(), previousRevision)) - cm.projectConfig = projectConfig - if cm.optimizelyConfig != nil { - cm.optimizelyConfig = NewOptimizelyConfig(projectConfig) - } - closeMutex(nil) - - if cm.notificationCenter != nil { - projectConfigUpdateNotification := notification.ProjectConfigUpdateNotification{ - Type: notification.ProjectConfigUpdate, - Revision: cm.projectConfig.GetRevision(), - } - if err = cm.notificationCenter.Send(notification.ProjectConfigUpdate, projectConfigUpdateNotification); err != nil { - cmLogger.Warning("Problem with sending notification") - } + err = cm.setConfig(projectConfig) + closeMutex(err) + if err == nil { + cmLogger.Debug(fmt.Sprintf("New datafile set with revision: %s. Old revision: %s", projectConfig.GetRevision(), previousRevision)) + cm.sendConfigUpdateNotification() } } @@ -181,7 +166,7 @@ func (cm *PollingProjectConfigManager) Start(ctx context.Context) { for { select { case <-t.C: - cm.SyncConfig([]byte{}) + cm.SyncConfig() case <-ctx.Done(): cmLogger.Debug("Polling Config Manager Stopped") return @@ -204,8 +189,30 @@ func NewPollingProjectConfigManager(sdkKey string, pollingMangerOptions ...Optio opt(&pollingProjectConfigManager) } - initDatafile := pollingProjectConfigManager.initDatafile - pollingProjectConfigManager.SyncConfig(initDatafile) // initial poll + if len(pollingProjectConfigManager.initDatafile) > 0 { + pollingProjectConfigManager.setInitialDatafile(pollingProjectConfigManager.initDatafile) + } else { + pollingProjectConfigManager.SyncConfig() // initial poll + } + return &pollingProjectConfigManager +} + +// NewAsyncPollingProjectConfigManager returns an instance of the async polling config manager with the customized configuration +func NewAsyncPollingProjectConfigManager(sdkKey string, pollingMangerOptions ...OptionFunc) *PollingProjectConfigManager { + + pollingProjectConfigManager := PollingProjectConfigManager{ + notificationCenter: registry.GetNotificationCenter(sdkKey), + pollingInterval: DefaultPollingInterval, + requester: utils.NewHTTPRequester(), + datafileURLTemplate: DatafileURLTemplate, + sdkKey: sdkKey, + } + + for _, opt := range pollingMangerOptions { + opt(&pollingProjectConfigManager) + } + + pollingProjectConfigManager.setInitialDatafile(pollingProjectConfigManager.initDatafile) return &pollingProjectConfigManager } @@ -256,3 +263,38 @@ func (cm *PollingProjectConfigManager) RemoveOnProjectConfigUpdate(id int) error } return nil } + +func (cm *PollingProjectConfigManager) setConfig(projectConfig ProjectConfig) error { + if projectConfig == nil { + return errors.New("unable to set nil config") + } + cm.projectConfig = projectConfig + if cm.optimizelyConfig != nil { + cm.optimizelyConfig = NewOptimizelyConfig(projectConfig) + } + return nil +} + +func (cm *PollingProjectConfigManager) setInitialDatafile(datafile []byte) { + if len(datafile) != 0 { + cm.configLock.Lock() + defer cm.configLock.Unlock() + projectConfig, err := datafileprojectconfig.NewDatafileProjectConfig(datafile) + if projectConfig != nil { + err = cm.setConfig(projectConfig) + } + cm.err = err + } +} + +func (cm *PollingProjectConfigManager) sendConfigUpdateNotification() { + if cm.notificationCenter != nil { + projectConfigUpdateNotification := notification.ProjectConfigUpdateNotification{ + Type: notification.ProjectConfigUpdate, + Revision: cm.projectConfig.GetRevision(), + } + if err := cm.notificationCenter.Send(notification.ProjectConfigUpdate, projectConfigUpdateNotification); err != nil { + cmLogger.Warning("Problem with sending notification") + } + } +} diff --git a/pkg/config/polling_manager_test.go b/pkg/config/polling_manager_test.go index 6e593e636..4d523026a 100644 --- a/pkg/config/polling_manager_test.go +++ b/pkg/config/polling_manager_test.go @@ -19,6 +19,7 @@ package config import ( "context" "net/http" + "sync/atomic" "testing" "time" @@ -44,27 +45,77 @@ func newExecGroup() *utils.ExecGroup { return utils.NewExecGroup(context.Background()) } +// assertion method to periodically check target function each tick. +func assertPeriodically(t *testing.T, evaluationMethod func() bool) { + assert.Eventually(t, func() bool { + return evaluationMethod() + }, 500*time.Millisecond, 110*time.Millisecond) +} + func TestNewPollingProjectConfigManagerWithOptions(t *testing.T) { + invalidDatafile := []byte(`INVALID`) + mockDatafile := []byte(`{"revision":"42"}`) + + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(invalidDatafile, http.Header{}, http.StatusOK, nil).Times(1) + + // Test we fetch using requester (invalid datafile) + sdkKey := "test_sdk_key" + eg := newExecGroup() + configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester), WithPollingInterval(100*time.Millisecond)) + mockRequester.AssertExpectations(t) + + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile, http.Header{}, http.StatusOK, nil).Times(1) + // poll after 100ms + eg.Go(configManager.Start) + evaluationMethod := func() bool { + actual, _ := configManager.GetConfig() + return actual.GetRevision() == "42" + } + assertPeriodically(t, evaluationMethod) + mockRequester.AssertExpectations(t) + eg.TerminateAndWait() +} + +func TestNewAsyncPollingProjectConfigManagerWithOptions(t *testing.T) { + mockDatafile := []byte(`{"revision":"42"}`) - projectConfig, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile) mockRequester := new(MockRequester) mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile, http.Header{}, http.StatusOK, nil) // Test we fetch using requester sdkKey := "test_sdk_key" - eg := newExecGroup() - configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) + configManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester), WithPollingInterval(100*time.Millisecond)) + + // poll after 100ms eg.Go(configManager.Start) + evaluationMethod := func() bool { + actual, _ := configManager.GetConfig() + return actual.GetRevision() == "42" + } + assertPeriodically(t, evaluationMethod) mockRequester.AssertExpectations(t) + eg.TerminateAndWait() +} + +func TestSyncConfigFetchesDatafileUsingRequester(t *testing.T) { + + mockDatafile := []byte(`{"revision":"42"}`) + projectConfig, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile) + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile, http.Header{}, http.StatusOK, nil) + + sdkKey := "test_sdk_key" + configManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) + configManager.SyncConfig() + mockRequester.AssertCalled(t, "Get", []utils.Header(nil)) actual, err := configManager.GetConfig() assert.Nil(t, err) assert.NotNil(t, actual) assert.Equal(t, projectConfig, actual) - - eg.TerminateAndWait() // just sending signal and improving coverage } func TestNewPollingProjectConfigManagerWithNull(t *testing.T) { @@ -72,40 +123,97 @@ func TestNewPollingProjectConfigManagerWithNull(t *testing.T) { mockRequester := new(MockRequester) mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile, http.Header{}, http.StatusOK, nil) - // Test we fetch using requester sdkKey := "test_sdk_key" - - eg := newExecGroup() configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) - eg.Go(configManager.Start) mockRequester.AssertExpectations(t) _, err := configManager.GetConfig() assert.NotNil(t, err) } +func TestNewAsyncPollingProjectConfigManagerWithNullDatafile(t *testing.T) { + mockDatafile := []byte("NOT-VALID") + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile, http.Header{}, http.StatusOK, nil) + + sdkKey := "test_sdk_key" + configManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) + + // Sync with null datafile + configManager.SyncConfig() + _, err := configManager.GetConfig() + assert.NotNil(t, err) + mockRequester.AssertExpectations(t) +} + func TestNewPollingProjectConfigManagerWithSimilarDatafileRevisions(t *testing.T) { + // Test newer datafile should not replace the older one if revisions are the same mockDatafile1 := []byte(`{"revision":"42","botFiltering":true}`) mockDatafile2 := []byte(`{"revision":"42","botFiltering":false}`) projectConfig1, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile1) mockRequester := new(MockRequester) - mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil) sdkKey := "test_sdk_key" + configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester), WithInitialDatafile(mockDatafile1)) - eg := newExecGroup() - configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) - eg.Go(configManager.Start) - mockRequester.AssertExpectations(t) + // Verify no notifications were sent since datafile update should not occur + var numberOfCalls uint64 = 0 + callback := func(notification notification.ProjectConfigUpdateNotification) { + atomic.AddUint64(&numberOfCalls, 1) + } + id, _ := configManager.OnProjectConfigUpdate(callback) + assert.NotEqual(t, 0, id) + // initialized with hardcoded datafile actual, err := configManager.GetConfig() assert.Nil(t, err) assert.NotNil(t, actual) assert.Equal(t, projectConfig1, actual) - configManager.SyncConfig(mockDatafile2) - actual, err = configManager.GetConfig() + // sync with datafile having similar revision + configManager.SyncConfig() + actual, _ = configManager.GetConfig() + assert.Equal(t, projectConfig1, actual) + mockRequester.AssertExpectations(t) + + // Check no notification was sent for similar datafile revision + assert.Equal(t, uint64(0), atomic.LoadUint64(&numberOfCalls)) +} + +func TestNewAsyncPollingProjectConfigManagerWithSimilarDatafileRevisions(t *testing.T) { + // Test newer datafile should not replace the older one if revisions are the same + mockDatafile1 := []byte(`{"revision":"42","botFiltering":true}`) + mockDatafile2 := []byte(`{"revision":"42","botFiltering":false}`) + projectConfig1, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile1) + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil) + + sdkKey := "test_sdk_key" + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester), WithInitialDatafile(mockDatafile1)) + + // Verify no notifications were sent since datafile update should not occur + var numberOfCalls uint64 = 0 + callback := func(notification notification.ProjectConfigUpdateNotification) { + atomic.AddUint64(&numberOfCalls, 1) + } + id, _ := asyncConfigManager.OnProjectConfigUpdate(callback) + assert.NotEqual(t, 0, id) + + // initialized with hardcoded datafile + actual, err := asyncConfigManager.GetConfig() + assert.Nil(t, err) + assert.NotNil(t, actual) assert.Equal(t, projectConfig1, actual) + + // sync with datafile having similar revision + asyncConfigManager.SyncConfig() + actual, _ = asyncConfigManager.GetConfig() + assert.Equal(t, projectConfig1, actual) + mockRequester.AssertExpectations(t) + + // Check no notification was sent for similar datafile revision + assert.Equal(t, uint64(0), atomic.LoadUint64(&numberOfCalls)) } func TestNewPollingProjectConfigManagerWithLastModifiedDates(t *testing.T) { @@ -120,79 +228,116 @@ func TestNewPollingProjectConfigManagerWithLastModifiedDates(t *testing.T) { mockRequester.On("Get", []utils.Header{utils.Header{Name: ModifiedSince, Value: modifiedDate}}).Return([]byte{}, responseHeaders, http.StatusNotModified, nil) sdkKey := "test_sdk_key" - - eg := newExecGroup() configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) - eg.Go(configManager.Start) - // Fetch valid config + // Fetch valid config (initial poll) actual, err := configManager.GetConfig() assert.Nil(t, err) assert.NotNil(t, actual) assert.Equal(t, projectConfig1, actual) - // Sync and check no changes were made to the previous config because of 304 error code - configManager.SyncConfig([]byte{}) - actual, err = configManager.GetConfig() - assert.Nil(t, err) - assert.NotNil(t, actual) - assert.Equal(t, projectConfig1, actual) + // Sync and check no changes were made to the previous config because of 304 error code (second poll) + configManager.SyncConfig() + actual, _ = configManager.GetConfig() + assert.Equal(t, "42", actual.GetRevision()) + mockRequester.AssertExpectations(t) +} + +func TestNewAsyncPollingProjectConfigManagerWithLastModifiedDates(t *testing.T) { + mockDatafile1 := []byte(`{"revision":"42","botFiltering":true}`) + mockRequester := new(MockRequester) + modifiedDate := "Wed, 16 Oct 2019 20:16:45 GMT" + responseHeaders := http.Header{} + responseHeaders.Set(LastModified, modifiedDate) + + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, responseHeaders, http.StatusOK, nil) + mockRequester.On("Get", []utils.Header{utils.Header{Name: ModifiedSince, Value: modifiedDate}}).Return([]byte{}, responseHeaders, http.StatusNotModified, nil) + + sdkKey := "test_sdk_key" + configManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) + + // Fetch valid config (first poll) + configManager.SyncConfig() + actual, _ := configManager.GetConfig() + assert.Equal(t, "42", actual.GetRevision()) + + // Sync and check no changes were made to the previous config because of 304 error code (second poll) + configManager.SyncConfig() + actual, _ = configManager.GetConfig() + assert.Equal(t, "42", actual.GetRevision()) mockRequester.AssertExpectations(t) } func TestNewPollingProjectConfigManagerWithDifferentDatafileRevisions(t *testing.T) { + // Test newer datafile should replace the older one if revisions are different mockDatafile1 := []byte(`{"revision":"42","botFiltering":true}`) mockDatafile2 := []byte(`{"revision":"43","botFiltering":false}`) projectConfig1, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile1) projectConfig2, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile2) mockRequester := new(MockRequester) - mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil) - // Test we fetch using requester sdkKey := "test_sdk_key" + configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester), WithInitialDatafile(mockDatafile1)) - eg := newExecGroup() - configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) - eg.Go(configManager.Start) - mockRequester.AssertExpectations(t) + // To verify ConfigUpdate notification was sent + var numberOfCalls uint64 = 0 + callback := func(notification notification.ProjectConfigUpdateNotification) { + atomic.AddUint64(&numberOfCalls, 1) + } + id, _ := configManager.OnProjectConfigUpdate(callback) + assert.NotEqual(t, 0, id) + // initialized with hardcoded datafile actual, err := configManager.GetConfig() assert.Nil(t, err) assert.NotNil(t, actual) assert.Equal(t, projectConfig1, actual) - configManager.SyncConfig(mockDatafile2) - actual, err = configManager.GetConfig() + // sync with datafile having different revision + configManager.SyncConfig() + actual, _ = configManager.GetConfig() assert.Equal(t, projectConfig2, actual) + mockRequester.AssertExpectations(t) + + // Check notification was sent for different datafile revision + assert.Equal(t, uint64(1), atomic.LoadUint64(&numberOfCalls)) } -func TestPollingGetOptimizelyConfig(t *testing.T) { +func TestNewAsyncPollingProjectConfigManagerWithDifferentDatafileRevisions(t *testing.T) { + // Test newer datafile should replace the older one if revisions are different mockDatafile1 := []byte(`{"revision":"42","botFiltering":true}`) mockDatafile2 := []byte(`{"revision":"43","botFiltering":false}`) + projectConfig1, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile1) + projectConfig2, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile2) mockRequester := new(MockRequester) - mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil) - // Test we fetch using requester sdkKey := "test_sdk_key" + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester), WithInitialDatafile(mockDatafile1)) - eg := newExecGroup() - configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) - eg.Go(configManager.Start) - mockRequester.AssertExpectations(t) - - assert.Nil(t, configManager.optimizelyConfig) + // To verify ConfigUpdate notification was sent + var numberOfCalls uint64 = 0 + callback := func(notification notification.ProjectConfigUpdateNotification) { + atomic.AddUint64(&numberOfCalls, 1) + } + id, _ := asyncConfigManager.OnProjectConfigUpdate(callback) + assert.NotEqual(t, 0, id) - projectConfig, err := configManager.GetConfig() + // initialized with hardcoded datafile + actual, err := asyncConfigManager.GetConfig() assert.Nil(t, err) - assert.NotNil(t, projectConfig) - optimizelyConfig := configManager.GetOptimizelyConfig() - - assert.Equal(t, "42", optimizelyConfig.Revision) + assert.NotNil(t, actual) + assert.Equal(t, projectConfig1, actual) - configManager.SyncConfig(mockDatafile2) - optimizelyConfig = configManager.GetOptimizelyConfig() - assert.Equal(t, "43", optimizelyConfig.Revision) + // sync with datafile having different revision + asyncConfigManager.SyncConfig() + actual, _ = asyncConfigManager.GetConfig() + assert.Equal(t, projectConfig2, actual) + mockRequester.AssertExpectations(t) + // Check notification was sent for different datafile revision + assert.Equal(t, uint64(1), atomic.LoadUint64(&numberOfCalls)) } func TestNewPollingProjectConfigManagerWithErrorHandling(t *testing.T) { @@ -202,91 +347,254 @@ func TestNewPollingProjectConfigManagerWithErrorHandling(t *testing.T) { projectConfig1, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile1) projectConfig2, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile2) mockRequester := new(MockRequester) - mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil).Times(1) - // Test we fetch using requester sdkKey := "test_sdk_key" - - eg := newExecGroup() configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) - eg.Go(configManager.Start) + // verifying initial poll for bad file mockRequester.AssertExpectations(t) - - actual, err := configManager.GetConfig() // polling for bad file + actual, err := configManager.GetConfig() assert.NotNil(t, err) assert.Nil(t, actual) assert.Nil(t, projectConfig1) - configManager.SyncConfig(mockDatafile2) // polling for good file + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil).Times(1) + configManager.SyncConfig() // polling for good file + mockRequester.AssertExpectations(t) actual, err = configManager.GetConfig() assert.Nil(t, err) assert.Equal(t, projectConfig2, actual) - configManager.SyncConfig(mockDatafile1) // polling for bad file, error not null but good project + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil).Times(1) + configManager.SyncConfig() // polling for bad file, error not null but good project + mockRequester.AssertExpectations(t) actual, err = configManager.GetConfig() assert.Nil(t, err) assert.Equal(t, projectConfig2, actual) } +func TestNewAsyncPollingProjectConfigManagerWithErrorHandling(t *testing.T) { + mockDatafile1 := []byte("NOT-VALID") + mockDatafile2 := []byte(`{"revision":"43","botFiltering":false}`) + + projectConfig1, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile1) + projectConfig2, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile2) + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil).Times(1) + + sdkKey := "test_sdk_key" + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) + + asyncConfigManager.SyncConfig() // polling for bad file + mockRequester.AssertExpectations(t) + actual, err := asyncConfigManager.GetConfig() + assert.NotNil(t, err) + assert.Nil(t, actual) + assert.Nil(t, projectConfig1) + + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil).Times(1) + asyncConfigManager.SyncConfig() // polling for good file + mockRequester.AssertExpectations(t) + actual, err = asyncConfigManager.GetConfig() + assert.Nil(t, err) + assert.Equal(t, projectConfig2, actual) + + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil).Times(1) + asyncConfigManager.SyncConfig() // polling for bad file, error not null but good project + mockRequester.AssertExpectations(t) + actual, err = asyncConfigManager.GetConfig() + assert.Nil(t, err) + assert.Equal(t, projectConfig2, actual) +} + func TestNewPollingProjectConfigManagerOnDecision(t *testing.T) { mockDatafile1 := []byte(`{"revision":"42","botFiltering":true}`) mockDatafile2 := []byte(`{"revision":"43","botFiltering":false}`) + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil) + + sdkKey := "test_sdk_key" + configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester), WithInitialDatafile(mockDatafile1)) + // To verify ConfigUpdate notification is sent when config is updated + var numberOfCalls uint64 = 0 + callback := func(notification notification.ProjectConfigUpdateNotification) { + atomic.AddUint64(&numberOfCalls, 1) + } + id, _ := configManager.OnProjectConfigUpdate(callback) + assert.NotEqual(t, 0, id) + + // initialized with hardcoded datafile + config1, err := configManager.GetConfig() + assert.Nil(t, err) + assert.NotNil(t, config1) + assert.Equal(t, uint64(0), atomic.LoadUint64(&numberOfCalls)) + + // Sync new datafile and test onDecision + configManager.SyncConfig() + config2, _ := configManager.GetConfig() + assert.NotEqual(t, config1, config2) + mockRequester.AssertExpectations(t) + assert.Equal(t, uint64(1), atomic.LoadUint64(&numberOfCalls)) + err = configManager.RemoveOnProjectConfigUpdate(id) + assert.Nil(t, err) +} + +func TestNewAsyncPollingProjectConfigManagerOnDecision(t *testing.T) { + mockDatafile1 := []byte(`{"revision":"42","botFiltering":true}`) + projectConfig1, _ := datafileprojectconfig.NewDatafileProjectConfig(mockDatafile1) mockRequester := new(MockRequester) mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil) - // Test we fetch using requester sdkKey := "test_sdk_key" + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) - eg := newExecGroup() - configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) - eg.Go(configManager.Start) - - var numberOfCalls = 0 + // To verify ConfigUpdate notification is sent when config is updated + var numberOfCalls uint64 = 0 callback := func(notification notification.ProjectConfigUpdateNotification) { - numberOfCalls++ + atomic.AddUint64(&numberOfCalls, 1) } - id, _ := configManager.OnProjectConfigUpdate(callback) + id, _ := asyncConfigManager.OnProjectConfigUpdate(callback) + assert.NotEqual(t, 0, id) + + // Sync new datafile and test onDecision + asyncConfigManager.SyncConfig() + actual, _ := asyncConfigManager.GetConfig() + assert.Equal(t, projectConfig1, actual) mockRequester.AssertExpectations(t) + assert.Equal(t, uint64(1), atomic.LoadUint64(&numberOfCalls)) + err := asyncConfigManager.RemoveOnProjectConfigUpdate(id) + assert.Nil(t, err) +} - actual, err := configManager.GetConfig() +func TestGetOptimizelyConfigForNewPollingProjectConfigManager(t *testing.T) { + + mockDatafile1 := []byte(`{"revision":"42","botFiltering":true}`) + mockDatafile2 := []byte(`{"revision":"43","botFiltering":false}`) + + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil) + + sdkKey := "test_sdk_key" + configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester), WithInitialDatafile(mockDatafile1)) + + // initialized with hardcoded datafile + projectConfig, err := configManager.GetConfig() assert.Nil(t, err) - assert.NotNil(t, actual) + assert.NotNil(t, projectConfig) + optimizelyConfig := configManager.GetOptimizelyConfig() + assert.Equal(t, "42", optimizelyConfig.Revision) - configManager.SyncConfig(mockDatafile2) - actual, err = configManager.GetConfig() + // Sync to update datafile + configManager.SyncConfig() + optimizelyConfig = configManager.GetOptimizelyConfig() + assert.Equal(t, "43", optimizelyConfig.Revision) + mockRequester.AssertExpectations(t) +} + +func TestGetOptimizelyConfigForNewAsyncPollingProjectConfigManager(t *testing.T) { + mockDatafile1 := []byte(`{"revision":"42","botFiltering":true}`) + mockDatafile2 := []byte(`{"revision":"43","botFiltering":false}`) + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil) + + sdkKey := "test_sdk_key" + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester), WithInitialDatafile(mockDatafile1)) + + // initialized with hardcoded datafile + projectConfig, err := asyncConfigManager.GetConfig() assert.Nil(t, err) - assert.NotNil(t, actual) + assert.NotNil(t, projectConfig) + optimizelyConfig := asyncConfigManager.GetOptimizelyConfig() + assert.Equal(t, "42", optimizelyConfig.Revision) + + // Sync to update datafile + asyncConfigManager.SyncConfig() + optimizelyConfig = asyncConfigManager.GetOptimizelyConfig() + assert.Equal(t, "43", optimizelyConfig.Revision) + mockRequester.AssertExpectations(t) +} - assert.NotEqual(t, id, 0) - assert.Equal(t, numberOfCalls, 1) +func TestNewPollingProjectConfigManagerHardcodedDatafile(t *testing.T) { + mockDatafile1 := []byte(`{"revision":"42"}`) + mockDatafile2 := []byte(`{"revision":"43"}`) + sdkKey := "test_sdk_key" - err = configManager.RemoveOnProjectConfigUpdate(id) + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil) + + configManager := NewPollingProjectConfigManager(sdkKey, WithInitialDatafile(mockDatafile1), WithRequester(mockRequester)) + mockRequester.AssertNotCalled(t, "Get", []utils.Header(nil)) + + config, err := configManager.GetConfig() assert.Nil(t, err) + assert.NotNil(t, config) + assert.Equal(t, "42", config.GetRevision()) +} - err = configManager.RemoveOnProjectConfigUpdate(id) +func TestNewAsyncPollingProjectConfigManagerHardcodedDatafile(t *testing.T) { + mockDatafile1 := []byte(`{"revision":"42"}`) + mockDatafile2 := []byte(`{"revision":"43"}`) + sdkKey := "test_sdk_key" + + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile2, http.Header{}, http.StatusOK, nil) + + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithInitialDatafile(mockDatafile1), WithRequester(mockRequester)) + mockRequester.AssertNotCalled(t, "Get", []utils.Header(nil)) + + config, err := asyncConfigManager.GetConfig() assert.Nil(t, err) + assert.NotNil(t, config) + assert.Equal(t, "42", config.GetRevision()) } -func TestPollingInterval(t *testing.T) { +func TestNewPollingProjectConfigManagerPullsImmediately(t *testing.T) { + mockDatafile1 := []byte(`{"revision":"42"}`) + sdkKey := "test_sdk_key" + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil) + + configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) + config, err := configManager.GetConfig() + + mockRequester.AssertExpectations(t) + assert.Nil(t, err) + assert.NotNil(t, config) + assert.Equal(t, "42", config.GetRevision()) +} + +func TestNewAsyncPollingProjectConfigManagerDoesNotPullImmediately(t *testing.T) { + mockDatafile1 := []byte(`{"revision":"42"}`) sdkKey := "test_sdk_key" - eg := newExecGroup() + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile1, http.Header{}, http.StatusOK, nil) + + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) + config, _ := asyncConfigManager.GetConfig() + assert.Nil(t, config) +} + +func TestPollingInterval(t *testing.T) { + + sdkKey := "test_sdk_key" configManager := NewPollingProjectConfigManager(sdkKey, WithPollingInterval(5*time.Second)) - eg.Go(configManager.Start) + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithPollingInterval(5*time.Second)) assert.Equal(t, configManager.pollingInterval, 5*time.Second) + assert.Equal(t, asyncConfigManager.pollingInterval, 5*time.Second) } func TestInitialDatafile(t *testing.T) { sdkKey := "test_sdk_key" - eg := newExecGroup() configManager := NewPollingProjectConfigManager(sdkKey, WithInitialDatafile([]byte("test"))) - eg.Go(configManager.Start) + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithInitialDatafile([]byte("test"))) assert.Equal(t, configManager.initDatafile, []byte("test")) + assert.Equal(t, asyncConfigManager.initDatafile, []byte("test")) } func TestDatafileTemplate(t *testing.T) { @@ -294,6 +602,21 @@ func TestDatafileTemplate(t *testing.T) { sdkKey := "test_sdk_key" datafileTemplate := "https://localhost/v1/%s.json" configManager := NewPollingProjectConfigManager(sdkKey, WithDatafileURLTemplate(datafileTemplate)) + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithDatafileURLTemplate(datafileTemplate)) assert.Equal(t, datafileTemplate, configManager.datafileURLTemplate) + assert.Equal(t, datafileTemplate, asyncConfigManager.datafileURLTemplate) +} + +func TestWithRequester(t *testing.T) { + + sdkKey := "test_sdk_key" + mockDatafile := []byte(`{"revision":"42"}`) + mockRequester := new(MockRequester) + mockRequester.On("Get", []utils.Header(nil)).Return(mockDatafile, http.Header{}, http.StatusOK, nil) + configManager := NewPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) + asyncConfigManager := NewAsyncPollingProjectConfigManager(sdkKey, WithRequester(mockRequester)) + + assert.Equal(t, mockRequester, configManager.requester) + assert.Equal(t, mockRequester, asyncConfigManager.requester) }