@@ -551,9 +551,10 @@ func TestGrpcGetEvaluationsFeatureFlagDisabled(t *testing.T) {
551551}
552552
553553// TestGrpcGetEvaluationsRuleBasedSegment verifies the whole rule-based
554- // segment flow: a segment whose membership is defined by a rule on a user
555- // attribute (no user list), referenced by a feature flag rule, evaluated
556- // through the gateway.
554+ // segment flow through the gateway: a segment with mixed membership — an
555+ // included-user list AND a rule on a user attribute — referenced by a
556+ // feature flag rule. A user belongs to the segment when it is in the list
557+ // OR matches the rule.
557558func TestGrpcGetEvaluationsRuleBasedSegment (t * testing.T ) {
558559 t .Parallel ()
559560 featureClient := newFeatureClient (t )
@@ -564,8 +565,7 @@ func TestGrpcGetEvaluationsRuleBasedSegment(t *testing.T) {
564565 tag := fmt .Sprintf ("%s-tag-%s" , prefixTestName , uuid )
565566 featureID := newFeatureID (t , uuid )
566567
567- // Rule-based segment: membership is defined by a user attribute,
568- // the user list stays empty.
568+ // Segment with a rule on a user attribute.
569569 attributeValue := fmt .Sprintf ("%s-plan-%s" , prefixTestName , uuid )
570570 segmentName := fmt .Sprintf ("%s-segment-%s" , prefixTestName , uuid )
571571 if * testID != "" {
@@ -590,6 +590,11 @@ func TestGrpcGetEvaluationsRuleBasedSegment(t *testing.T) {
590590 if err != nil {
591591 t .Fatal (err )
592592 }
593+ // Mixed membership: also upload a user to the segment's include list.
594+ // The upload must complete before the first evaluation so the gateway
595+ // caches the complete segment state (user list + rules) on first fetch.
596+ listedUserID := newUserID (t , fmt .Sprintf ("listed-%s" , uuid ))
597+ uploadSegmentUsersAndWait (t , featureClient , segmentRes .Segment .Id , []string {listedUserID })
593598
594599 // Feature with a rule targeting the segment: matched users get variation B.
595600 req := newCreateFeatureReq (featureID )
@@ -612,7 +617,19 @@ func TestGrpcGetEvaluationsRuleBasedSegment(t *testing.T) {
612617 assert .Equal (t , featureproto .Reason_RULE , eval .Reason .Type )
613618 assert .Equal (t , req .Variations [1 ].Value , eval .VariationValue )
614619
615- // A user without the attribute is not in the segment
620+ // The listed user gets variation B even though
621+ // its attributes do not match the segment rule.
622+ res = grpcGetEvaluationsWithUser (t , tag , & userproto.User {
623+ Id : listedUserID ,
624+ })
625+ eval , err = findFeature (res .Evaluations .Evaluations , featureID )
626+ if err != nil {
627+ t .Fatal (err )
628+ }
629+ assert .Equal (t , featureproto .Reason_RULE , eval .Reason .Type )
630+ assert .Equal (t , req .Variations [1 ].Value , eval .VariationValue )
631+
632+ // A user neither listed nor matching the rule is not in the segment
616633 // and gets the default variation A.
617634 res = grpcGetEvaluationsWithUser (t , tag , & userproto.User {
618635 Id : newUserID (t , newUUID (t )),
@@ -1782,6 +1799,45 @@ func addRule(t *testing.T, featureID, variationID string, client featureclient.C
17821799 }
17831800}
17841801
1802+ // uploadSegmentUsersAndWait uploads the users to the segment's include list
1803+ // and waits until the asynchronous upload completes.
1804+ func uploadSegmentUsersAndWait (
1805+ t * testing.T ,
1806+ client featureclient.Client ,
1807+ segmentID string ,
1808+ userIDs []string ,
1809+ ) {
1810+ t .Helper ()
1811+ ctx , cancel := context .WithTimeout (context .Background (), timeout )
1812+ defer cancel ()
1813+ data := []byte (strings .Join (userIDs , "\n " ) + "\n " )
1814+ _ , err := client .BulkUploadSegmentUsers (ctx , & featureproto.BulkUploadSegmentUsersRequest {
1815+ EnvironmentId : * environmentID ,
1816+ SegmentId : segmentID ,
1817+ Data : data ,
1818+ State : featureproto .SegmentUser_INCLUDED ,
1819+ })
1820+ if err != nil {
1821+ t .Fatal (err )
1822+ }
1823+ req := & featureproto.ListSegmentUsersRequest {
1824+ SegmentId : segmentID ,
1825+ State : & wrapperspb.Int32Value {Value : int32 (featureproto .SegmentUser_INCLUDED )},
1826+ EnvironmentId : * environmentID ,
1827+ }
1828+ for i := 0 ; i < 25 ; i ++ {
1829+ if err := ctx .Err (); err != nil {
1830+ t .Fatalf ("uploadSegmentUsersAndWait: context done: %v" , err )
1831+ }
1832+ res , err := client .ListSegmentUsers (ctx , req )
1833+ if err == nil && res != nil && len (res .Users ) >= len (userIDs ) {
1834+ return
1835+ }
1836+ time .Sleep (2 * time .Second )
1837+ }
1838+ t .Fatalf ("segment users not ready. segmentID: %s" , segmentID )
1839+ }
1840+
17851841// addSegmentRule adds a fixed-strategy rule with a SEGMENT clause
17861842// referencing the given segment.
17871843func addSegmentRule (t * testing.T , featureID , variationID , segmentID string , client featureclient.Client ) {
0 commit comments