package evaluation import ( "fmt" "testing" "github.com/launchdarkly/go-sdk-common/v3/ldcontext" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldbuilders" "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel" "github.com/stretchr/testify/assert" ) const dateStr1 = "2017-12-06T00:00:00.000-07:00" const dateStr2 = "2017-12-06T00:01:01.000-07:00" const dateMs1 = 10000000 const dateMs2 = 10000001 const invalidDate = "hey what's this?" type opTestInfo struct { opName ldmodel.Operator userValue interface{} clauseValue interface{} moreClauseValues []interface{} expected bool } var operatorTests = []opTestInfo{ // numeric operators {"in", int(99), int(99), nil, true}, {"in", int(99), int(99), []interface{}{int(98), int(97), int(96)}, true}, {"in", float64(99.0001), float64(99.0001), nil, true}, {"in", float64(99.0001), float64(99.0001), []interface{}{float64(98), float64(97), float64(96)}, true}, {"lessThan", int(1), float64(1.99999), nil, true}, {"lessThan", float64(1.99999), int(1), nil, false}, {"lessThan", int(1), uint(2), nil, true}, {"lessThanOrEqual", int(1), float64(1), nil, true}, {"greaterThan", int(2), float64(1.99999), nil, true}, {"greaterThan", float64(1.99999), int(2), nil, false}, {"greaterThan", int(2), uint(1), nil, true}, {"greaterThanOrEqual", int(1), float64(1), nil, true}, // string operators {"in", "x", "x", nil, true}, {"in", "x", "x", []interface{}{"a", "b", "c"}, true}, {"in", "x", "xyz", nil, false}, {"startsWith", "xyz", "x", nil, true}, {"startsWith", "x", "xyz", nil, false}, {"endsWith", "xyz", "z", nil, true}, {"endsWith", "z", "xyz", nil, false}, {"contains", "xyz", "y", nil, true}, {"contains", "y", "xyz", nil, false}, // mixed strings and numbers {"in", "99", int(99), nil, false}, {"in", int(99), "99", nil, false}, {"contains", "99", int(99), nil, false}, {"startsWith", "99", int(99), nil, false}, {"endsWith", "99", int(99), nil, false}, {"lessThanOrEqual", "99", int(99), nil, false}, {"lessThanOrEqual", int(99), "99", nil, false}, {"greaterThanOrEqual", "99", int(99), nil, false}, {"greaterThanOrEqual", int(99), "99", nil, false}, // equality of boolean values {"in", true, true, nil, true}, {"in", false, false, nil, true}, {"in", true, false, nil, false}, {"in", false, true, nil, false}, {"in", true, false, []interface{}{true}, true}, // regex {"matches", "hello world", "hello.*rld", nil, true}, {"matches", "hello world", "hello.*orl", nil, true}, {"matches", "hello world", "l+", nil, true}, {"matches", "hello world", "(world|planet)", nil, true}, {"matches", "hello world", "aloha", nil, false}, {"matches", "hello world", "***bad regex", nil, false}, // date operators {"before", dateStr1, dateStr2, nil, true}, {"before", dateMs1, dateMs2, nil, true}, {"before", dateStr2, dateStr1, nil, false}, {"before", dateMs2, dateMs1, nil, false}, {"before", dateStr1, dateStr1, nil, false}, {"before", dateMs1, dateMs1, nil, false}, {"before", nil, dateStr1, nil, false}, {"before", dateStr1, invalidDate, nil, false}, {"after", dateStr2, dateStr1, nil, true}, {"after", dateMs2, dateMs1, nil, true}, {"after", dateStr1, dateStr2, nil, false}, {"after", dateMs1, dateMs2, nil, false}, {"after", dateStr1, dateStr1, nil, false}, {"after", dateMs1, dateMs1, nil, false}, {"after", nil, dateStr1, nil, false}, {"after", dateStr1, invalidDate, nil, false}, // semver operators {"semVerEqual", "2.0.0", "2.0.0", nil, true}, {"semVerEqual", "2.0", "2.0.0", nil, true}, {"semVerEqual", "2-rc1", "2.0.0-rc1", nil, true}, {"semVerEqual", "2+build2", "2.0.0+build2", nil, true}, {"semVerEqual", "2.0.0", "2.0.1", nil, false}, {"semVerLessThan", "2.0.0", "2.0.1", nil, true}, {"semVerLessThan", "2.0", "2.0.1", nil, true}, {"semVerLessThan", "2.0.1", "2.0.0", nil, false}, {"semVerLessThan", "2.0.1", "2.0", nil, false}, {"semVerLessThan", "2.0.1", "xbad%ver", nil, false}, {"semVerLessThan", "2.0.0-rc", "2.0.0-rc.beta", nil, true}, {"semVerGreaterThan", "2.0.1", "2.0", nil, true}, {"semVerGreaterThan", "10.0.1", "2.0", nil, true}, {"semVerGreaterThan", "2.0.0", "2.0.1", nil, false}, {"semVerGreaterThan", "2.0", "2.0.1", nil, false}, {"semVerGreaterThan", "2.0.1", "xbad%ver", nil, false}, {"semVerGreaterThan", "2.0.0-rc.1", "2.0.0-rc.0", nil, true}, // invalid operator {"whatever", "x", "x", nil, false}, } func TestAllOperators(t *testing.T) { userAttr := "attr" for _, ti := range operatorTests { for _, withPreprocessing := range []bool{false, true} { t.Run( fmt.Sprintf("%v %s %v should be %v (preprocess: %t)", ti.userValue, ti.opName, ti.clauseValue, ti.expected, withPreprocessing), func(t *testing.T) { uValue := ldvalue.CopyArbitraryValue(ti.userValue) cValue := ldvalue.CopyArbitraryValue(ti.clauseValue) c := ldbuilders.Clause(userAttr, ti.opName) for _, v := range ti.moreClauseValues { c.Values = append(c.Values, ldvalue.CopyArbitraryValue(v)) } c.Values = append(c.Values, cValue) if withPreprocessing { flag := ldmodel.FeatureFlag{ Rules: []ldmodel.FlagRule{ {Clauses: []ldmodel.Clause{c}}, }, } ldmodel.PreprocessFlag(&flag) c = flag.Rules[0].Clauses[0] } context := ldcontext.NewBuilder("key").SetValue(userAttr, uValue).Build() isMatch, err := makeEvalScope(context).clauseMatchesContext(&c, evaluationStack{}) assert.NoError(t, err) assert.Equal(t, ti.expected, isMatch) }, ) } } }