package evaluation import ( "testing" "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders" "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldlog" "github.com/launchdarkly/go-sdk-common/v3/ldlogtest" "github.com/launchdarkly/go-sdk-common/v3/ldreason" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" m "github.com/launchdarkly/go-test-helpers/v3/matchers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var flagTestContext = ldcontext.New("x") var fallthroughValue = ldvalue.String("fall") var offValue = ldvalue.String("off") var onValue = ldvalue.String("on") func TestFlagReturnsOffVariationIfFlagIsOff(t *testing.T) { f := ldbuilders.NewFlagBuilder("feature"). On(false). OffVariation(1). FallthroughVariation(0). Variations(fallthroughValue, offValue, onValue). Build() result := basicEvaluator().Evaluate(&f, flagTestContext, FailOnAnyPrereqEvent(t)) m.In(t).Assert(result, ResultDetailProps(1, offValue, ldreason.NewEvalReasonOff())) assert.False(t, result.IsExperiment) } func TestFlagReturnsNilIfFlagIsOffAndOffVariationIsUnspecified(t *testing.T) { f := ldbuilders.NewFlagBuilder("feature"). On(false). FallthroughVariation(0). Variations(fallthroughValue, offValue, onValue). Build() result := basicEvaluator().Evaluate(&f, flagTestContext, FailOnAnyPrereqEvent(t)) m.In(t).Assert(result.Detail, EvalDetailEquals(ldreason.EvaluationDetail{Reason: ldreason.NewEvalReasonOff()})) assert.False(t, result.IsExperiment) } func TestFlagReturnsFallthroughIfFlagIsOnAndThereAreNoRules(t *testing.T) { f := ldbuilders.NewFlagBuilder("feature"). On(true). FallthroughVariation(0). Variations(fallthroughValue, offValue, onValue). Build() result := basicEvaluator().Evaluate(&f, flagTestContext, FailOnAnyPrereqEvent(t)) m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonFallthrough())) assert.False(t, result.IsExperiment) } func TestFlagMatchesContextFromRules(t *testing.T) { f := makeFlagToMatchContext(flagTestContext, ldbuilders.Variation(2)) result := basicEvaluator().Evaluate(&f, flagTestContext, FailOnAnyPrereqEvent(t)) m.In(t).Assert(result, ResultDetailProps(2, onValue, ldreason.NewEvalReasonRuleMatch(0, "rule-id"))) assert.False(t, result.IsExperiment) } func TestFlagReturnsWhetherContextWasInFallthroughExperiment(t *testing.T) { // seed here carefully chosen so users fall into different buckets user1, user2, user3 := ldcontext.New("userKeyA"), ldcontext.New("userKeyB"), ldcontext.New("userKeyC") f := ldbuilders.NewFlagBuilder("experiment"). On(true). Fallthrough(ldbuilders.Experiment( ldvalue.NewOptionalInt(61), ldbuilders.Bucket(0, 10000), ldbuilders.Bucket(2, 20000), ldbuilders.BucketUntracked(0, 70000), )). Variations(fallthroughValue, offValue, onValue). Build() result := basicEvaluator().Evaluate(&f, user1, nil) // bucketVal = 0.09801207 m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonFallthroughExperiment(true))) assert.True(t, result.IsExperiment) result = basicEvaluator().Evaluate(&f, user2, nil) // bucketVal = 0.14483777 m.In(t).Assert(result, ResultDetailProps(2, onValue, ldreason.NewEvalReasonFallthroughExperiment(true))) assert.True(t, result.IsExperiment) result = basicEvaluator().Evaluate(&f, user3, nil) // bucketVal = 0.9242641 m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonFallthrough())) assert.False(t, result.IsExperiment) } func TestFlagReturnsWhetherContextWasInRuleExperiment(t *testing.T) { // seed here carefully chosen so users fall into different buckets user1, user2, user3 := ldcontext.New("userKeyA"), ldcontext.New("userKeyB"), ldcontext.New("userKeyC") f := ldbuilders.NewFlagBuilder("experiment"). On(true). AddRule(makeRuleToMatchUserKeyPrefix("user", ldbuilders.Experiment( ldvalue.NewOptionalInt(61), ldbuilders.Bucket(0, 10000), ldbuilders.Bucket(2, 20000), ldbuilders.BucketUntracked(0, 70000), ))). Variations(fallthroughValue, offValue, onValue). Build() result := basicEvaluator().Evaluate(&f, user1, nil) // bucketVal = 0.09801207 m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonRuleMatchExperiment(0, "rule-id", true))) assert.True(t, result.IsExperiment) result = basicEvaluator().Evaluate(&f, user2, nil) // bucketVal = 0.14483777 m.In(t).Assert(result, ResultDetailProps(2, onValue, ldreason.NewEvalReasonRuleMatchExperiment(0, "rule-id", true))) assert.True(t, result.IsExperiment) result = basicEvaluator().Evaluate(&f, user3, nil) // bucketVal = 0.9242641 m.In(t).Assert(result, ResultDetailProps(0, fallthroughValue, ldreason.NewEvalReasonRuleMatch(0, "rule-id"))) assert.False(t, result.IsExperiment) } func TestMalformedFlagErrorForBadFlagProperties(t *testing.T) { basicContext := ldcontext.New("userkey") type testCaseParams struct { name string context ldcontext.Context flag ldmodel.FeatureFlag message string } for _, p := range []testCaseParams{ { name: "fallthrough with variation index too high", context: basicContext, flag: ldbuilders.NewFlagBuilder("feature"). On(true). FallthroughVariation(999). Variations(fallthroughValue, offValue, onValue). Build(), message: "nonexistent variation index 999", }, { name: "fallthrough with negative variation index", context: basicContext, flag: ldbuilders.NewFlagBuilder("feature"). On(true). FallthroughVariation(-1). Variations(fallthroughValue, offValue, onValue). Build(), message: "nonexistent variation index -1", }, { name: "fallthrough with neither variation nor rollout", context: basicContext, flag: ldbuilders.NewFlagBuilder("feature"). On(true). Variations(fallthroughValue, offValue, onValue). Build(), message: "rollout or experiment with no variations", }, { name: "fallthrough with empty rollout", context: basicContext, flag: ldbuilders.NewFlagBuilder("feature"). On(true). Fallthrough(ldbuilders.Rollout()). Variations(fallthroughValue, offValue, onValue). Build(), message: "rollout or experiment with no variations", }, } { t.Run(p.name, func(t *testing.T) { t.Run("returns error", func(t *testing.T) { result := basicEvaluator().Evaluate(&p.flag, p.context, FailOnAnyPrereqEvent(t)) m.In(t).Assert(result, ResultDetailError(ldreason.EvalErrorMalformedFlag)) }) t.Run("logs error", func(t *testing.T) { logCapture := ldlogtest.NewMockLog() e := NewEvaluatorWithOptions(basicDataProvider(), EvaluatorOptionErrorLogger(logCapture.Loggers.ForLevel(ldlog.Error))) _ = e.Evaluate(&p.flag, p.context, FailOnAnyPrereqEvent(t)) errorLines := logCapture.GetOutput(ldlog.Error) if assert.Len(t, errorLines, 1) { assert.Regexp(t, p.message, errorLines[0]) } }) }) } } func TestUserNotSpecifiedErrorForInvalidContext(t *testing.T) { badContext := ldcontext.New("") require.Error(t, badContext.Err()) f := ldbuilders.NewFlagBuilder("feature"). On(false). OffVariation(1). FallthroughVariation(0). Variations(fallthroughValue, offValue, onValue). Build() result := basicEvaluator().Evaluate(&f, badContext, FailOnAnyPrereqEvent(t)) assertResultDetail(t, ldreason.NewEvaluationDetailForError(ldreason.EvalErrorUserNotSpecified, ldvalue.Null()), result) }