diff --git a/internal/server/evaluation/evaluation_test.go b/internal/server/evaluation/evaluation_test.go index c294ff20bc..248482e315 100644 --- a/internal/server/evaluation/evaluation_test.go +++ b/internal/server/evaluation/evaluation_test.go @@ -394,15 +394,20 @@ func TestBoolean_PercentageRuleFallthrough_SegmentMatch(t *testing.T) { RolloutType: flipt.RolloutType_SEGMENT_ROLLOUT_TYPE, Rank: 2, Segment: &storage.RolloutSegment{ - Key: "test-segment", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, - Value: true, - Constraints: []storage.EvaluationConstraint{ - { - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "hello", - Operator: flipt.OpEQ, - Value: "world", + Value: true, + SegmentOperator: flipt.SegmentOperator_OR_SEGMENT_OPERATOR, + Segments: map[string]*storage.EvaluationSegment{ + "test-segment": { + SegmentKey: "test-segment", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "hello", + Operator: flipt.OpEQ, + Value: "world", + }, + }, }, }, }, @@ -447,21 +452,26 @@ func TestBoolean_SegmentMatch_MultipleConstraints(t *testing.T) { RolloutType: flipt.RolloutType_SEGMENT_ROLLOUT_TYPE, Rank: 1, Segment: &storage.RolloutSegment{ - Key: "test-segment", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, - Value: true, - Constraints: []storage.EvaluationConstraint{ - { - Type: flipt.ComparisonType_NUMBER_COMPARISON_TYPE, - Property: "pitimes100", - Operator: flipt.OpEQ, - Value: "314", - }, - { - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "hello", - Operator: flipt.OpEQ, - Value: "world", + Value: true, + SegmentOperator: flipt.SegmentOperator_OR_SEGMENT_OPERATOR, + Segments: map[string]*storage.EvaluationSegment{ + "test-segment": { + SegmentKey: "test-segment", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + Type: flipt.ComparisonType_NUMBER_COMPARISON_TYPE, + Property: "pitimes100", + Operator: flipt.OpEQ, + Value: "314", + }, + { + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "hello", + Operator: flipt.OpEQ, + Value: "world", + }, + }, }, }, }, @@ -483,6 +493,77 @@ func TestBoolean_SegmentMatch_MultipleConstraints(t *testing.T) { assert.Equal(t, rpcevaluation.EvaluationReason_MATCH_EVALUATION_REASON, res.Reason) } +func TestBoolean_SegmentMatch_MultipleSegments_WithAnd(t *testing.T) { + var ( + flagKey = "test-flag" + namespaceKey = "test-namespace" + store = &evaluationStoreMock{} + logger = zaptest.NewLogger(t) + s = New(logger, store) + ) + + store.On("GetFlag", mock.Anything, namespaceKey, flagKey).Return( + &flipt.Flag{ + NamespaceKey: "test-namespace", + Key: "test-flag", + Enabled: true, + Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, + }, nil) + + store.On("GetEvaluationRollouts", mock.Anything, namespaceKey, flagKey).Return([]*storage.EvaluationRollout{ + { + NamespaceKey: namespaceKey, + RolloutType: flipt.RolloutType_SEGMENT_ROLLOUT_TYPE, + Rank: 1, + Segment: &storage.RolloutSegment{ + Value: true, + SegmentOperator: flipt.SegmentOperator_AND_SEGMENT_OPERATOR, + Segments: map[string]*storage.EvaluationSegment{ + "test-segment": { + SegmentKey: "test-segment", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + Type: flipt.ComparisonType_NUMBER_COMPARISON_TYPE, + Property: "pitimes100", + Operator: flipt.OpEQ, + Value: "314", + }, + }, + }, + "another-segment": { + SegmentKey: "another-segment", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "hello", + Operator: flipt.OpEQ, + Value: "world", + }, + }, + }, + }, + }, + }, + }, nil) + + res, err := s.Boolean(context.TODO(), &rpcevaluation.EvaluationRequest{ + FlagKey: flagKey, + EntityId: "test-entity", + NamespaceKey: namespaceKey, + Context: map[string]string{ + "hello": "world", + "pitimes100": "314", + }, + }) + + require.NoError(t, err) + + assert.Equal(t, true, res.Enabled) + assert.Equal(t, rpcevaluation.EvaluationReason_MATCH_EVALUATION_REASON, res.Reason) +} + func TestBoolean_RulesOutOfOrder(t *testing.T) { var ( flagKey = "test-flag" @@ -515,15 +596,20 @@ func TestBoolean_RulesOutOfOrder(t *testing.T) { RolloutType: flipt.RolloutType_SEGMENT_ROLLOUT_TYPE, Rank: 0, Segment: &storage.RolloutSegment{ - Key: "test-segment", - MatchType: flipt.MatchType_ANY_MATCH_TYPE, - Value: true, - Constraints: []storage.EvaluationConstraint{ - { - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "hello", - Operator: flipt.OpEQ, - Value: "world", + Value: true, + SegmentOperator: flipt.SegmentOperator_OR_SEGMENT_OPERATOR, + Segments: map[string]*storage.EvaluationSegment{ + "test-segment": { + SegmentKey: "test-segment", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "hello", + Operator: flipt.OpEQ, + Value: "world", + }, + }, }, }, }, @@ -656,13 +742,19 @@ func TestBatch_Success(t *testing.T) { SegmentKey: "bar", SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, Rank: 0, - Constraints: []storage.EvaluationConstraint{ - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "hello", - Operator: flipt.OpEQ, - Value: "world", + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "hello", + Operator: flipt.OpEQ, + Value: "world", + }, + }, }, }, }, diff --git a/internal/server/evaluation/legacy_evaluator_test.go b/internal/server/evaluation/legacy_evaluator_test.go index 902fb93d0e..2ae598de31 100644 --- a/internal/server/evaluation/legacy_evaluator_test.go +++ b/internal/server/evaluation/legacy_evaluator_test.go @@ -849,34 +849,42 @@ func TestEvaluator_RulesOutOfOrder(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 1, - Constraints: []storage.EvaluationConstraint{ - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", + ID: "1", + FlagKey: "foo", + Rank: 1, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + }, }, }, }, { - ID: "2", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", + ID: "2", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + }, }, }, }, @@ -906,18 +914,22 @@ func TestEvaluator_ErrorParsingNumber(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 1, - Constraints: []storage.EvaluationConstraint{ - { - ID: "2", - Type: flipt.ComparisonType_NUMBER_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "boz", + ID: "1", + FlagKey: "foo", + Rank: 1, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + ID: "2", + Type: flipt.ComparisonType_NUMBER_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "boz", + }, + }, }, }, }, @@ -946,18 +958,22 @@ func TestEvaluator_ErrorParsingDateTime(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 1, - Constraints: []storage.EvaluationConstraint{ - { - ID: "2", - Type: flipt.ComparisonType_DATETIME_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "boz", + ID: "1", + FlagKey: "foo", + Rank: 1, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + ID: "2", + Type: flipt.ComparisonType_DATETIME_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "boz", + }, + }, }, }, }, @@ -987,18 +1003,22 @@ func TestEvaluator_ErrorGettingDistributions(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + }, }, }, }, @@ -1030,18 +1050,22 @@ func TestEvaluator_MatchAll_NoVariants_NoDistributions(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + }, }, }, }, @@ -1104,7 +1128,7 @@ func TestEvaluator_MatchAll_NoVariants_NoDistributions(t *testing.T) { } } -func TestEvaluator_DistributionNotMatched(t *testing.T) { +func TestEvaluator_MatchAll_MultipleSegments(t *testing.T) { var ( store = &evaluationStoreMock{} logger = zaptest.NewLogger(t) @@ -1114,26 +1138,135 @@ func TestEvaluator_DistributionNotMatched(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - // constraint: bar (string) == baz - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", + ID: "1", + FlagKey: "foo", + Rank: 0, + SegmentOperator: flipt.SegmentOperator_AND_SEGMENT_OPERATOR, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + }, }, - // constraint: admin (bool) == true - { - ID: "3", - Type: flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE, - Property: "admin", - Operator: flipt.OpTrue, + "foo": { + SegmentKey: "foo", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "company", + Operator: flipt.OpEQ, + Value: "flipt", + }, + }, + }, + }, + }, + }, nil) + + store.On("GetEvaluationDistributions", mock.Anything, "1").Return([]*storage.EvaluationDistribution{}, nil) + + tests := []struct { + name string + req *flipt.EvaluationRequest + wantMatch bool + }{ + { + name: "match string value", + req: &flipt.EvaluationRequest{ + FlagKey: "foo", + EntityId: "1", + Context: map[string]string{ + "bar": "baz", + "company": "flipt", + }, + }, + wantMatch: true, + }, + { + name: "no match string value", + req: &flipt.EvaluationRequest{ + FlagKey: "foo", + EntityId: "1", + Context: map[string]string{ + "bar": "boz", + }, + }, + }, + } + + for _, tt := range tests { + var ( + req = tt.req + wantMatch = tt.wantMatch + ) + + t.Run(tt.name, func(t *testing.T) { + resp, err := s.Evaluate(context.TODO(), enabledFlag, req) + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, "foo", resp.FlagKey) + assert.Equal(t, req.Context, resp.RequestContext) + + if !wantMatch { + assert.False(t, resp.Match) + assert.Len(t, resp.SegmentKeys, 0) + return + } + + assert.True(t, resp.Match) + assert.Len(t, resp.SegmentKeys, 2) + assert.Equal(t, resp.SegmentKeys[0], "bar") + assert.Equal(t, resp.SegmentKeys[1], "foo") + assert.Empty(t, resp.Value) + assert.Equal(t, flipt.EvaluationReason_MATCH_EVALUATION_REASON, resp.Reason) + }) + } +} + +func TestEvaluator_DistributionNotMatched(t *testing.T) { + var ( + store = &evaluationStoreMock{} + logger = zaptest.NewLogger(t) + s = NewEvaluator(logger, store) + ) + + store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( + []*storage.EvaluationRule{ + { + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + // constraint: bar (string) == baz + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + // constraint: admin (bool) == true + { + ID: "3", + Type: flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE, + Property: "admin", + Operator: flipt.OpTrue, + }, + }, }, }, }, @@ -1177,26 +1310,30 @@ func TestEvaluator_MatchAll_SingleVariantDistribution(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - // constraint: bar (string) == baz - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", - }, - // constraint: admin (bool) == true - { - ID: "3", - Type: flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE, - Property: "admin", - Operator: flipt.OpTrue, + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + // constraint: bar (string) == baz + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + // constraint: admin (bool) == true + { + ID: "3", + Type: flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE, + Property: "admin", + Operator: flipt.OpTrue, + }, + }, }, }, }, @@ -1302,19 +1439,23 @@ func TestEvaluator_MatchAll_RolloutDistribution(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - // constraint: bar (string) == baz - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + // constraint: bar (string) == baz + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + }, }, }, }, @@ -1418,27 +1559,35 @@ func TestEvaluator_MatchAll_RolloutDistribution_MultiRule(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "subscribers", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - // constraint: premium_user (bool) == true - { - ID: "2", - Type: flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE, - Property: "premium_user", - Operator: flipt.OpTrue, + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "subscribers": { + SegmentKey: "subscribers", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + // constraint: premium_user (bool) == true + { + ID: "2", + Type: flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE, + Property: "premium_user", + Operator: flipt.OpTrue, + }, + }, }, }, }, { - ID: "2", - FlagKey: "foo", - SegmentKey: "all_users", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 1, + ID: "2", + FlagKey: "foo", + Rank: 1, + Segments: map[string]*storage.EvaluationSegment{ + "all_users": { + SegmentKey: "all_users", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + }, + }, }, }, nil) @@ -1488,11 +1637,15 @@ func TestEvaluator_MatchAll_NoConstraints(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 0, + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + }, + }, }, }, nil) @@ -1594,18 +1747,22 @@ func TestEvaluator_MatchAny_NoVariants_NoDistributions(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ANY_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + }, }, }, }, @@ -1678,26 +1835,30 @@ func TestEvaluator_MatchAny_SingleVariantDistribution(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ANY_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - // constraint: bar (string) == baz - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", - }, - // constraint: admin (bool) == true - { - ID: "3", - Type: flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE, - Property: "admin", - Operator: flipt.OpTrue, + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + // constraint: bar (string) == baz + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + // constraint: admin (bool) == true + { + ID: "3", + Type: flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE, + Property: "admin", + Operator: flipt.OpTrue, + }, + }, }, }, }, @@ -1835,19 +1996,23 @@ func TestEvaluator_MatchAny_RolloutDistribution(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ANY_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - // constraint: bar (string) == baz - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + // constraint: bar (string) == baz + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + }, }, }, }, @@ -1951,18 +2116,22 @@ func TestEvaluator_MatchAny_RolloutDistribution_MultiRule(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "subscribers", - SegmentMatchType: flipt.MatchType_ANY_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - // constraint: premium_user (bool) == true - { - ID: "2", - Type: flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE, - Property: "premium_user", - Operator: flipt.OpTrue, + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "subscribers": { + SegmentKey: "subscribers", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + // constraint: premium_user (bool) == true + { + ID: "2", + Type: flipt.ComparisonType_BOOLEAN_COMPARISON_TYPE, + Property: "premium_user", + Operator: flipt.OpTrue, + }, + }, }, }, }, @@ -2021,11 +2190,15 @@ func TestEvaluator_MatchAny_NoConstraints(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ANY_MATCH_TYPE, - Rank: 0, + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + }, + }, }, }, nil) @@ -2127,19 +2300,23 @@ func TestEvaluator_FirstRolloutRuleIsZero(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - // constraint: bar (string) == baz - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ANY_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + // constraint: bar (string) == baz + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + }, }, }, }, @@ -2222,19 +2399,23 @@ func TestEvaluator_MultipleZeroRolloutDistributions(t *testing.T) { store.On("GetEvaluationRules", mock.Anything, mock.Anything, "foo").Return( []*storage.EvaluationRule{ { - ID: "1", - FlagKey: "foo", - SegmentKey: "bar", - SegmentMatchType: flipt.MatchType_ALL_MATCH_TYPE, - Rank: 0, - Constraints: []storage.EvaluationConstraint{ - // constraint: bar (string) == baz - { - ID: "2", - Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, - Property: "bar", - Operator: flipt.OpEQ, - Value: "baz", + ID: "1", + FlagKey: "foo", + Rank: 0, + Segments: map[string]*storage.EvaluationSegment{ + "bar": { + SegmentKey: "bar", + MatchType: flipt.MatchType_ALL_MATCH_TYPE, + Constraints: []storage.EvaluationConstraint{ + // constraint: bar (string) == baz + { + ID: "2", + Type: flipt.ComparisonType_STRING_COMPARISON_TYPE, + Property: "bar", + Operator: flipt.OpEQ, + Value: "baz", + }, + }, }, }, }, diff --git a/internal/server/rollout_test.go b/internal/server/rollout_test.go index 511439edcd..fe8705d976 100644 --- a/internal/server/rollout_test.go +++ b/internal/server/rollout_test.go @@ -109,8 +109,35 @@ func TestCreateRollout(t *testing.T) { Rank: 1, Rule: &flipt.Rollout_Segment{ Segment: &flipt.RolloutSegment{ - SegmentKey: "segmentKey", - Value: true, + SegmentOperator: flipt.SegmentOperator_OR_SEGMENT_OPERATOR, + SegmentKey: "segmentKey", + Value: true, + }, + }, + }, + }, + { + name: "segments", + req: &flipt.CreateRolloutRequest{ + FlagKey: "flagKey", + Rank: 1, + Rule: &flipt.CreateRolloutRequest_Segment{ + Segment: &flipt.RolloutSegment{ + SegmentOperator: flipt.SegmentOperator_AND_SEGMENT_OPERATOR, + SegmentKeys: []string{"segmentKey1", "segmentKey2"}, + Value: true, + }, + }, + }, + resp: &flipt.Rollout{ + Id: "1", + FlagKey: "flagKey", + Rank: 1, + Rule: &flipt.Rollout_Segment{ + Segment: &flipt.RolloutSegment{ + SegmentOperator: flipt.SegmentOperator_AND_SEGMENT_OPERATOR, + SegmentKeys: []string{"segmentKey1", "segmentKey2"}, + Value: true, }, }, }, diff --git a/internal/server/rule_test.go b/internal/server/rule_test.go index 3033a64fe7..0bd51e08a4 100644 --- a/internal/server/rule_test.go +++ b/internal/server/rule_test.go @@ -149,6 +149,36 @@ func TestCreateRule(t *testing.T) { assert.NotNil(t, got) } +func TestCreateRule_MultipleSegments(t *testing.T) { + var ( + store = &storeMock{} + logger = zaptest.NewLogger(t) + s = &Server{ + logger: logger, + store: store, + } + req = &flipt.CreateRuleRequest{ + FlagKey: "flagKey", + SegmentKeys: []string{"segmentKey1", "segmentKey2"}, + SegmentOperator: flipt.SegmentOperator_AND_SEGMENT_OPERATOR, + Rank: 1, + } + ) + + store.On("CreateRule", mock.Anything, req).Return(&flipt.Rule{ + Id: "1", + FlagKey: req.FlagKey, + SegmentKeys: req.SegmentKeys, + SegmentOperator: req.SegmentOperator, + Rank: req.Rank, + }, nil) + + got, err := s.CreateRule(context.TODO(), req) + require.NoError(t, err) + + assert.NotNil(t, got) +} + func TestUpdateRule(t *testing.T) { var ( store = &storeMock{} diff --git a/internal/storage/sql/migrator.go b/internal/storage/sql/migrator.go index 262f2176bf..ad7b6aa3fd 100644 --- a/internal/storage/sql/migrator.go +++ b/internal/storage/sql/migrator.go @@ -18,7 +18,7 @@ import ( ) var expectedVersions = map[Driver]uint{ - SQLite: 10, + SQLite: 11, Postgres: 10, MySQL: 8, CockroachDB: 7, diff --git a/internal/storage/sql/rollout_test.go b/internal/storage/sql/rollout_test.go index 0cebf68572..8fbc86afbb 100644 --- a/internal/storage/sql/rollout_test.go +++ b/internal/storage/sql/rollout_test.go @@ -191,6 +191,95 @@ func (s *DBTestSuite) TestListRollouts() { } } +func (s *DBTestSuite) TestListRollouts_MultipleSegments() { + t := s.T() + + flag, err := s.store.CreateFlag(context.TODO(), &flipt.CreateFlagRequest{ + Key: t.Name(), + Name: "foo", + Description: "bar", + Enabled: true, + Type: flipt.FlagType_BOOLEAN_FLAG_TYPE, + }) + + require.NoError(t, err) + assert.NotNil(t, flag) + + variant, err := s.store.CreateVariant(context.TODO(), &flipt.CreateVariantRequest{ + FlagKey: flag.Key, + Key: t.Name(), + Name: "foo", + Description: "bar", + }) + + require.NoError(t, err) + assert.NotNil(t, variant) + + firstSegment, err := s.store.CreateSegment(context.TODO(), &flipt.CreateSegmentRequest{ + Key: t.Name(), + Name: "foo", + Description: "bar", + }) + + require.NoError(t, err) + assert.NotNil(t, firstSegment) + + secondSegment, err := s.store.CreateSegment(context.TODO(), &flipt.CreateSegmentRequest{ + Key: "another_segment", + Name: "foo", + Description: "bar", + }) + + require.NoError(t, err) + assert.NotNil(t, secondSegment) + + reqs := []*flipt.CreateRolloutRequest{ + { + FlagKey: flag.Key, + Rank: 1, + Rule: &flipt.CreateRolloutRequest_Segment{ + Segment: &flipt.RolloutSegment{ + Value: true, + SegmentKeys: []string{firstSegment.Key, secondSegment.Key}, + }, + }, + }, + { + FlagKey: flag.Key, + Rank: 2, + Rule: &flipt.CreateRolloutRequest_Segment{ + Segment: &flipt.RolloutSegment{ + Value: true, + SegmentKeys: []string{firstSegment.Key, secondSegment.Key}, + }, + }, + }, + } + + for _, req := range reqs { + _, err := s.store.CreateRollout(context.TODO(), req) + require.NoError(t, err) + } + + res, err := s.store.ListRollouts(context.TODO(), storage.DefaultNamespace, flag.Key) + require.NoError(t, err) + + got := res.Results + assert.NotZero(t, len(got)) + + for _, rollout := range got { + assert.Equal(t, storage.DefaultNamespace, rollout.NamespaceKey) + + rs, ok := rollout.Rule.(*flipt.Rollout_Segment) + assert.True(t, ok, "rule should successfully assert to a rollout segment") + assert.Len(t, rs.Segment.SegmentKeys, 2) + assert.Equal(t, rs.Segment.SegmentKeys[0], firstSegment.Key) + assert.Equal(t, rs.Segment.SegmentKeys[1], secondSegment.Key) + assert.NotZero(t, rollout.CreatedAt) + assert.NotZero(t, rollout.UpdatedAt) + } +} + func (s *DBTestSuite) TestListRolloutsNamespace() { t := s.T() diff --git a/internal/storage/sql/rule_test.go b/internal/storage/sql/rule_test.go index ef4851c3bd..9507471eca 100644 --- a/internal/storage/sql/rule_test.go +++ b/internal/storage/sql/rule_test.go @@ -72,6 +72,73 @@ func (s *DBTestSuite) TestGetRule() { assert.NotZero(t, got.UpdatedAt) } +func (s *DBTestSuite) TestGetRule_MultipleSegments() { + t := s.T() + + flag, err := s.store.CreateFlag(context.TODO(), &flipt.CreateFlagRequest{ + Key: t.Name(), + Name: "foo", + Description: "bar", + Enabled: true, + }) + + require.NoError(t, err) + assert.NotNil(t, flag) + + variant, err := s.store.CreateVariant(context.TODO(), &flipt.CreateVariantRequest{ + FlagKey: flag.Key, + Key: t.Name(), + Name: "foo", + Description: "bar", + }) + + require.NoError(t, err) + assert.NotNil(t, variant) + + firstSegment, err := s.store.CreateSegment(context.TODO(), &flipt.CreateSegmentRequest{ + Key: t.Name(), + Name: "foo", + Description: "bar", + }) + + require.NoError(t, err) + assert.NotNil(t, firstSegment) + + secondSegment, err := s.store.CreateSegment(context.TODO(), &flipt.CreateSegmentRequest{ + Key: "another_segment", + Name: "bar", + Description: "foo", + }) + + require.NoError(t, err) + assert.NotNil(t, secondSegment) + + rule, err := s.store.CreateRule(context.TODO(), &flipt.CreateRuleRequest{ + FlagKey: flag.Key, + SegmentKeys: []string{firstSegment.Key, secondSegment.Key}, + Rank: 1, + }) + + require.NoError(t, err) + assert.NotNil(t, rule) + + got, err := s.store.GetRule(context.TODO(), storage.DefaultNamespace, rule.Id) + + require.NoError(t, err) + assert.NotNil(t, got) + + assert.Equal(t, rule.Id, got.Id) + assert.Equal(t, storage.DefaultNamespace, got.NamespaceKey) + assert.Equal(t, rule.FlagKey, got.FlagKey) + + assert.Len(t, rule.SegmentKeys, 2) + assert.Equal(t, firstSegment.Key, rule.SegmentKeys[0]) + assert.Equal(t, secondSegment.Key, rule.SegmentKeys[1]) + assert.Equal(t, rule.Rank, got.Rank) + assert.NotZero(t, got.CreatedAt) + assert.NotZero(t, got.UpdatedAt) +} + func (s *DBTestSuite) TestGetRuleNamespace() { t := s.T() @@ -211,6 +278,84 @@ func (s *DBTestSuite) TestListRules() { } } +func (s *DBTestSuite) TestListRules_MultipleSegments() { + t := s.T() + + flag, err := s.store.CreateFlag(context.TODO(), &flipt.CreateFlagRequest{ + Key: t.Name(), + Name: "foo", + Description: "bar", + Enabled: true, + }) + + require.NoError(t, err) + assert.NotNil(t, flag) + + variant, err := s.store.CreateVariant(context.TODO(), &flipt.CreateVariantRequest{ + FlagKey: flag.Key, + Key: t.Name(), + Name: "foo", + Description: "bar", + }) + + require.NoError(t, err) + assert.NotNil(t, variant) + + firstSegment, err := s.store.CreateSegment(context.TODO(), &flipt.CreateSegmentRequest{ + Key: t.Name(), + Name: "foo", + Description: "bar", + }) + + require.NoError(t, err) + assert.NotNil(t, firstSegment) + + secondSegment, err := s.store.CreateSegment(context.TODO(), &flipt.CreateSegmentRequest{ + Key: "another_segment", + Name: "foo", + Description: "bar", + }) + + require.NoError(t, err) + assert.NotNil(t, secondSegment) + + reqs := []*flipt.CreateRuleRequest{ + { + FlagKey: flag.Key, + SegmentKeys: []string{firstSegment.Key, secondSegment.Key}, + Rank: 1, + }, + { + FlagKey: flag.Key, + SegmentKeys: []string{firstSegment.Key, secondSegment.Key}, + Rank: 2, + }, + } + + for _, req := range reqs { + _, err := s.store.CreateRule(context.TODO(), req) + require.NoError(t, err) + } + + _, err = s.store.ListRules(context.TODO(), storage.DefaultNamespace, flag.Key, storage.WithPageToken("Hello World")) + assert.EqualError(t, err, "pageToken is not valid: \"Hello World\"") + + res, err := s.store.ListRules(context.TODO(), storage.DefaultNamespace, flag.Key) + require.NoError(t, err) + + got := res.Results + assert.NotZero(t, len(got)) + + for _, rule := range got { + assert.Equal(t, storage.DefaultNamespace, rule.NamespaceKey) + assert.Len(t, rule.SegmentKeys, 2) + assert.Equal(t, firstSegment.Key, rule.SegmentKeys[0]) + assert.Equal(t, secondSegment.Key, rule.SegmentKeys[1]) + assert.NotZero(t, rule.CreatedAt) + assert.NotZero(t, rule.UpdatedAt) + } +} + func (s *DBTestSuite) TestListRulesNamespace() { t := s.T()