...

Source file src/github.com/launchdarkly/go-server-sdk-evaluation/v2/evaluator_flag_test.go

Documentation: github.com/launchdarkly/go-server-sdk-evaluation/v2

     1  package evaluation
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
     7  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
     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  	m "github.com/launchdarkly/go-test-helpers/v3/matchers"
    15  
    16  	"github.com/stretchr/testify/assert"
    17  	"github.com/stretchr/testify/require"
    18  )
    19  
    20  var flagTestContext = ldcontext.New("x")
    21  
    22  var fallthroughValue = ldvalue.String("fall")
    23  var offValue = ldvalue.String("off")
    24  var onValue = ldvalue.String("on")
    25  
    26  func TestFlagReturnsOffVariationIfFlagIsOff(t *testing.T) {
    27  	f := ldbuilders.NewFlagBuilder("feature").
    28  		On(false).
    29  		OffVariation(1).
    30  		FallthroughVariation(0).
    31  		Variations(fallthroughValue, offValue, onValue).
    32  		Build()
    33  
    34  	result := basicEvaluator().Evaluate(&f, flagTestContext, FailOnAnyPrereqEvent(t))
    35  	m.In(t).Assert(result, ResultDetailProps(1, offValue, ldreason.NewEvalReasonOff()))
    36  	assert.False(t, result.IsExperiment)
    37  }
    38  
    39  func TestFlagReturnsNilIfFlagIsOffAndOffVariationIsUnspecified(t *testing.T) {
    40  	f := ldbuilders.NewFlagBuilder("feature").
    41  		On(false).
    42  		FallthroughVariation(0).
    43  		Variations(fallthroughValue, offValue, onValue).
    44  		Build()
    45  
    46  	result := basicEvaluator().Evaluate(&f, flagTestContext, FailOnAnyPrereqEvent(t))
    47  	m.In(t).Assert(result.Detail, EvalDetailEquals(ldreason.EvaluationDetail{Reason: ldreason.NewEvalReasonOff()}))
    48  	assert.False(t, result.IsExperiment)
    49  }
    50  
    51  func TestFlagReturnsFallthroughIfFlagIsOnAndThereAreNoRules(t *testing.T) {
    52  	f := ldbuilders.NewFlagBuilder("feature").
    53  		On(true).
    54  		FallthroughVariation(0).
    55  		Variations(fallthroughValue, offValue, onValue).
    56  		Build()
    57  
    58  	result := basicEvaluator().Evaluate(&f, flagTestContext, FailOnAnyPrereqEvent(t))
    59  	m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonFallthrough()))
    60  	assert.False(t, result.IsExperiment)
    61  }
    62  
    63  func TestFlagMatchesContextFromRules(t *testing.T) {
    64  	f := makeFlagToMatchContext(flagTestContext, ldbuilders.Variation(2))
    65  
    66  	result := basicEvaluator().Evaluate(&f, flagTestContext, FailOnAnyPrereqEvent(t))
    67  	m.In(t).Assert(result, ResultDetailProps(2, onValue, ldreason.NewEvalReasonRuleMatch(0, "rule-id")))
    68  	assert.False(t, result.IsExperiment)
    69  }
    70  
    71  func TestFlagReturnsWhetherContextWasInFallthroughExperiment(t *testing.T) {
    72  	// seed here carefully chosen so users fall into different buckets
    73  	user1, user2, user3 := ldcontext.New("userKeyA"), ldcontext.New("userKeyB"), ldcontext.New("userKeyC")
    74  
    75  	f := ldbuilders.NewFlagBuilder("experiment").
    76  		On(true).
    77  		Fallthrough(ldbuilders.Experiment(
    78  			ldvalue.NewOptionalInt(61),
    79  			ldbuilders.Bucket(0, 10000),
    80  			ldbuilders.Bucket(2, 20000),
    81  			ldbuilders.BucketUntracked(0, 70000),
    82  		)).
    83  		Variations(fallthroughValue, offValue, onValue).
    84  		Build()
    85  
    86  	result := basicEvaluator().Evaluate(&f, user1, nil)
    87  	// bucketVal = 0.09801207
    88  	m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonFallthroughExperiment(true)))
    89  	assert.True(t, result.IsExperiment)
    90  
    91  	result = basicEvaluator().Evaluate(&f, user2, nil)
    92  	// bucketVal = 0.14483777
    93  	m.In(t).Assert(result, ResultDetailProps(2, onValue, ldreason.NewEvalReasonFallthroughExperiment(true)))
    94  	assert.True(t, result.IsExperiment)
    95  
    96  	result = basicEvaluator().Evaluate(&f, user3, nil)
    97  	// bucketVal = 0.9242641
    98  	m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonFallthrough()))
    99  	assert.False(t, result.IsExperiment)
   100  }
   101  
   102  func TestFlagReturnsWhetherContextWasInRuleExperiment(t *testing.T) {
   103  	// seed here carefully chosen so users fall into different buckets
   104  	user1, user2, user3 := ldcontext.New("userKeyA"), ldcontext.New("userKeyB"), ldcontext.New("userKeyC")
   105  
   106  	f := ldbuilders.NewFlagBuilder("experiment").
   107  		On(true).
   108  		AddRule(makeRuleToMatchUserKeyPrefix("user", ldbuilders.Experiment(
   109  			ldvalue.NewOptionalInt(61),
   110  			ldbuilders.Bucket(0, 10000),
   111  			ldbuilders.Bucket(2, 20000),
   112  			ldbuilders.BucketUntracked(0, 70000),
   113  		))).
   114  		Variations(fallthroughValue, offValue, onValue).
   115  		Build()
   116  
   117  	result := basicEvaluator().Evaluate(&f, user1, nil)
   118  	// bucketVal = 0.09801207
   119  	m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonRuleMatchExperiment(0, "rule-id", true)))
   120  	assert.True(t, result.IsExperiment)
   121  
   122  	result = basicEvaluator().Evaluate(&f, user2, nil)
   123  	// bucketVal = 0.14483777
   124  	m.In(t).Assert(result, ResultDetailProps(2, onValue, ldreason.NewEvalReasonRuleMatchExperiment(0, "rule-id", true)))
   125  	assert.True(t, result.IsExperiment)
   126  
   127  	result = basicEvaluator().Evaluate(&f, user3, nil)
   128  	// bucketVal = 0.9242641
   129  	m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonRuleMatch(0, "rule-id")))
   130  	assert.False(t, result.IsExperiment)
   131  }
   132  
   133  func TestMalformedFlagErrorForBadFlagProperties(t *testing.T) {
   134  	basicContext := ldcontext.New("userkey")
   135  
   136  	type testCaseParams struct {
   137  		name    string
   138  		context ldcontext.Context
   139  		flag    ldmodel.FeatureFlag
   140  		message string
   141  	}
   142  
   143  	for _, p := range []testCaseParams{
   144  		{
   145  			name:    "fallthrough with variation index too high",
   146  			context: basicContext,
   147  			flag: ldbuilders.NewFlagBuilder("feature").
   148  				On(true).
   149  				FallthroughVariation(999).
   150  				Variations(fallthroughValue, offValue, onValue).
   151  				Build(),
   152  			message: "nonexistent variation index 999",
   153  		},
   154  		{
   155  			name:    "fallthrough with negative variation index",
   156  			context: basicContext,
   157  			flag: ldbuilders.NewFlagBuilder("feature").
   158  				On(true).
   159  				FallthroughVariation(-1).
   160  				Variations(fallthroughValue, offValue, onValue).
   161  				Build(),
   162  			message: "nonexistent variation index -1",
   163  		},
   164  		{
   165  			name:    "fallthrough with neither variation nor rollout",
   166  			context: basicContext,
   167  			flag: ldbuilders.NewFlagBuilder("feature").
   168  				On(true).
   169  				Variations(fallthroughValue, offValue, onValue).
   170  				Build(),
   171  			message: "rollout or experiment with no variations",
   172  		},
   173  		{
   174  			name:    "fallthrough with empty rollout",
   175  			context: basicContext,
   176  			flag: ldbuilders.NewFlagBuilder("feature").
   177  				On(true).
   178  				Fallthrough(ldbuilders.Rollout()).
   179  				Variations(fallthroughValue, offValue, onValue).
   180  				Build(),
   181  			message: "rollout or experiment with no variations",
   182  		},
   183  	} {
   184  		t.Run(p.name, func(t *testing.T) {
   185  			t.Run("returns error", func(t *testing.T) {
   186  				result := basicEvaluator().Evaluate(&p.flag, p.context, FailOnAnyPrereqEvent(t))
   187  				m.In(t).Assert(result, ResultDetailError(ldreason.EvalErrorMalformedFlag))
   188  			})
   189  
   190  			t.Run("logs error", func(t *testing.T) {
   191  				logCapture := ldlogtest.NewMockLog()
   192  				e := NewEvaluatorWithOptions(basicDataProvider(),
   193  					EvaluatorOptionErrorLogger(logCapture.Loggers.ForLevel(ldlog.Error)))
   194  				_ = e.Evaluate(&p.flag, p.context, FailOnAnyPrereqEvent(t))
   195  
   196  				errorLines := logCapture.GetOutput(ldlog.Error)
   197  				if assert.Len(t, errorLines, 1) {
   198  					assert.Regexp(t, p.message, errorLines[0])
   199  				}
   200  			})
   201  		})
   202  	}
   203  }
   204  
   205  func TestUserNotSpecifiedErrorForInvalidContext(t *testing.T) {
   206  	badContext := ldcontext.New("")
   207  	require.Error(t, badContext.Err())
   208  
   209  	f := ldbuilders.NewFlagBuilder("feature").
   210  		On(false).
   211  		OffVariation(1).
   212  		FallthroughVariation(0).
   213  		Variations(fallthroughValue, offValue, onValue).
   214  		Build()
   215  
   216  	result := basicEvaluator().Evaluate(&f, badContext, FailOnAnyPrereqEvent(t))
   217  	assertResultDetail(t, ldreason.NewEvaluationDetailForError(ldreason.EvalErrorUserNotSpecified, ldvalue.Null()), result)
   218  }
   219  

View as plain text