Skip to content

Commit 3d81ef8

Browse files
test: cover mixed list + rule segment membership in the gateway e2e test
Extend the rule-based segment gateway e2e test to upload a user to the segment's include list, verifying all three membership paths through GetEvaluations: matched by rule, matched by list, and neither. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e5881d7 commit 3d81ef8

1 file changed

Lines changed: 62 additions & 6 deletions

File tree

test/e2e/gateway/api_grpc_test.go

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
557558
func 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.
17871843
func addSegmentRule(t *testing.T, featureID, variationID, segmentID string, client featureclient.Client) {

0 commit comments

Comments
 (0)