...

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

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

     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  	// Note: segment key and salt are significant in bucketing, so they're specified explicitly for this test
   292  	segmentKey, salt := "segkey", "salty"
   293  	key1, key2 := "userKeyA", "userKeyZ"
   294  	customAttr := "attr1"
   295  	weightCutoff := 30000
   296  	// key1 is known to have a bucket value of 0.14574753 (14574) and therefore falls within the cutoff;
   297  	// key2 is known to have a bucket value of 0.45679215 (45679) so it is outside of the cutoff.
   298  
   299  	type params struct {
   300  		kind      ldcontext.Kind
   301  		multiKind bool
   302  		bucketBy  string
   303  	}
   304  	var allParams []params
   305  	// Note: currently we're not testing any scenarios where the target kind is not "user",
   306  	// pending spec updates which will add support for this to the model
   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  		// See comments in evaluator_segment.go about failure modes of computeBucketValue.
   352  		// In these tests, we're setting the weight to 1 so that the rule will only match
   353  		// if the bucket value is 0, which is incredibly unlikely to be a real hash value.
   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)). // this would normally always be a match
   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