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
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
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
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
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
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
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
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
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