...

Source file src/github.com/growthbook/growthbook-golang/feature_repository_test.go

Documentation: github.com/growthbook/growthbook-golang

     1  package growthbook
     2  
     3  import (
     4  	"encoding/json"
     5  	"net/http"
     6  	"net/http/httptest"
     7  	"reflect"
     8  	"runtime"
     9  	"strings"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/ian-ross/sse/v2"
    15  )
    16  
    17  type env struct {
    18  	sync.RWMutex
    19  	server       *httptest.Server
    20  	sseServer    *sse.Server
    21  	fetchFails   bool
    22  	featureValue *string
    23  	callCount    *int
    24  	urls         map[string]int
    25  }
    26  
    27  func (e *env) checkCalls(t *testing.T, expected int) {
    28  	e.RLock()
    29  	defer e.RUnlock()
    30  	if *e.callCount != expected {
    31  		t.Errorf("Expected %d calls to API, got %d", expected, *e.callCount)
    32  	}
    33  }
    34  
    35  func (e *env) close() {
    36  	if e.sseServer != nil {
    37  		e.sseServer.Close()
    38  	}
    39  	e.server.Close()
    40  }
    41  
    42  func setupEncrypted(features string, provideSSE bool) *env {
    43  	return setupWithDelay(provideSSE, 50*time.Millisecond, features)
    44  }
    45  
    46  func setup(provideSSE bool) *env {
    47  	return setupWithDelay(provideSSE, 50*time.Millisecond, "")
    48  }
    49  
    50  func setupWithDelay(provideSSE bool, delay time.Duration, encryptedFeatures string) *env {
    51  	SetLogger(&testLog)
    52  	testLog.reset()
    53  
    54  	initialValue := "initial"
    55  	callCount := 0
    56  	urls := map[string]int{}
    57  	env := env{featureValue: &initialValue, callCount: &callCount, urls: urls}
    58  
    59  	// We need to set up a mock server to handle normal API requests and
    60  	// SSE updates.
    61  	mux := http.NewServeMux()
    62  
    63  	// Normal GET.
    64  	mux.HandleFunc("/api/features/", func(w http.ResponseWriter, r *http.Request) {
    65  		env.Lock()
    66  		defer env.Unlock()
    67  		callCount++
    68  		env.urls[r.URL.Path]++
    69  
    70  		time.Sleep(delay)
    71  
    72  		if env.fetchFails {
    73  			w.WriteHeader(http.StatusBadRequest)
    74  			w.Write([]byte("fetch failed"))
    75  		} else {
    76  			features := FeatureMap{}
    77  			if encryptedFeatures == "" {
    78  				features = FeatureMap{"foo": {DefaultValue: *env.featureValue}}
    79  			}
    80  			response := &FeatureAPIResponse{
    81  				Features:          features,
    82  				DateUpdated:       time.Now(),
    83  				EncryptedFeatures: encryptedFeatures,
    84  			}
    85  
    86  			if provideSSE {
    87  				w.Header().Set("X-SSE-Support", "enabled")
    88  			}
    89  			w.Header().Set("Content-Type", "application/json")
    90  			w.WriteHeader(http.StatusOK)
    91  			json.NewEncoder(w).Encode(response)
    92  		}
    93  	})
    94  
    95  	// SSE server handler.
    96  	if provideSSE {
    97  		env.sseServer = sse.New()
    98  		env.sseServer.CreateStream("features")
    99  		mux.HandleFunc("/sub/", env.sseServer.ServeHTTP)
   100  	}
   101  
   102  	env.server = httptest.NewServer(mux)
   103  
   104  	return &env
   105  }
   106  
   107  func makeGB(apiHost string, clientKey string, ttl time.Duration) *GrowthBook {
   108  	context := NewContext().
   109  		WithAPIHost(apiHost).
   110  		WithClientKey(clientKey)
   111  	if ttl != 0 {
   112  		context = context.WithCacheTTL(ttl)
   113  	}
   114  	return New(context)
   115  }
   116  
   117  func checkFeature(t *testing.T, gb *GrowthBook, feature string, expected interface{}) {
   118  	value := gb.EvalFeature(feature).Value
   119  	if value != expected {
   120  		t.Errorf("feature value, expected %v, got %v", expected, value)
   121  	}
   122  }
   123  
   124  func checkLogs(t *testing.T) {
   125  	if len(testLog.errors) != 0 {
   126  		t.Errorf("test log has errors: %s", testLog.allErrors())
   127  	}
   128  	if len(testLog.warnings) != 0 {
   129  		bad := false
   130  		for _, e := range testLog.errors {
   131  			if !strings.Contains(e, "SSE event stream disconnected") {
   132  				bad = true
   133  			}
   134  		}
   135  		if bad {
   136  			t.Errorf("test log has warnings: %s", testLog.allWarnings())
   137  		}
   138  	}
   139  }
   140  
   141  func knownWarnings(t *testing.T, count int) {
   142  	if len(testLog.errors) != 0 {
   143  		t.Error("found errors when looking for known warnings: ", testLog.allErrors())
   144  		return
   145  	}
   146  	if len(testLog.warnings) == count {
   147  		testLog.reset()
   148  		return
   149  	}
   150  
   151  	t.Errorf("expected %d log warnings, got %d: %s", count,
   152  		len(testLog.warnings), testLog.allWarnings())
   153  }
   154  
   155  func knownErrors(t *testing.T, messages ...string) {
   156  	if len(testLog.errors) != len(messages) {
   157  		t.Errorf("expected %d log errors, got %d: %s", len(messages),
   158  			len(testLog.errors), testLog.allErrors())
   159  		return
   160  	}
   161  
   162  	for i, msg := range messages {
   163  		if !strings.HasPrefix(testLog.errors[i], msg) {
   164  			t.Errorf("expected error message %d '%s...', got '%s'", i+1, msg, testLog.errors[i])
   165  		}
   166  	}
   167  
   168  	testLog.reset()
   169  }
   170  
   171  func knownSSEErrors(t *testing.T) func() {
   172  	return func() {
   173  		for _, msg := range testLog.errors {
   174  			if !strings.HasPrefix(msg, "SSE error:") {
   175  				t.Errorf("unexpected error in log: '%s'", msg)
   176  			}
   177  		}
   178  	}
   179  }
   180  
   181  func checkReady(t *testing.T, gb *GrowthBook, expected bool) {
   182  	if gb.Ready() != expected {
   183  		t.Errorf("expected ready flag to be %v", expected)
   184  	}
   185  }
   186  
   187  func checkEmptyFeatures(t *testing.T, gb *GrowthBook) {
   188  	if len(gb.Features()) != 0 {
   189  		t.Error("expected feature map to be empty")
   190  	}
   191  }
   192  
   193  func TestRepoDebounceFetchRequests(t *testing.T) {
   194  	env := setup(false)
   195  	defer cache.Clear()
   196  	defer checkLogs(t)
   197  	defer env.close()
   198  
   199  	cache.Clear()
   200  
   201  	gb1 := makeGB(env.server.URL, "qwerty1234", 0)
   202  	gb2 := makeGB(env.server.URL, "other", 0)
   203  	gb3 := makeGB(env.server.URL, "qwerty1234", 0)
   204  
   205  	gb1.LoadFeatures(nil)
   206  	gb2.LoadFeatures(nil)
   207  	gb3.LoadFeatures(nil)
   208  
   209  	env.checkCalls(t, 2)
   210  	if env.urls["/api/features/other"] != 1 ||
   211  		env.urls["/api/features/qwerty1234"] != 1 {
   212  		t.Errorf("unexpected URL calls: %v", env.urls)
   213  	}
   214  
   215  	checkFeature(t, gb1, "foo", "initial")
   216  	checkFeature(t, gb2, "foo", "initial")
   217  	checkFeature(t, gb3, "foo", "initial")
   218  }
   219  
   220  func TestRepoUsesCacheAndCanRefreshManually(t *testing.T) {
   221  	env := setup(false)
   222  	defer cache.Clear()
   223  	defer checkLogs(t)
   224  	defer env.close()
   225  
   226  	cache.Clear()
   227  
   228  	// Set cache TTL short so we can test expiry.
   229  	gb := makeGB(env.server.URL, "qwerty1234", 100*time.Millisecond)
   230  	time.Sleep(20 * time.Millisecond)
   231  
   232  	// Initial value of feature should be null.
   233  	checkFeature(t, gb, "foo", nil)
   234  	env.checkCalls(t, 1)
   235  	knownWarnings(t, 1)
   236  
   237  	// Once features are loaded, value should be from the fetch request.
   238  	gb.LoadFeatures(nil)
   239  	checkFeature(t, gb, "foo", "initial")
   240  	env.checkCalls(t, 1)
   241  
   242  	// Value changes in API
   243  	*env.featureValue = "changed"
   244  
   245  	// New instances should get cached value
   246  	gb2 := makeGB(env.server.URL, "qwerty1234", 100*time.Millisecond)
   247  	checkFeature(t, gb2, "foo", nil)
   248  	knownWarnings(t, 1)
   249  	gb2.LoadFeatures(&FeatureRepoOptions{AutoRefresh: true})
   250  	checkFeature(t, gb2, "foo", "initial")
   251  
   252  	// Instance without autoRefresh.
   253  	gb3 := makeGB(env.server.URL, "qwerty1234", 100*time.Millisecond)
   254  	checkFeature(t, gb3, "foo", nil)
   255  	knownWarnings(t, 1)
   256  	gb3.LoadFeatures(nil)
   257  	checkFeature(t, gb3, "foo", "initial")
   258  
   259  	env.checkCalls(t, 1)
   260  
   261  	// Old instances should also get cached value.
   262  	checkFeature(t, gb, "foo", "initial")
   263  
   264  	// Refreshing while cache is fresh should not cause a new network
   265  	// request.
   266  	gb.RefreshFeatures(nil)
   267  	env.checkCalls(t, 1)
   268  
   269  	// Wait a bit for cache to become stale and refresh again.
   270  	time.Sleep(100 * time.Millisecond)
   271  	gb.RefreshFeatures(nil)
   272  	env.checkCalls(t, 2)
   273  
   274  	// The instance being updated should get the new value.
   275  	checkFeature(t, gb, "foo", "changed")
   276  
   277  	// The instance with auto-refresh should now have the new value.
   278  	checkFeature(t, gb2, "foo", "changed")
   279  
   280  	// The instance without auto-refresh should continue to have the old
   281  	// value.
   282  	checkFeature(t, gb3, "foo", "initial")
   283  
   284  	// New instances should get the new value
   285  	gb4 := makeGB(env.server.URL, "qwerty1234", 100*time.Millisecond)
   286  	checkFeature(t, gb4, "foo", nil)
   287  	knownWarnings(t, 1)
   288  	gb4.LoadFeatures(nil)
   289  	checkFeature(t, gb4, "foo", "changed")
   290  
   291  	env.checkCalls(t, 2)
   292  }
   293  
   294  func TestRepoUpdatesFeaturesBasedOnSSE1(t *testing.T) {
   295  	env := setup(true)
   296  	defer cache.Clear()
   297  	defer checkLogs(t)
   298  	defer env.close()
   299  
   300  	cache.Clear()
   301  
   302  	gb := makeGB(env.server.URL, "qwerty1234", 0)
   303  
   304  	// This is needed just to force cleanup of the SSE refresh goroutine
   305  	// at test exit. This shouldn't be a problem in long-lived server
   306  	// processes.
   307  	defer func() {
   308  		gb = nil
   309  		runtime.GC()
   310  	}()
   311  
   312  	// Load features and check API calls.
   313  	gb.LoadFeatures(&FeatureRepoOptions{AutoRefresh: true})
   314  	env.checkCalls(t, 1)
   315  
   316  	// Check feature before SSE message.
   317  	checkFeature(t, gb, "foo", "initial")
   318  
   319  	// Trigger mock SSE send.
   320  	featuresJson := `{"features": {"foo": {"defaultValue": "changed"}}}`
   321  	env.sseServer.Publish("features", &sse.Event{Data: []byte(featuresJson)})
   322  
   323  	// Wait a little...
   324  	time.Sleep(20 * time.Millisecond)
   325  
   326  	// Check feature after SSE message.
   327  	checkFeature(t, gb, "foo", "changed")
   328  	env.checkCalls(t, 1)
   329  }
   330  
   331  func TestRepoUpdatesFeaturesBasedOnSSE2(t *testing.T) {
   332  	env := setup(true)
   333  	defer cache.Clear()
   334  	defer checkLogs(t)
   335  	defer env.close()
   336  
   337  	cache.Clear()
   338  
   339  	gb := makeGB(env.server.URL, "qwerty1234", 0)
   340  	gb2 := makeGB(env.server.URL, "qwerty1234", 0)
   341  
   342  	// This is needed just to force cleanup of the SSE refresh goroutine
   343  	// at test exit. This shouldn't be a problem in long-lived server
   344  	// processes.
   345  	defer func() {
   346  		gb = nil
   347  		gb2 = nil
   348  		runtime.GC()
   349  	}()
   350  
   351  	// Load features and check API calls.
   352  	gb.LoadFeatures(nil)
   353  	gb2.LoadFeatures(&FeatureRepoOptions{AutoRefresh: true})
   354  	env.checkCalls(t, 1)
   355  
   356  	// Check feature before SSE message.
   357  	checkFeature(t, gb, "foo", "initial")
   358  	checkFeature(t, gb2, "foo", "initial")
   359  
   360  	// Trigger mock SSE send.
   361  	featuresJson := `{"features": {"foo": {"defaultValue": "changed"}}}`
   362  	env.sseServer.Publish("features", &sse.Event{Data: []byte(featuresJson)})
   363  
   364  	// Wait a little...
   365  	time.Sleep(20 * time.Millisecond)
   366  
   367  	// Check feature after SSE message.
   368  	checkFeature(t, gb, "foo", "initial")
   369  	checkFeature(t, gb2, "foo", "changed")
   370  	env.checkCalls(t, 1)
   371  }
   372  
   373  func TestRepoExposesAReadyFlag(t *testing.T) {
   374  	env := setup(false)
   375  	defer cache.Clear()
   376  	defer checkLogs(t)
   377  	defer env.close()
   378  
   379  	cache.Clear()
   380  	*env.featureValue = "api"
   381  
   382  	gb := makeGB(env.server.URL, "qwerty1234", 0)
   383  
   384  	if gb.Ready() {
   385  		t.Error("expected ready flag to be false")
   386  	}
   387  	gb.LoadFeatures(nil)
   388  	env.checkCalls(t, 1)
   389  	if !gb.Ready() {
   390  		t.Error("expected ready flag to be true")
   391  	}
   392  
   393  	gb2 := makeGB(env.server.URL, "qwerty1234", 0)
   394  	if gb2.Ready() {
   395  		t.Error("expected ready flag to be false")
   396  	}
   397  	gb2.WithFeatures(FeatureMap{"foo": &Feature{DefaultValue: "manual"}})
   398  	if !gb2.Ready() {
   399  		t.Error("expected ready flag to be false")
   400  	}
   401  }
   402  
   403  func TestRepoHandlesBrokenFetchResponses(t *testing.T) {
   404  	env := setup(false)
   405  	defer cache.Clear()
   406  	defer checkLogs(t)
   407  	defer env.close()
   408  
   409  	cache.Clear()
   410  	env.fetchFails = true
   411  
   412  	gb := makeGB(env.server.URL, "qwerty1234", 0)
   413  	checkReady(t, gb, false)
   414  	gb.LoadFeatures(nil)
   415  
   416  	// Attempts network request, logs the error.
   417  	env.checkCalls(t, 1)
   418  	knownErrors(t, "Error fetching features")
   419  
   420  	// Ready state changes to true
   421  	checkReady(t, gb, true)
   422  	checkEmptyFeatures(t, gb)
   423  
   424  	// Logs the error, doesn't cache result.
   425  	gb.RefreshFeatures(nil)
   426  	checkEmptyFeatures(t, gb)
   427  	env.checkCalls(t, 2)
   428  	knownErrors(t, "Error fetching features")
   429  
   430  	checkLogs(t)
   431  }
   432  
   433  func TestRepoHandlesSuperLongAPIRequests(t *testing.T) {
   434  	env := setupWithDelay(false, 100*time.Millisecond, "")
   435  	defer cache.Clear()
   436  	defer checkLogs(t)
   437  	defer env.close()
   438  
   439  	cache.Clear()
   440  	*env.featureValue = "api"
   441  
   442  	gb := makeGB(env.server.URL, "qwerty1234", 0)
   443  	checkReady(t, gb, false)
   444  
   445  	// Doesn't throw errors.
   446  	gb.LoadFeatures(&FeatureRepoOptions{Timeout: 20 * time.Millisecond})
   447  	env.checkCalls(t, 1)
   448  	checkLogs(t)
   449  
   450  	// Ready state remains false.
   451  	checkReady(t, gb, false)
   452  	checkEmptyFeatures(t, gb)
   453  
   454  	// After fetch finished in the background, refreshing should
   455  	// actually finish in time.
   456  	time.Sleep(100 * time.Millisecond)
   457  	gb.RefreshFeatures(&FeatureRepoOptions{Timeout: 20 * time.Millisecond})
   458  	env.checkCalls(t, 1)
   459  	checkReady(t, gb, true)
   460  	checkFeature(t, gb, "foo", "api")
   461  }
   462  
   463  func TestRepoHandlesSSEErrors(t *testing.T) {
   464  	env := setup(true)
   465  	defer cache.Clear()
   466  	defer checkLogs(t)
   467  	defer env.close()
   468  
   469  	cache.Clear()
   470  
   471  	gb := makeGB(env.server.URL, "qwerty1234", 0)
   472  
   473  	gb.LoadFeatures(&FeatureRepoOptions{AutoRefresh: true})
   474  	env.checkCalls(t, 1)
   475  	checkFeature(t, gb, "foo", "initial")
   476  
   477  	// Simulate SSE data.
   478  	env.sseServer.Publish("features", &sse.Event{Data: []byte("broken(response")})
   479  
   480  	// After SSE fired, should log an error and feature value should
   481  	// remain the same.
   482  	time.Sleep(20 * time.Millisecond)
   483  	env.checkCalls(t, 1)
   484  	checkFeature(t, gb, "foo", "initial")
   485  	knownErrors(t, "SSE error")
   486  
   487  	cache.Clear()
   488  }
   489  
   490  // This is a more complex test scenario for checking that parallel
   491  // handling of auto-refresh, SSE updates and SSE errors works
   492  // correctly together.
   493  
   494  // Disabled for the moment...
   495  
   496  // func TestRepoComplexSSEScenario(t *testing.T) {
   497  // 	env := setup(true)
   498  // 	defer cache.Clear()
   499  // 	// We're going to generate SSE errors here, but that's all we expect
   500  // 	// to see.
   501  // 	defer knownSSEErrors(t)
   502  // 	defer env.close()
   503  
   504  // 	cache.Clear()
   505  
   506  // 	// Data recording for test goroutines.
   507  // 	type record struct {
   508  // 		result string
   509  // 		t      time.Time
   510  // 	}
   511  
   512  // 	var wg sync.WaitGroup
   513  
   514  // 	// Test function to run in a goroutine: evaluates features at
   515  // 	// randomly spaced intervals, storing the results and the sample
   516  // 	// times, until told to stop.
   517  // 	tester := func(gb *GrowthBook, doneCh chan struct{}, vals *[]*record) {
   518  // 		defer wg.Done()
   519  // 		tick := time.NewTicker(time.Duration(100+rand.Intn(100)) * time.Millisecond)
   520  // 		gb.LoadFeatures(&FeatureRepoOptions{AutoRefresh: true})
   521  // 		for {
   522  // 			select {
   523  // 			case <-doneCh:
   524  // 				return
   525  
   526  // 			case <-tick.C:
   527  // 				f, _ := gb.EvalFeature("foo").Value.(string)
   528  // 				*vals = append(*vals, &record{f, time.Now()})
   529  // 			}
   530  // 		}
   531  // 	}
   532  
   533  // 	// Set up test goroutines, each with an independent GrowthBook
   534  // 	// instance, cancellation channel and result storage.
   535  // 	gbs := make([]*GrowthBook, 10)
   536  // 	doneChs := make([]chan struct{}, 10)
   537  // 	vals := make([][]*record, 10)
   538  // 	wg.Add(10)
   539  // 	for i := 0; i < 10; i++ {
   540  // 		gbs[i] = makeGB(env.server.URL, "qwerty1234", 0)
   541  // 		doneChs[i] = make(chan struct{})
   542  // 		vals[i] = []*record{}
   543  // 		go tester(gbs[i], doneChs[i], &vals[i])
   544  // 	}
   545  
   546  // 	// Command storage.
   547  // 	type command struct {
   548  // 		cmd int
   549  // 		t   time.Time
   550  // 	}
   551  // 	commands := make([]command, 100)
   552  
   553  // 	// Command loop: send SSE events at random intervals, with
   554  // 	// approximately 10% failure rate (and always at least three
   555  // 	// failures in a row, to trigger SSE client reconnection).
   556  // 	bad := 0
   557  // 	for i := 0; i < 100; i++ {
   558  // 		ok := rand.Intn(100) < 90
   559  // 		if ok && bad == 0 {
   560  // 			featuresJson := fmt.Sprintf(
   561  // 				`{"features": {"foo": {"defaultValue": "val%d"}}, "dateUpdated": "%s"}`,
   562  // 				i+1, time.Now().Format(dateLayout))
   563  // 			commands[i] = command{i + 1, time.Now()}
   564  // 			env.sseServer.Publish("features", &sse.Event{Data: []byte(featuresJson)})
   565  // 		} else {
   566  // 			if bad == 0 {
   567  // 				bad = 3
   568  // 			} else {
   569  // 				bad--
   570  // 			}
   571  // 			commands[i] = command{-(i + 1), time.Now()}
   572  // 			env.sseServer.Publish("features", &sse.Event{Data: []byte("broken(bad")})
   573  // 		}
   574  // 		time.Sleep(time.Duration(50+rand.Intn(50)) * time.Millisecond)
   575  // 	}
   576  
   577  // 	// Stop the test goroutines and zero their GrowthBook instances so
   578  // 	// that finalizers will run and background SSE refresh will stop
   579  // 	// too.
   580  // 	for i := 0; i < 10; i++ {
   581  // 		doneChs[i] <- struct{}{}
   582  // 		gbs[i] = nil
   583  // 	}
   584  // 	wg.Wait()
   585  
   586  // 	// Check the results from the test goroutines by finding the
   587  // 	// relevant times in the command history. Allow some slack for small
   588  // 	// time differences.
   589  // 	errors := 0
   590  // 	for i := 0; i < 10; i++ {
   591  // 		for _, v := range vals[i] {
   592  // 			if v.result == "initial" {
   593  // 				continue
   594  // 			}
   595  // 			cmdidx, _ := sortFind(len(commands), func(i int) int {
   596  // 				if v.t == commands[i].t {
   597  // 					return 0
   598  // 				}
   599  // 				if v.t.After(commands[i].t) {
   600  // 					return 1
   601  // 				}
   602  // 				return -1
   603  // 			})
   604  
   605  // 			cmdidx--
   606  // 			expected := fmt.Sprintf("val%d", commands[cmdidx].cmd)
   607  
   608  // 			beforeidx := cmdidx - 1
   609  // 			for beforeidx > 0 && commands[beforeidx].cmd < 0 {
   610  // 				beforeidx--
   611  // 			}
   612  // 			before := fmt.Sprintf("val%d", commands[beforeidx].cmd)
   613  
   614  // 			afteridx := cmdidx + 1
   615  // 			for afteridx < len(commands)-1 && commands[afteridx].cmd < 0 {
   616  // 				afteridx++
   617  // 			}
   618  // 			after := ""
   619  // 			if afteridx < len(commands) {
   620  // 				after = fmt.Sprintf("val%d", commands[afteridx].cmd)
   621  // 			}
   622  
   623  // 			if v.result != expected && v.result != before && v.result != after {
   624  // 				errors++
   625  // 				t.Error("unexpected feature value")
   626  // 				fmt.Println(v.result, expected, v.t, cmdidx, beforeidx, afteridx)
   627  // 			}
   628  // 		}
   629  // 	}
   630  // }
   631  
   632  func TestRepoDoesntDoBackgroundSyncWhenDisabled(t *testing.T) {
   633  	env := setup(true)
   634  	defer cache.Clear()
   635  	defer checkLogs(t)
   636  	defer env.close()
   637  
   638  	cache.Clear()
   639  	ConfigureCacheBackgroundSync(false)
   640  	defer ConfigureCacheBackgroundSync(true)
   641  
   642  	gb := makeGB(env.server.URL, "qwerty1234", 0)
   643  	gb2 := makeGB(env.server.URL, "qwerty1234", 0)
   644  
   645  	gb.LoadFeatures(nil)
   646  	gb2.LoadFeatures(&FeatureRepoOptions{AutoRefresh: true})
   647  
   648  	// Initial value from API.
   649  	env.checkCalls(t, 1)
   650  	checkFeature(t, gb, "foo", "initial")
   651  	checkFeature(t, gb2, "foo", "initial")
   652  
   653  	// Trigger mock SSE send.
   654  	featuresJson := `{"features": {"foo": {"defaultValue": "changed"}}}`
   655  	env.sseServer.Publish("features", &sse.Event{Data: []byte(featuresJson)})
   656  
   657  	// SSE update is ignored.
   658  	time.Sleep(100 * time.Millisecond)
   659  	checkFeature(t, gb, "foo", "initial")
   660  	checkFeature(t, gb2, "foo", "initial")
   661  	env.checkCalls(t, 1)
   662  }
   663  
   664  func TestRepoDecryptsFeatures(t *testing.T) {
   665  	encryptedFeatures := "vMSg2Bj/IurObDsWVmvkUg==.L6qtQkIzKDoE2Dix6IAKDcVel8PHUnzJ7JjmLjFZFQDqidRIoCxKmvxvUj2kTuHFTQ3/NJ3D6XhxhXXv2+dsXpw5woQf0eAgqrcxHrbtFORs18tRXRZza7zqgzwvcznx"
   666  
   667  	env := setupEncrypted(encryptedFeatures, false)
   668  	defer cache.Clear()
   669  	defer checkLogs(t)
   670  	defer env.close()
   671  
   672  	cache.Clear()
   673  
   674  	context := NewContext().
   675  		WithAPIHost(env.server.URL).
   676  		WithClientKey("qwerty1234").
   677  		WithDecryptionKey("Ns04T5n9+59rl2x3SlNHtQ==")
   678  	gb := New(context)
   679  
   680  	gb.LoadFeatures(nil)
   681  
   682  	env.checkCalls(t, 1)
   683  
   684  	expectedJson := `{
   685      "testfeature1": {
   686        "defaultValue": true,
   687        "rules": [{"condition": { "id": "1234" }, "force": false}]
   688      }
   689    }`
   690  	expected := ParseFeatureMap([]byte(expectedJson))
   691  	actual := gb.Features()
   692  
   693  	if !reflect.DeepEqual(actual, expected) {
   694  		t.Error("unexpected features value: ", actual)
   695  	}
   696  }
   697  

View as plain text