1 package evaluation
2
3 import (
4 "fmt"
5 "testing"
6
7 "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders"
8 "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
9
10 "github.com/launchdarkly/go-sdk-common/v3/ldattr"
11 "github.com/launchdarkly/go-sdk-common/v3/ldcontext"
12 "github.com/launchdarkly/go-sdk-common/v3/ldlog"
13 "github.com/launchdarkly/go-sdk-common/v3/ldlogtest"
14 "github.com/launchdarkly/go-sdk-common/v3/ldreason"
15 "github.com/launchdarkly/go-sdk-common/v3/ldvalue"
16 m "github.com/launchdarkly/go-test-helpers/v3/matchers"
17
18 "github.com/stretchr/testify/assert"
19 "github.com/stretchr/testify/require"
20 )
21
22 func assertSegmentMatch(t *testing.T, segment ldmodel.Segment, context ldcontext.Context, expected bool) {
23 f := makeBooleanFlagToMatchAnyOfSegments(segment.Key)
24 evaluator := NewEvaluator(basicDataProvider().withStoredSegments(segment))
25 result := evaluator.Evaluate(&f, context, nil)
26 assert.Equal(t, expected, result.Detail.Value.BoolValue())
27 }
28
29 type segmentMatchParams struct {
30 name string
31 segment ldmodel.Segment
32 context ldcontext.Context
33 shouldMatch bool
34 }
35
36 func buildSegment() *ldbuilders.SegmentBuilder { return ldbuilders.NewSegmentBuilder("segmentkey") }
37
38 func doSegmentMatchTest(t *testing.T, p segmentMatchParams) {
39 desc := "should not match"
40 if p.shouldMatch {
41 desc = "should match"
42 }
43 t.Run(fmt.Sprintf("%s, %s", p.name, desc), func(t *testing.T) {
44 assertSegmentMatch(t, p.segment, p.context, p.shouldMatch)
45 })
46 }
47
48 func TestSegmentMatch(t *testing.T) {
49 userKey, otherKey := "key1", "key2"
50 otherKind := ldcontext.Kind("kind2")
51
52 defaultKindParams := []segmentMatchParams{
53 {
54 name: "neither included nor excluded, no rules",
55 segment: buildSegment().Build(),
56 shouldMatch: false,
57 },
58 {
59 name: "included by key",
60 segment: buildSegment().Included(otherKey, userKey).Build(),
61 shouldMatch: true,
62 },
63 {
64 name: "included by key and also excluded",
65 segment: buildSegment().Included(userKey).Excluded(userKey).Build(),
66 shouldMatch: true,
67 },
68 {
69 name: "includedContexts for other kinds do not apply",
70 segment: buildSegment().IncludedContextKind(otherKind, userKey).Build(),
71 shouldMatch: false,
72 },
73 {
74 name: "neither included nor excluded, rule match",
75 segment: buildSegment().
76 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(
77 ldbuilders.Clause(ldattr.KeyAttr, ldmodel.OperatorIn, ldvalue.String(userKey)),
78 )).
79 Build(),
80 shouldMatch: true,
81 },
82 {
83 name: "excluded, so rules are ignored",
84 segment: buildSegment().
85 Excluded(userKey).
86 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(
87 ldbuilders.Clause(ldattr.KeyAttr, ldmodel.OperatorIn, ldvalue.String(userKey)),
88 )).
89 Build(),
90 shouldMatch: false,
91 },
92 }
93
94 t.Run("single-kind context of default kind", func(t *testing.T) {
95 context := ldcontext.New(userKey)
96 for _, p := range defaultKindParams {
97 p1 := p
98 p1.context = context
99 doSegmentMatchTest(t, p1)
100 }
101 })
102
103 t.Run("multi-kind context, targeting default kind", func(t *testing.T) {
104 context := ldcontext.NewMulti(ldcontext.New(userKey), ldcontext.NewWithKind("kind2", "irrelevantKey"))
105 for _, p := range defaultKindParams {
106 p1 := p
107 p1.context = context
108 doSegmentMatchTest(t, p1)
109 }
110 })
111
112 t.Run("multi-kind context, targeting non-default kind", func(t *testing.T) {
113 for _, alsoHasDefault := range []bool{false, true} {
114 t.Run(fmt.Sprintf("also has default: %t", alsoHasDefault), func(t *testing.T) {
115 context := ldcontext.NewWithKind(otherKind, otherKey)
116 if alsoHasDefault {
117 context = ldcontext.NewMulti(ldcontext.New(userKey), context)
118 }
119 for _, p := range []segmentMatchParams{
120 {
121 name: "included by key",
122 segment: buildSegment().IncludedContextKind(otherKind, otherKey).Build(),
123 shouldMatch: true,
124 },
125 {
126 name: "default-kind included list is ignored for other kind",
127 segment: buildSegment().Included(otherKey).Build(),
128 shouldMatch: false,
129 },
130 {
131 name: "target list for nonexistent context does not match",
132 segment: buildSegment().IncludedContextKind("nonexistentKind", otherKey).Build(),
133 shouldMatch: false,
134 },
135 {
136 name: "included by key and also excluded",
137 segment: buildSegment().IncludedContextKind(otherKind, otherKey).ExcludedContextKind(otherKind, otherKey).Build(),
138 shouldMatch: true,
139 },
140 {
141 name: "neither included nor excluded, rule match",
142 segment: buildSegment().
143 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(
144 ldbuilders.ClauseWithKind(otherKind, ldattr.KeyAttr, ldmodel.OperatorIn, ldvalue.String(otherKey)),
145 )).
146 Build(),
147 shouldMatch: true,
148 },
149 {
150 name: "excluded, so rules are ignored",
151 segment: buildSegment().
152 ExcludedContextKind(otherKind, otherKey).
153 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(
154 ldbuilders.ClauseWithKind(otherKind, ldattr.KeyAttr, ldmodel.OperatorIn, ldvalue.String(otherKey)),
155 )).
156 Build(),
157 shouldMatch: false,
158 },
159 } {
160 p1 := p
161 p1.context = context
162 doSegmentMatchTest(t, p1)
163 }
164 })
165 }
166 })
167
168 t.Run("multi-kind context with only non-default kinds", func(t *testing.T) {
169 context := ldcontext.NewMulti(
170 ldcontext.NewWithKind(otherKind, otherKey),
171 ldcontext.NewWithKind("irrelevantKind", "irrelevantKey"),
172 )
173 for _, p := range []segmentMatchParams{
174 {
175 name: "included by key",
176 segment: buildSegment().IncludedContextKind(otherKind, otherKey).Build(),
177 shouldMatch: true,
178 },
179 {
180 name: "default-kind included list is ignored for other kind",
181 segment: buildSegment().Included(otherKey).Build(),
182 shouldMatch: false,
183 },
184 {
185 name: "included by key and also excluded",
186 segment: buildSegment().IncludedContextKind(otherKind, otherKey).ExcludedContextKind(otherKind, otherKey).Build(),
187 shouldMatch: true,
188 },
189 {
190 name: "neither included nor excluded, rule match",
191 segment: buildSegment().
192 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(
193 ldbuilders.ClauseWithKind(otherKind, ldattr.KeyAttr, ldmodel.OperatorIn, ldvalue.String(otherKey)),
194 )).
195 Build(),
196 shouldMatch: true,
197 },
198 {
199 name: "excluded, so rules are ignored",
200 segment: buildSegment().
201 ExcludedContextKind(otherKind, otherKey).
202 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(
203 ldbuilders.ClauseWithKind(otherKind, ldattr.KeyAttr, ldmodel.OperatorIn, ldvalue.String(otherKey)),
204 )).
205 Build(),
206 shouldMatch: false,
207 },
208 } {
209 p1 := p
210 p1.context = context
211 doSegmentMatchTest(t, p1)
212 }
213 })
214 }
215
216 func TestSegmentMatchClauseFallsThroughIfSegmentNotFound(t *testing.T) {
217 f := makeBooleanFlagToMatchAnyOfSegments("unknown-segment-key")
218 evaluator := NewEvaluator(basicDataProvider().withNonexistentSegment("unknown-segment-key"))
219
220 result := evaluator.Evaluate(&f, flagTestContext, nil)
221 assert.False(t, result.Detail.Value.BoolValue())
222 }
223
224 func TestCanMatchJustOneSegmentFromList(t *testing.T) {
225 segment := buildSegment().Included(flagTestContext.Key()).Build()
226 f := makeBooleanFlagToMatchAnyOfSegments("unknown-segment-key", segment.Key)
227 evaluator := NewEvaluator(basicDataProvider().withStoredSegments(segment).withNonexistentSegment("unknown-segment-key"))
228
229 result := evaluator.Evaluate(&f, flagTestContext, nil)
230 assert.True(t, result.Detail.Value.BoolValue())
231 }
232
233 func TestSegmentRulesCanReferenceOtherSegments(t *testing.T) {
234 context1, context2, context3 := ldcontext.New("key1"), ldcontext.New("key2"), ldcontext.New("key3")
235
236 segment0 := ldbuilders.NewSegmentBuilder("segmentkey0").
237 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(ldbuilders.SegmentMatchClause("segmentkey1"))).
238 Build()
239 segment1 := ldbuilders.NewSegmentBuilder("segmentkey1").
240 Included(context1.Key()).
241 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(ldbuilders.SegmentMatchClause("segmentkey2"))).
242 Build()
243 segment2 := ldbuilders.NewSegmentBuilder("segmentkey2").
244 Included(context2.Key()).
245 Build()
246
247 flag := makeBooleanFlagToMatchAnyOfSegments(segment0.Key)
248 evaluator := NewEvaluator(basicDataProvider().withStoredSegments(segment0, segment1, segment2))
249
250 assert.True(t, evaluator.Evaluate(&flag, context1, nil).Detail.Value.BoolValue())
251 assert.True(t, evaluator.Evaluate(&flag, context2, nil).Detail.Value.BoolValue())
252 assert.False(t, evaluator.Evaluate(&flag, context3, nil).Detail.Value.BoolValue())
253 }
254
255 func TestSegmentCycleDetection(t *testing.T) {
256 for _, cycleGoesToOriginalSegment := range []bool{true, false} {
257 t.Run(fmt.Sprintf("cycleGoesToOriginalFlag=%t", cycleGoesToOriginalSegment), func(t *testing.T) {
258
259 segment0 := ldbuilders.NewSegmentBuilder("segmentkey0").
260 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(ldbuilders.SegmentMatchClause("segmentkey1"))).
261 Build()
262 segment1 := ldbuilders.NewSegmentBuilder("segmentkey1").
263 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(ldbuilders.SegmentMatchClause("segmentkey2"))).
264 Build()
265 cycleTargetKey := segment1.Key
266 if cycleGoesToOriginalSegment {
267 cycleTargetKey = segment0.Key
268 }
269 segment2 := ldbuilders.NewSegmentBuilder("segmentkey2").
270 AddRule(ldbuilders.NewSegmentRuleBuilder().Clauses(ldbuilders.SegmentMatchClause(cycleTargetKey))).
271 Build()
272
273 flag := makeBooleanFlagToMatchAnyOfSegments(segment0.Key)
274 logCapture := ldlogtest.NewMockLog()
275 evaluator := NewEvaluatorWithOptions(
276 basicDataProvider().withStoredSegments(segment0, segment1, segment2),
277 EvaluatorOptionErrorLogger(logCapture.Loggers.ForLevel(ldlog.Error)),
278 )
279
280 result := evaluator.Evaluate(&flag, flagTestContext, FailOnAnyPrereqEvent(t))
281 m.In(t).Assert(result, ResultDetailError(ldreason.EvalErrorMalformedFlag))
282
283 errorLines := logCapture.GetOutput(ldlog.Error)
284 require.Len(t, errorLines, 1)
285 assert.Regexp(t, `.*segment rule.*circular reference`, errorLines[0])
286 })
287 }
288 }
289
290 func TestSegmentRulePercentageRollout(t *testing.T) {
291
292 segmentKey, salt := "segkey", "salty"
293 key1, key2 := "userKeyA", "userKeyZ"
294 customAttr := "attr1"
295 weightCutoff := 30000
296
297
298
299 type params struct {
300 kind ldcontext.Kind
301 multiKind bool
302 bucketBy string
303 }
304 var allParams []params
305
306
307 for _, multiKind := range []bool{true, false} {
308 for _, bucketBy := range []string{"", customAttr} {
309 allParams = append(allParams, params{
310 kind: ldcontext.DefaultKind,
311 multiKind: multiKind,
312 bucketBy: bucketBy,
313 })
314 }
315 }
316 for _, p := range allParams {
317 t.Run(fmt.Sprintf("%+v", p), func(t *testing.T) {
318 clauseMatchingAnyKeyForContextKind := ldbuilders.Negate(
319 ldbuilders.ClauseWithKind(p.kind, ldattr.KeyAttr, ldmodel.OperatorIn, ldvalue.String("")))
320 rule := ldbuilders.NewSegmentRuleBuilder().
321 Clauses(clauseMatchingAnyKeyForContextKind).
322 Weight(weightCutoff)
323 if p.bucketBy != "" {
324 rule.BucketBy(p.bucketBy)
325 }
326 segment := ldbuilders.NewSegmentBuilder(segmentKey).
327 AddRule(rule).
328 Salt(salt).
329 Build()
330 makeSingleKindContext := func(key string) ldcontext.Context {
331 if p.bucketBy == "" {
332 return ldcontext.NewWithKind(p.kind, key)
333 }
334 return ldcontext.NewBuilder("irrelevantKey").Kind(p.kind).SetString(p.bucketBy, key).Build()
335 }
336 makeContext := makeSingleKindContext
337 if p.multiKind {
338 makeContext = func(key string) ldcontext.Context {
339 return ldcontext.NewMulti(makeSingleKindContext(key),
340 ldcontext.NewWithKind("irrelevantKind", "irrelevantKey"))
341 }
342 }
343 assertSegmentMatch(t, segment, makeContext(key1), true)
344 assertSegmentMatch(t, segment, makeContext(key2), false)
345 })
346 }
347 }
348
349 func TestSegmentRuleRolloutFailureConditions(t *testing.T) {
350 t.Run("conditions that produce zero bucket value causing a match", func(t *testing.T) {
351
352
353
354
355 t.Run("bucketBy attribute not found", func(t *testing.T) {
356 segment := buildSegment().Salt("salty").
357 AddRule(ldbuilders.NewSegmentRuleBuilder().
358 Clauses(makeClauseToMatchAnyContextOfAnyKind()).
359 BucketBy("unknown-attribute").
360 Weight(1)).
361 Build()
362
363 context := ldcontext.New("key")
364 assertSegmentMatch(t, segment, context, true)
365 })
366
367 t.Run("bucketBy attribute has invalid value type", func(t *testing.T) {
368 segment := buildSegment().Salt("salty").
369 AddRule(ldbuilders.NewSegmentRuleBuilder().
370 Clauses(makeClauseToMatchAnyContextOfAnyKind()).
371 BucketBy("attr1").
372 Weight(1)).
373 Build()
374
375 context := ldcontext.NewBuilder("key").SetBool("attr1", true).Build()
376 assertSegmentMatch(t, segment, context, true)
377 })
378 })
379
380 t.Run("conditions that force a non-match", func(t *testing.T) {
381 t.Run("context kind not found", func(t *testing.T) {
382 segment := buildSegment().
383 AddRule(ldbuilders.NewSegmentRuleBuilder().
384 Clauses(makeClauseToMatchAnyContextOfAnyKind()).
385 Weight(100000)).
386 Salt("salty").
387 Build()
388
389 t.Run("single-kind context", func(t *testing.T) {
390 context := ldcontext.NewWithKind("org", "userKeyA")
391 assertSegmentMatch(t, segment, context, false)
392 })
393
394 t.Run("multi-kind context", func(t *testing.T) {
395 context := ldcontext.NewMulti(ldcontext.NewWithKind("org", "userKeyA"),
396 ldcontext.NewWithKind("other", "userKeyA"))
397 assertSegmentMatch(t, segment, context, false)
398 })
399 })
400 })
401 }
402
403 func TestSegmentRuleRolloutGetsAttributesFromSpecifiedContextKind(t *testing.T) {
404 segment := buildSegment().
405 AddRule(ldbuilders.NewSegmentRuleBuilder().
406 Clauses(ldbuilders.Clause(ldattr.KeyAttr, ldmodel.OperatorContains, ldvalue.String("x"))).
407 Weight(30000)).
408 Salt("salty").
409 Build()
410
411 t.Run("single-kind context", func(t *testing.T) {
412 context := ldcontext.NewWithKind("org", "userKeyA")
413 assertSegmentMatch(t, segment, context, false)
414 })
415
416 t.Run("multi-kind context", func(t *testing.T) {
417 context := ldcontext.NewMulti(ldcontext.NewWithKind("org", "userKeyA"),
418 ldcontext.NewWithKind("other", "userKeyA"))
419 assertSegmentMatch(t, segment, context, false)
420 })
421 }
422
423 func TestMalformedFlagErrorForBadSegmentProperties(t *testing.T) {
424 basicContext := ldcontext.New("userkey")
425
426 type testCaseParams struct {
427 name string
428 context ldcontext.Context
429 segment ldmodel.Segment
430 message string
431 }
432
433 for _, p := range []testCaseParams{
434 {
435 name: "bucketBy with invalid attribute",
436 context: basicContext,
437 segment: buildSegment().
438 AddRule(ldbuilders.NewSegmentRuleBuilder().
439 Clauses(ldbuilders.Clause(ldattr.KeyAttr, ldmodel.OperatorIn, ldvalue.String(basicContext.Key()))).
440 BucketByRef(ldattr.NewRef("///")).
441 Weight(30000)).
442 Salt("salty").
443 Build(),
444 message: "attribute reference",
445 },
446 {
447 name: "clause with undefined attribute",
448 context: basicContext,
449 segment: buildSegment().
450 AddRule(ldbuilders.NewSegmentRuleBuilder().
451 Clauses(ldbuilders.ClauseRef(ldattr.Ref{}, ldmodel.OperatorIn, ldvalue.String("a"))).
452 BucketByRef(ldattr.NewRef("///")).
453 Weight(30000)).
454 Salt("salty").
455 Build(),
456 message: "rule clause did not specify an attribute",
457 },
458 {
459 name: "clause with invalid attribute reference",
460 context: basicContext,
461 segment: buildSegment().
462 AddRule(ldbuilders.NewSegmentRuleBuilder().
463 Clauses(ldbuilders.ClauseRef(ldattr.NewRef("///"), ldmodel.OperatorIn, ldvalue.String("a"))).
464 BucketByRef(ldattr.NewRef("///")).
465 Weight(30000)).
466 Build(),
467 message: "invalid attribute reference",
468 },
469 } {
470 t.Run(p.name, func(t *testing.T) {
471 flag := makeBooleanFlagToMatchAnyOfSegments(p.segment.Key)
472
473 t.Run("returns error", func(t *testing.T) {
474 e := NewEvaluator(basicDataProvider().withStoredSegments(p.segment))
475 result := e.Evaluate(&flag, p.context, FailOnAnyPrereqEvent(t))
476
477 m.In(t).Assert(result, ResultDetailError(ldreason.EvalErrorMalformedFlag))
478 })
479
480 t.Run("logs error", func(t *testing.T) {
481 logCapture := ldlogtest.NewMockLog()
482 e := NewEvaluatorWithOptions(basicDataProvider().withStoredSegments(p.segment),
483 EvaluatorOptionErrorLogger(logCapture.Loggers.ForLevel(ldlog.Error)))
484 _ = e.Evaluate(&flag, p.context, FailOnAnyPrereqEvent(t))
485
486 errorLines := logCapture.GetOutput(ldlog.Error)
487 if assert.Len(t, errorLines, 1) {
488 assert.Regexp(t, `segment "`+p.segment.Key+`".*`+p.message, errorLines[0])
489 }
490 })
491 })
492 }
493 }
494
View as plain text