diff --git a/integrationtests/flag_builder_test.go b/integrationtests/flag_builder_test.go index 662dd72a..002e34c3 100644 --- a/integrationtests/flag_builder_test.go +++ b/integrationtests/flag_builder_test.go @@ -1,3 +1,5 @@ +//go:build integrationtests + package integrationtests import ( @@ -17,28 +19,13 @@ type flagBuilder struct { on bool variations []ldapi.Variation prerequisites []ldapi.Prerequisite + clientSide ldapi.ClientSideAvailabilityPost helper *apiHelper } -func (f *flagBuilder) logAPIError(desc string, err error) error { - var apiError ldapi.GenericOpenAPIError - if errors.As(err, &apiError) { - body := string(apiError.Body()) - f.helper.loggers.Errorf("%s: %s (response body: %s)", f.scopedOp(desc), err, body) - } else { - f.helper.loggers.Errorf("%s: %s", f.scopedOp(desc), err) - } - return err -} - -func (f *flagBuilder) scopedOp(desc string) string { - return fmt.Sprintf("%s in %s/%s", desc, f.projectKey, f.envKey) -} - -func (f *flagBuilder) logAPISuccess(desc string) { - f.helper.loggers.Infof(f.scopedOp(desc)) -} - +// newFlagBuilder creates a builder for a flag which will be created in the specified project and environment. +// By default, the flag has two variations: off = false, and on = true. The flag is on by default. +// Additionally, the flag is available to both mobile and client SDKs. func newFlagBuilder(helper *apiHelper, flagKey string, projectKey string, envKey string) *flagBuilder { builder := &flagBuilder{ key: flagKey, @@ -48,10 +35,15 @@ func newFlagBuilder(helper *apiHelper, flagKey string, projectKey string, envKey offVariation: 0, fallthroughVariation: 1, helper: helper, + clientSide: ldapi.ClientSideAvailabilityPost{ + UsingMobileKey: true, + UsingEnvironmentId: true, + }, } return builder.Variations(ldvalue.Bool(false), ldvalue.Bool(true)) } +// Variations overwrites the flag's variations. A valid flag has two or more variations. func (f *flagBuilder) Variations(variation1 ldvalue.Value, variations ...ldvalue.Value) *flagBuilder { f.variations = nil for _, value := range append([]ldvalue.Value{variation1}, variations...) { @@ -61,45 +53,47 @@ func (f *flagBuilder) Variations(variation1 ldvalue.Value, variations ...ldvalue return f } +// ClientSideUsingEnvironmentID enables the flag for clients that use environment ID for auth. +func (f *flagBuilder) ClientSideUsingEnvironmentID(usingEnvID bool) *flagBuilder { + f.clientSide.UsingEnvironmentId = usingEnvID + return f +} + +// Prerequisites overwrites the flag's prerequisites. func (f *flagBuilder) Prerequisites(prerequisites []ldapi.Prerequisite) *flagBuilder { f.prerequisites = prerequisites return f } +// Prerequisite is a helper that calls Prerequisites with a single value. func (f *flagBuilder) Prerequisite(prerequisiteKey string, variation int32) *flagBuilder { return f.Prerequisites([]ldapi.Prerequisite{{Key: prerequisiteKey, Variation: variation}}) } +// OffVariation sets the flag's off variation. func (f *flagBuilder) OffVariation(v int) *flagBuilder { f.offVariation = v return f } +// FallthroughVariation sets the flag's fallthrough variation. func (f *flagBuilder) FallthroughVariation(v int) *flagBuilder { f.fallthroughVariation = v return f } +// On enables or disables flag targeting. func (f *flagBuilder) On(on bool) *flagBuilder { f.on = on return f } +// Create creates the flag using the LD REST API. func (f *flagBuilder) Create() error { - - if len(f.variations) < 2 { - return errors.New("must have >= 2 variations") - } - if f.offVariation < 0 || f.offVariation >= len(f.variations) { - return errors.New("offVariation out of range") - } - if f.fallthroughVariation < 0 || f.fallthroughVariation >= len(f.variations) { - return errors.New("fallthroughVariation out of range") - } - flagPost := ldapi.FeatureFlagBody{ - Name: f.key, - Key: f.key, + Name: f.key, + Key: f.key, + ClientSideAvailability: &f.clientSide, } _, _, err := f.helper.apiClient.FeatureFlagsApi. @@ -136,3 +130,22 @@ func (f *flagBuilder) Create() error { return nil } + +func (f *flagBuilder) logAPIError(desc string, err error) error { + var apiError ldapi.GenericOpenAPIError + if errors.As(err, &apiError) { + body := string(apiError.Body()) + f.helper.loggers.Errorf("%s: %s (response body: %s)", f.scopedOp(desc), err, body) + } else { + f.helper.loggers.Errorf("%s: %s", f.scopedOp(desc), err) + } + return err +} + +func (f *flagBuilder) scopedOp(desc string) string { + return fmt.Sprintf("%s %s in %s/%s", desc, f.key, f.projectKey, f.envKey) +} + +func (f *flagBuilder) logAPISuccess(desc string) { + f.helper.loggers.Infof(f.scopedOp(desc)) +} diff --git a/integrationtests/standard_mode_prerequisite_flags_test.go b/integrationtests/standard_mode_prerequisite_flags_test.go index e087d66b..6397dfa1 100644 --- a/integrationtests/standard_mode_prerequisite_flags_test.go +++ b/integrationtests/standard_mode_prerequisite_flags_test.go @@ -165,6 +165,40 @@ func makeFailedPrerequisites(api *scopedApiHelper) (map[string][]string, error) }, nil } +func makeIgnoreClientSideOnlyPrereqs(api *scopedApiHelper) (map[string][]string, error) { + + // flag -> prereq1, prereq2 + + if err := api.newFlag("prereq1"). + On(true).Variations(ldvalue.Bool(false), ldvalue.Bool(true)). + ClientSideUsingEnvironmentID(true). + Create(); err != nil { + return nil, err + } + + if err := api.newFlag("prereq2"). + On(true).Variations(ldvalue.Bool(false), ldvalue.Bool(true)). + ClientSideUsingEnvironmentID(false). + Create(); err != nil { + return nil, err + } + if err := api.newFlag("flag"). + On(true).Variations(ldvalue.Bool(false), ldvalue.Bool(true)). + ClientSideUsingEnvironmentID(true). + Prerequisites([]ldapi.Prerequisite{ + {Key: "prereq1", Variation: 1}, + {Key: "prereq2", Variation: 1}, + }).Create(); err != nil { + return nil, err + } + + return map[string][]string{ + "flag": {"prereq1", "prereq2"}, + "prereq1": {}, + }, nil + +} + func testStandardModeWithPrerequisites(t *testing.T, manager *integrationTestManager) { t.Run("includes top-level prerequisites", func(t *testing.T) { api, err := newScopedApiHelper(manager.apiHelper) @@ -199,4 +233,22 @@ func testStandardModeWithPrerequisites(t *testing.T, manager *integrationTestMan }) manager.verifyFlagPrerequisites(t, api.projAndEnvs(), prerequisites) }) + + t.Run("ignores client-side-only for prereq keys", func(t *testing.T) { + api, err := newScopedApiHelper(manager.apiHelper) + require.NoError(t, err) + defer api.cleanup() + + prerequisites, err := makeIgnoreClientSideOnlyPrereqs(api) + require.NoError(t, err) + + manager.startRelay(t, api.envVariables()) + defer manager.stopRelay(t) + + manager.awaitEnvironments(t, api.projAndEnvs(), nil, func(proj projectInfo, env environmentInfo) string { + return env.name + }) + + manager.verifyFlagPrerequisites(t, api.projAndEnvs(), prerequisites) + }) } diff --git a/integrationtests/test_manager_test.go b/integrationtests/test_manager_test.go index 585a8e7a..e34e6c09 100644 --- a/integrationtests/test_manager_test.go +++ b/integrationtests/test_manager_test.go @@ -7,11 +7,13 @@ import ( "encoding/base64" "encoding/json" "fmt" + "golang.org/x/exp/maps" "io" "log" "net/http" "net/url" "os" + "slices" "strings" "sync" "testing" @@ -368,7 +370,19 @@ func (m *integrationTestManager) verifyFlagPrerequisites(t *testing.T, projsAndE userJSON := `{"key":"any-user-key"}` projsAndEnvs.enumerateEnvs(func(proj projectInfo, env environmentInfo) { - prereqMap := m.getFlagPrerequisites(t, proj, env, userJSON) + prereqMap := m.getFlagPrerequisites(t, env, userJSON) + + expectedKeys := maps.Keys(prereqs) + slices.Sort(expectedKeys) + + gotKeys := prereqMap.Keys(nil) + slices.Sort(gotKeys) + + if !slices.Equal(expectedKeys, gotKeys) { + m.loggers.Errorf("Expected %v flag keys with prerequisites, but got %v", expectedKeys, gotKeys) + t.Fail() + } + for flagKey, prereqKeys := range prereqs { prereqArray := prereqMap.GetByKey(flagKey).AsValueArray() @@ -482,10 +496,12 @@ func (m *integrationTestManager) getFlagValues(t *testing.T, proj projectInfo, e return ldvalue.Null() } -func (m *integrationTestManager) getFlagPrerequisites(t *testing.T, proj projectInfo, env environmentInfo, userJSON string) ldvalue.Value { +// Note: unlike getFlagValues, this helper makes a request to a client-side endpoint, rather than the server-side +// evaluation endpoint *that returns a client side payload.* +func (m *integrationTestManager) getFlagPrerequisites(t *testing.T, env environmentInfo, userJSON string) ldvalue.Value { userBase64 := base64.URLEncoding.EncodeToString([]byte(userJSON)) - u, err := url.Parse(m.relayBaseURL + "/sdk/evalx/users/" + userBase64) + u, err := url.Parse(m.relayBaseURL + "/sdk/evalx/" + string(env.id) + "/users/" + userBase64) if err != nil { t.Fatalf("couldn't parse flag evaluation URL: %v", err) } @@ -498,7 +514,7 @@ func (m *integrationTestManager) getFlagPrerequisites(t *testing.T, proj project req, err := http.NewRequest("GET", u.String(), nil) require.NoError(t, err) - req.Header.Add("Authorization", string(env.sdkKey)) + req.Header.Add("Authorization", string(env.id)) resp, err := m.makeHTTPRequestToRelay(req) require.NoError(t, err) if assert.Equal(t, 200, resp.StatusCode, "requested flags for environment "+env.key) {