package ldclient import ( "encoding/json" "errors" "testing" "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest/mocks" "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/lduser" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" ldevents "github.com/launchdarkly/go-sdk-events/v2" "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders" "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel" "github.com/launchdarkly/go-server-sdk/v6/internal/datakinds" "github.com/launchdarkly/go-server-sdk/v6/internal/datastore" "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest" "github.com/launchdarkly/go-server-sdk/v6/ldcomponents" "github.com/launchdarkly/go-server-sdk/v6/subsystems" "github.com/launchdarkly/go-server-sdk/v6/subsystems/ldstoretypes" "github.com/launchdarkly/go-server-sdk/v6/testhelpers/ldtestdata" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( evalFlagKey = "flag-key" expectedVariationForSingleValueFlag = 2 expectedFlagVersion = 1 ) var evalTestUser = lduser.NewUser("userkey") var fallthroughValue = ldvalue.String("fall") var offValue = ldvalue.String("off") var onValue = ldvalue.String("on") var expectedReasonForSingleValueFlag = ldreason.NewEvalReasonFallthrough() var noReason = ldreason.EvaluationReason{} func makeClauseToMatchUser(user ldcontext.Context) ldmodel.Clause { return ldbuilders.Clause("key", ldmodel.OperatorIn, ldvalue.String(user.Key())) } func makeClauseToNotMatchUser(user ldcontext.Context) ldmodel.Clause { return ldbuilders.Clause("key", ldmodel.OperatorIn, ldvalue.String("not-"+user.Key())) } type clientEvalTestParams struct { client *LDClient store subsystems.DataStore data *ldtestdata.TestDataSource events *mocks.CapturingEventProcessor mockLog *ldlogtest.MockLog } func (p clientEvalTestParams) setupSingleValueFlag(key string, value ldvalue.Value) { values := []ldvalue.Value{} for i := 0; i < expectedVariationForSingleValueFlag; i++ { // We add some unused variations so that the result variation index won't be zero, since it's always // hard to tell if a zero is an intentional result or just an uninitialized variable. values = append(values, ldvalue.String("should not get this value")) } values = append(values, value) p.data.Update(p.data.Flag(key).On(true). FallthroughVariationIndex(expectedVariationForSingleValueFlag). Variations(values...)) } func withClientEvalTestParams(callback func(clientEvalTestParams)) { p := clientEvalTestParams{} p.store = datastore.NewInMemoryDataStore(ldlog.NewDisabledLoggers()) p.data = ldtestdata.DataSource() p.events = &mocks.CapturingEventProcessor{} p.mockLog = ldlogtest.NewMockLog() config := Config{ Offline: false, DataStore: mocks.SingleComponentConfigurer[subsystems.DataStore]{Instance: p.store}, DataSource: p.data, Events: mocks.SingleComponentConfigurer[ldevents.EventProcessor]{Instance: p.events}, Logging: ldcomponents.Logging().Loggers(p.mockLog.Loggers), } p.client, _ = MakeCustomClient("sdk_key", config, 0) defer p.client.Close() callback(p) } func (p clientEvalTestParams) requireSingleEvent(t *testing.T) ldevents.EvaluationData { events := p.events.Events require.Equal(t, 1, len(events)) return events[0].(ldevents.EvaluationData) } func (p clientEvalTestParams) expectSingleEvaluationEvent( t *testing.T, flagKey string, value ldvalue.Value, defaultVal ldvalue.Value, reason ldreason.EvaluationReason, ) { assertEvalEvent(t, p.requireSingleEvent(t), flagKey, expectedFlagVersion, evalTestUser, value, expectedVariationForSingleValueFlag, defaultVal, reason) } func assertEvalEvent( t *testing.T, actualEvent ldevents.EvaluationData, flagKey string, flagVersion int, user ldcontext.Context, value ldvalue.Value, variation int, defaultVal ldvalue.Value, reason ldreason.EvaluationReason, ) { expectedEvent := ldevents.EvaluationData{ BaseEvent: ldevents.BaseEvent{ CreationDate: actualEvent.CreationDate, Context: ldevents.Context(user), }, Key: flagKey, Version: ldvalue.NewOptionalInt(flagVersion), Value: value, Variation: ldvalue.NewOptionalInt(variation), Default: defaultVal, Reason: reason, } assert.Equal(t, expectedEvent, actualEvent) } func TestBoolVariation(t *testing.T) { expected, defaultVal := true, false t.Run("simple", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.Bool(true)) actual, err := p.client.BoolVariation(evalFlagKey, evalTestUser, defaultVal) assert.NoError(t, err) assert.Equal(t, expected, actual) p.expectSingleEvaluationEvent(t, evalFlagKey, ldvalue.Bool(expected), ldvalue.Bool(defaultVal), noReason) }) }) t.Run("detail", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.Bool(true)) actual, detail, err := p.client.BoolVariationDetail(evalFlagKey, evalTestUser, defaultVal) assert.NoError(t, err) assert.Equal(t, expected, actual) assert.Equal(t, ldreason.NewEvaluationDetail(ldvalue.Bool(expected), expectedVariationForSingleValueFlag, expectedReasonForSingleValueFlag), detail) p.expectSingleEvaluationEvent(t, evalFlagKey, ldvalue.Bool(expected), ldvalue.Bool(defaultVal), detail.Reason) }) }) } func TestIntVariation(t *testing.T) { expected, defaultVal := 100, 10000 t.Run("simple", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.Int(expected)) actual, err := p.client.IntVariation(evalFlagKey, evalTestUser, defaultVal) assert.NoError(t, err) assert.Equal(t, expected, actual) p.expectSingleEvaluationEvent(t, evalFlagKey, ldvalue.Int(expected), ldvalue.Int(defaultVal), noReason) }) }) t.Run("detail", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.Int(expected)) actual, detail, err := p.client.IntVariationDetail(evalFlagKey, evalTestUser, defaultVal) assert.NoError(t, err) assert.Equal(t, expected, actual) assert.Equal(t, ldreason.NewEvaluationDetail(ldvalue.Int(expected), expectedVariationForSingleValueFlag, expectedReasonForSingleValueFlag), detail) p.expectSingleEvaluationEvent(t, evalFlagKey, ldvalue.Int(expected), ldvalue.Int(defaultVal), detail.Reason) }) }) t.Run("rounds float toward zero", func(t *testing.T) { flag1Key, flag2Key, flag3Key, flag4Key := "flag1", "flag2", "flag3", "flag4" withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(flag1Key, ldvalue.Float64(2.25)) p.setupSingleValueFlag(flag2Key, ldvalue.Float64(2.75)) p.setupSingleValueFlag(flag3Key, ldvalue.Float64(-2.25)) p.setupSingleValueFlag(flag4Key, ldvalue.Float64(-2.75)) actual, err := p.client.IntVariation(flag1Key, evalTestUser, 0) assert.NoError(t, err) assert.Equal(t, 2, actual) actual, err = p.client.IntVariation(flag2Key, evalTestUser, 0) assert.NoError(t, err) assert.Equal(t, 2, actual) actual, err = p.client.IntVariation(flag3Key, evalTestUser, 0) assert.NoError(t, err) assert.Equal(t, -2, actual) actual, err = p.client.IntVariation(flag4Key, evalTestUser, 0) assert.NoError(t, err) assert.Equal(t, -2, actual) }) }) } func TestFloat64Variation(t *testing.T) { expected, defaultVal := 100.01, 0.0 t.Run("simple", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.Float64(expected)) actual, err := p.client.Float64Variation(evalFlagKey, evalTestUser, defaultVal) assert.NoError(t, err) assert.Equal(t, expected, actual) p.expectSingleEvaluationEvent(t, evalFlagKey, ldvalue.Float64(expected), ldvalue.Float64(defaultVal), noReason) }) }) t.Run("detail", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.Float64(expected)) actual, detail, err := p.client.Float64VariationDetail(evalFlagKey, evalTestUser, defaultVal) assert.NoError(t, err) assert.Equal(t, expected, actual) assert.Equal(t, ldreason.NewEvaluationDetail(ldvalue.Float64(expected), expectedVariationForSingleValueFlag, expectedReasonForSingleValueFlag), detail) p.expectSingleEvaluationEvent(t, evalFlagKey, ldvalue.Float64(expected), ldvalue.Float64(defaultVal), detail.Reason) }) }) } func TestStringVariation(t *testing.T) { expected, defaultVal := "b", "a" t.Run("simple", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.String(expected)) actual, err := p.client.StringVariation(evalFlagKey, evalTestUser, defaultVal) assert.NoError(t, err) assert.Equal(t, expected, actual) p.expectSingleEvaluationEvent(t, evalFlagKey, ldvalue.String(expected), ldvalue.String(defaultVal), noReason) }) }) t.Run("detail", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.String(expected)) actual, detail, err := p.client.StringVariationDetail(evalFlagKey, evalTestUser, defaultVal) assert.NoError(t, err) assert.Equal(t, expected, actual) assert.Equal(t, ldreason.NewEvaluationDetail(ldvalue.String(expected), expectedVariationForSingleValueFlag, expectedReasonForSingleValueFlag), detail) p.expectSingleEvaluationEvent(t, evalFlagKey, ldvalue.String(expected), ldvalue.String(defaultVal), detail.Reason) }) }) } func TestJSONRawVariation(t *testing.T) { expectedValue := map[string]interface{}{"field2": "value2"} expectedJSON, _ := json.Marshal(expectedValue) expectedRaw := json.RawMessage(expectedJSON) defaultVal := json.RawMessage([]byte(`{"default":"default"}`)) t.Run("simple", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.CopyArbitraryValue(expectedValue)) actual, err := p.client.JSONVariation(evalFlagKey, evalTestUser, ldvalue.Raw(defaultVal)) assert.NoError(t, err) assert.Equal(t, expectedRaw, actual.AsRaw()) p.expectSingleEvaluationEvent(t, evalFlagKey, ldvalue.CopyArbitraryValue(expectedValue), ldvalue.CopyArbitraryValue(defaultVal), noReason) }) }) t.Run("detail", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.CopyArbitraryValue(expectedValue)) actual, detail, err := p.client.JSONVariationDetail(evalFlagKey, evalTestUser, ldvalue.Raw(defaultVal)) assert.NoError(t, err) assert.Equal(t, expectedRaw, actual.AsRaw()) assert.Equal(t, ldreason.NewEvaluationDetail(ldvalue.Parse(expectedRaw), expectedVariationForSingleValueFlag, expectedReasonForSingleValueFlag), detail) p.expectSingleEvaluationEvent(t, evalFlagKey, ldvalue.CopyArbitraryValue(expectedValue), ldvalue.CopyArbitraryValue(defaultVal), detail.Reason) }) }) } func TestJSONVariation(t *testing.T) { expected := ldvalue.CopyArbitraryValue(map[string]interface{}{"field2": "value2"}) defaultVal := ldvalue.String("no") t.Run("simple", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, expected) actual, err := p.client.JSONVariation(evalFlagKey, evalTestUser, defaultVal) assert.NoError(t, err) assert.Equal(t, expected, actual) p.expectSingleEvaluationEvent(t, evalFlagKey, expected, defaultVal, noReason) }) }) t.Run("detail", func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, expected) actual, detail, err := p.client.JSONVariationDetail(evalFlagKey, evalTestUser, defaultVal) assert.NoError(t, err) assert.Equal(t, expected, actual) assert.Equal(t, ldreason.NewEvaluationDetail(expected, expectedVariationForSingleValueFlag, expectedReasonForSingleValueFlag), detail) p.expectSingleEvaluationEvent(t, evalFlagKey, expected, defaultVal, detail.Reason) }) }) } func TestEvaluatingUnknownFlagReturnsDefault(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { value, err := p.client.StringVariation("no-such-flag", evalTestUser, "default") assert.Error(t, err) assert.Equal(t, "default", value) }) } func TestEvaluatingUnknownFlagReturnsDefaultWithDetail(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { _, detail, err := p.client.StringVariationDetail("no-such-flag", evalTestUser, "default") assert.Error(t, err) assert.Equal(t, ldvalue.String("default"), detail.Value) assert.Equal(t, ldvalue.OptionalInt{}, detail.VariationIndex) assert.Equal(t, ldreason.NewEvalReasonError(ldreason.EvalErrorFlagNotFound), detail.Reason) assert.True(t, detail.IsDefaultValue()) }) } func TestDefaultIsReturnedIfFlagEvaluatesToNil(t *testing.T) { flag := ldbuilders.NewFlagBuilder(evalFlagKey).Build() // flag is off and we haven't defined an off variation withClientEvalTestParams(func(p clientEvalTestParams) { p.data.UsePreconfiguredFlag(flag) value, err := p.client.StringVariation(evalFlagKey, evalTestUser, "default") assert.NoError(t, err) assert.Equal(t, "default", value) }) } func TestDefaultIsReturnedIfFlagEvaluatesToNilWithDetail(t *testing.T) { flag := ldbuilders.NewFlagBuilder(evalFlagKey).Build() // flag is off and we haven't defined an off variation withClientEvalTestParams(func(p clientEvalTestParams) { p.data.UsePreconfiguredFlag(flag) _, detail, err := p.client.StringVariationDetail(evalFlagKey, evalTestUser, "default") assert.NoError(t, err) assert.Equal(t, ldvalue.String("default"), detail.Value) assert.Equal(t, ldvalue.OptionalInt{}, detail.VariationIndex) assert.Equal(t, ldreason.NewEvalReasonOff(), detail.Reason) }) } func TestDefaultIsReturnedIfFlagReturnsWrongType(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.setupSingleValueFlag(evalFlagKey, ldvalue.ArrayOf()) v1a, err1a := p.client.BoolVariation(evalFlagKey, evalTestUser, false) v1b, detail1, err1b := p.client.BoolVariationDetail(evalFlagKey, evalTestUser, false) assert.NoError(t, err1a) assert.NoError(t, err1b) assert.False(t, v1a) assert.False(t, v1b) assert.Equal(t, ldreason.EvalErrorWrongType, detail1.Reason.GetErrorKind()) v2a, err2a := p.client.IntVariation(evalFlagKey, evalTestUser, -1) v2b, detail2, err2b := p.client.IntVariationDetail(evalFlagKey, evalTestUser, -1) assert.NoError(t, err2a) assert.NoError(t, err2b) assert.Equal(t, -1, v2a) assert.Equal(t, -1, v2b) assert.Equal(t, ldreason.EvalErrorWrongType, detail2.Reason.GetErrorKind()) v3a, err3a := p.client.Float64Variation(evalFlagKey, evalTestUser, -1) v3b, detail3, err3b := p.client.Float64VariationDetail(evalFlagKey, evalTestUser, -1) assert.NoError(t, err3a) assert.NoError(t, err3b) assert.Equal(t, float64(-1), v3a) assert.Equal(t, float64(-1), v3b) assert.Equal(t, ldreason.EvalErrorWrongType, detail3.Reason.GetErrorKind()) v4a, err4a := p.client.StringVariation(evalFlagKey, evalTestUser, "x") v4b, detail4, err4b := p.client.StringVariationDetail(evalFlagKey, evalTestUser, "x") assert.NoError(t, err4a) assert.NoError(t, err4b) assert.Equal(t, "x", v4a) assert.Equal(t, "x", v4b) assert.Equal(t, ldreason.EvalErrorWrongType, detail4.Reason.GetErrorKind()) }) } func TestEvaluateWithInvalidContext(t *testing.T) { flagKey := "flag" for _, contextParams := range []struct { name string context ldcontext.Context errorText string }{ {"empty key", ldcontext.New(""), "context key must not be empty"}, {"invalid kind", ldcontext.NewWithKind("!bad!", "key"), "context kind contains disallowed characters"}, } { t.Run(contextParams.name, func(t *testing.T) { c := contextParams.context for _, evalFnParams := range []struct { name string fn func(*LDClient) error }{ {"BoolVariation", func(client *LDClient) error { _, err := client.BoolVariation(flagKey, c, false); return err }}, {"IntVariation", func(client *LDClient) error { _, err := client.IntVariation(flagKey, c, 0); return err }}, {"Float64Variation", func(client *LDClient) error { _, err := client.Float64Variation(flagKey, c, 0); return err }}, {"StringVariation", func(client *LDClient) error { _, err := client.StringVariation(flagKey, c, ""); return err }}, {"JSONVariation", func(client *LDClient) error { _, err := client.JSONVariation(flagKey, c, ldvalue.Null()); return err }}, } { t.Run(evalFnParams.name, func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { err := evalFnParams.fn(p.client) assert.Error(t, err) p.mockLog.AssertMessageMatch(t, true, ldlog.Warn, contextParams.errorText) }) }) } for _, evalFnParams := range []struct { name string fn func(*LDClient) (ldreason.EvaluationDetail, error) }{ {"BoolVariationDetail", func(client *LDClient) (ldreason.EvaluationDetail, error) { _, detail, err := client.BoolVariationDetail(flagKey, c, false) return detail, err }}, {"IntVariationDetail", func(client *LDClient) (ldreason.EvaluationDetail, error) { _, detail, err := client.IntVariationDetail(flagKey, c, 0) return detail, err }}, {"Float64VariationDetail", func(client *LDClient) (ldreason.EvaluationDetail, error) { _, detail, err := client.Float64VariationDetail(flagKey, c, 0) return detail, err }}, {"StringVariationDetail", func(client *LDClient) (ldreason.EvaluationDetail, error) { _, detail, err := client.StringVariationDetail(flagKey, c, "") return detail, err }}, {"JSONVariationDetail", func(client *LDClient) (ldreason.EvaluationDetail, error) { _, detail, err := client.JSONVariationDetail(flagKey, c, ldvalue.Null()) return detail, err }}, } { t.Run(evalFnParams.name, func(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { detail, err := evalFnParams.fn(p.client) assert.Error(t, err) assert.Equal(t, ldreason.NewEvalReasonError(ldreason.EvalErrorUserNotSpecified), detail.Reason) p.mockLog.AssertMessageMatch(t, true, ldlog.Warn, contextParams.errorText) }) }) } }) } } func TestEventTrackingAndReasonCanBeForcedForRule(t *testing.T) { flag := ldbuilders.NewFlagBuilder(evalFlagKey). On(true). AddRule(ldbuilders.NewRuleBuilder(). ID("rule-id"). Clauses(makeClauseToMatchUser(evalTestUser)). Variation(1). TrackEvents(true)). Variations(offValue, onValue). Version(1). Build() withClientEvalTestParams(func(p clientEvalTestParams) { p.data.UsePreconfiguredFlag(flag) value, err := p.client.StringVariation(evalFlagKey, evalTestUser, "default") assert.NoError(t, err) assert.Equal(t, "on", value) e := p.requireSingleEvent(t) assert.True(t, e.RequireFullEvent) assert.Equal(t, ldreason.NewEvalReasonRuleMatch(0, "rule-id"), e.Reason) }) } func TestEventTrackingAndReasonAreNotForcedIfFlagIsNotSetForMatchingRule(t *testing.T) { flag := ldbuilders.NewFlagBuilder(evalFlagKey). On(true). AddRule(ldbuilders.NewRuleBuilder(). ID("id0"). Clauses(makeClauseToNotMatchUser(evalTestUser)). Variation(0). TrackEvents(true)). AddRule(ldbuilders.NewRuleBuilder(). ID("id1"). Clauses(makeClauseToMatchUser(evalTestUser)). Variation(1)). Variations(offValue, onValue). Version(1). Build() withClientEvalTestParams(func(p clientEvalTestParams) { p.data.UsePreconfiguredFlag(flag) value, err := p.client.StringVariation(evalFlagKey, evalTestUser, "default") assert.NoError(t, err) assert.Equal(t, "on", value) e := p.requireSingleEvent(t) assert.False(t, e.RequireFullEvent) assert.Equal(t, ldreason.EvaluationReason{}, e.Reason) }) } func TestEventTrackingAndReasonCanBeForcedForFallthrough(t *testing.T) { flag := ldbuilders.NewFlagBuilder(evalFlagKey). On(true). FallthroughVariation(1). Variations(offValue, onValue). TrackEventsFallthrough(true). Version(1). Build() withClientEvalTestParams(func(p clientEvalTestParams) { p.data.UsePreconfiguredFlag(flag) value, err := p.client.StringVariation(evalFlagKey, evalTestUser, "default") assert.NoError(t, err) assert.Equal(t, "on", value) e := p.requireSingleEvent(t) assert.True(t, e.RequireFullEvent) assert.Equal(t, ldreason.NewEvalReasonFallthrough(), e.Reason) }) } func TestEventTrackingAndReasonAreNotForcedForFallthroughIfFlagIsNotSet(t *testing.T) { flag := ldbuilders.NewFlagBuilder(evalFlagKey). On(true). FallthroughVariation(1). Variations(offValue, onValue). Version(1). Build() withClientEvalTestParams(func(p clientEvalTestParams) { p.data.UsePreconfiguredFlag(flag) value, err := p.client.StringVariation(evalFlagKey, evalTestUser, "default") assert.NoError(t, err) assert.Equal(t, "on", value) e := p.requireSingleEvent(t) assert.False(t, e.RequireFullEvent) assert.Equal(t, ldreason.EvaluationReason{}, e.Reason) }) } func TestEventTrackingAndReasonAreNotForcedForFallthroughIfReasonIsNotFallthrough(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { p.data.Update(p.data.Flag(evalFlagKey).Variations(offValue, onValue).OffVariationIndex(0).On(false)) value, err := p.client.StringVariation(evalFlagKey, evalTestUser, "default") assert.NoError(t, err) assert.Equal(t, "off", value) e := p.requireSingleEvent(t) assert.False(t, e.RequireFullEvent) assert.Equal(t, ldreason.EvaluationReason{}, e.Reason) }) } func TestEvaluatingUnknownFlagSendsEvent(t *testing.T) { withClientEvalTestParams(func(p clientEvalTestParams) { _, err := p.client.StringVariation("no-such-flag", evalTestUser, "x") assert.Error(t, err) e := p.requireSingleEvent(t) expectedEvent := ldevents.EvaluationData{ BaseEvent: ldevents.BaseEvent{ CreationDate: e.CreationDate, Context: ldevents.Context(evalTestUser), }, Key: "no-such-flag", Value: ldvalue.String("x"), Default: ldvalue.String("x"), } assert.Equal(t, expectedEvent, e) }) } func TestEvaluatingFlagWithPrerequisiteSendsPrerequisiteEvent(t *testing.T) { flag0 := ldbuilders.NewFlagBuilder("flag0"). On(true). FallthroughVariation(1). Variations(ldvalue.String("a"), ldvalue.String("b")). AddPrerequisite("flag1", 1). Build() flag1 := ldbuilders.NewFlagBuilder("flag1"). On(true). FallthroughVariation(1). Variations(ldvalue.String("c"), ldvalue.String("d")). Build() withClientEvalTestParams(func(p clientEvalTestParams) { p.data.UsePreconfiguredFlag(flag0) p.data.UsePreconfiguredFlag(flag1) user := lduser.NewUser("userKey") _, err := p.client.StringVariation(flag0.Key, user, "x") assert.NoError(t, err) events := p.events.Events assert.Len(t, events, 2) e0 := events[0].(ldevents.EvaluationData) expected0 := ldevents.EvaluationData{ BaseEvent: ldevents.BaseEvent{ CreationDate: e0.CreationDate, Context: ldevents.Context(user), }, Key: flag1.Key, Version: ldvalue.NewOptionalInt(1), Value: ldvalue.String("d"), Variation: ldvalue.NewOptionalInt(1), Default: ldvalue.Null(), PrereqOf: ldvalue.NewOptionalString(flag0.Key), } assert.Equal(t, expected0, e0) e1 := events[1].(ldevents.EvaluationData) expected1 := ldevents.EvaluationData{ BaseEvent: ldevents.BaseEvent{ CreationDate: e1.CreationDate, Context: ldevents.Context(user), }, Key: flag0.Key, Version: ldvalue.NewOptionalInt(1), Value: ldvalue.String("b"), Variation: ldvalue.NewOptionalInt(1), Default: ldvalue.String("x"), } assert.Equal(t, expected1, e1) }) } func TestEvalErrorIfStoreReturnsError(t *testing.T) { myError := errors.New("sorry") store := mocks.NewCapturingDataStore(datastore.NewInMemoryDataStore(sharedtest.NewTestLoggers())) _ = store.Init(nil) store.SetFakeError(myError) client := makeTestClientWithConfig(func(c *Config) { c.DataStore = mocks.SingleComponentConfigurer[subsystems.DataStore]{Instance: store} }) defer client.Close() value, err := client.BoolVariation("flag", evalTestUser, false) assert.False(t, value) assert.Equal(t, myError, err) } func TestEvalErrorIfStoreHasNonFlagObject(t *testing.T) { key := "not-really-a-flag" notAFlag := 9 withClientEvalTestParams(func(p clientEvalTestParams) { p.store.Upsert(datakinds.Features, key, ldstoretypes.ItemDescriptor{Version: 1, Item: notAFlag}) value, err := p.client.BoolVariation(key, evalTestUser, false) assert.False(t, value) assert.Error(t, err) }) } func TestUnknownFlagErrorLogging(t *testing.T) { testEvalErrorLogging(t, nil, "unknown-flag", evalTestUser, "", "unknown feature key: unknown-flag\\. Verify that this feature key exists\\. Returning default value") } func TestMalformedFlagErrorLogging(t *testing.T) { flag := ldbuilders.NewFlagBuilder("bad-flag").On(false).OffVariation(99).Build() testEvalErrorLogging(t, &flag, "", evalTestUser, `Invalid flag configuration.*"bad-flag".*nonexistent variation index 99`, "Flag evaluation for bad-flag failed with error MALFORMED_FLAG, default value was returned") } func testEvalErrorLogging(t *testing.T, flag *ldmodel.FeatureFlag, key string, user ldcontext.Context, expectedErrorRegex, expectedWarningRegex string) { runTest := func(withLogging bool) { mockLoggers := ldlogtest.NewMockLog() testData := ldtestdata.DataSource() client := makeTestClientWithConfig(func(c *Config) { c.DataSource = testData c.Logging = ldcomponents.Logging().Loggers(mockLoggers.Loggers).MinLevel(ldlog.Warn).LogEvaluationErrors(withLogging) }) defer client.Close() if flag != nil { testData.UsePreconfiguredFlag(*flag) key = flag.Key } value, _ := client.StringVariation(key, user, "default") assert.Equal(t, "default", value) if expectedErrorRegex == "" { require.Len(t, mockLoggers.GetOutput(ldlog.Error), 0) } else { require.Len(t, mockLoggers.GetOutput(ldlog.Error), 1) assert.Regexp(t, expectedErrorRegex, mockLoggers.GetOutput(ldlog.Error)[0]) } if withLogging { require.Len(t, mockLoggers.GetOutput(ldlog.Warn), 1) assert.Regexp(t, expectedWarningRegex, mockLoggers.GetOutput(ldlog.Warn)[0]) } else { assert.Len(t, mockLoggers.GetOutput(ldlog.Warn), 0) } } runTest(false) runTest(true) } func TestEvalReturnsDefaultIfClientAndStoreAreNotInitialized(t *testing.T) { mockLoggers := ldlogtest.NewMockLog() client := makeTestClientWithConfig(func(c *Config) { c.DataSource = mocks.DataSourceThatNeverInitializes() c.Logging = ldcomponents.Logging().Loggers(mockLoggers.Loggers) }) defer client.Close() value, err := client.BoolVariation("flagkey", evalTestUser, false) require.Error(t, err) assert.Equal(t, "feature flag evaluation called before LaunchDarkly client initialization completed", err.Error()) assert.False(t, value) assert.Len(t, mockLoggers.GetOutput(ldlog.Warn), 0) } func TestEvalUsesStoreAndLogsWarningIfClientIsNotInitializedButStoreIsInitialized(t *testing.T) { mockLoggers := ldlogtest.NewMockLog() flag := ldbuilders.NewFlagBuilder(evalFlagKey).SingleVariation(ldvalue.Bool(true)).Build() store := datastore.NewInMemoryDataStore(sharedtest.NewTestLoggers()) _ = store.Init(nil) _, _ = store.Upsert(datakinds.Features, flag.Key, sharedtest.FlagDescriptor(flag)) client := makeTestClientWithConfig(func(c *Config) { c.DataSource = mocks.DataSourceThatNeverInitializes() c.DataStore = mocks.SingleComponentConfigurer[subsystems.DataStore]{Instance: store} c.Logging = ldcomponents.Logging().Loggers(mockLoggers.Loggers) }) defer client.Close() value, err := client.BoolVariation(flag.Key, evalTestUser, false) assert.NoError(t, err) assert.True(t, value) assert.Len(t, mockLoggers.GetOutput(ldlog.Warn), 1) assert.Contains(t, mockLoggers.GetOutput(ldlog.Warn)[0], "using last known values") }