Skip to content

Commit

Permalink
add flagBuilder pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
cwaldren-ld committed Oct 16, 2024
1 parent fe7b668 commit 4b3c33c
Show file tree
Hide file tree
Showing 2 changed files with 201 additions and 36 deletions.
138 changes: 138 additions & 0 deletions integrationtests/flag_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package integrationtests

import (
"errors"
"fmt"

ldapi "github.com/launchdarkly/api-client-go/v13"
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
)

type flagBuilder struct {
key string
projectKey string
envKey string
offVariation int
fallthroughVariation int
on bool
variations []ldapi.Variation
prerequisites []ldapi.Prerequisite
helper *apiHelper

Check failure on line 20 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Benchmarks

undefined: apiHelper

Check failure on line 20 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Benchmarks

undefined: apiHelper

Check failure on line 20 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Unit Tests

undefined: apiHelper

Check failure on line 20 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Unit Tests

undefined: 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))
}

func newFlagBuilder(helper *apiHelper, flagKey string, projectKey string, envKey string) *flagBuilder {

Check failure on line 42 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Benchmarks

undefined: apiHelper

Check failure on line 42 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Benchmarks

undefined: apiHelper

Check failure on line 42 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Unit Tests

undefined: apiHelper

Check failure on line 42 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Unit Tests

undefined: apiHelper
builder := &flagBuilder{
key: flagKey,
projectKey: projectKey,
envKey: envKey,
on: true,
offVariation: 0,
fallthroughVariation: 1,
helper: helper,
}
return builder.Variations(ldvalue.Bool(false), ldvalue.Bool(true))
}

func (f *flagBuilder) Variations(variation1 ldvalue.Value, variations ...ldvalue.Value) *flagBuilder {
f.variations = nil
for _, value := range append([]ldvalue.Value{variation1}, variations...) {
valueAsInterface := value.AsArbitraryValue()
f.variations = append(f.variations, ldapi.Variation{Value: &valueAsInterface})
}
return f
}

func (f *flagBuilder) Prerequisites(prerequisites []ldapi.Prerequisite) *flagBuilder {
f.prerequisites = prerequisites
return f
}

func (f *flagBuilder) Prerequisite(prerequisiteKey string, variation int32) *flagBuilder {
return f.Prerequisites([]ldapi.Prerequisite{{Key: prerequisiteKey, Variation: variation}})
}

func (f *flagBuilder) OffVariation(v int) *flagBuilder {
f.offVariation = v
return f
}

func (f *flagBuilder) FallthroughVariation(v int) *flagBuilder {
f.fallthroughVariation = v
return f
}

func (f *flagBuilder) On(on bool) *flagBuilder {
f.on = on
return f
}

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,
}

_, _, err := f.helper.apiClient.FeatureFlagsApi.
PostFeatureFlag(f.helper.apiContext, f.projectKey).
FeatureFlagBody(flagPost).
Execute()

if err != nil {
return f.logAPIError("create flag", err)
} else {
f.logAPISuccess("create flag")
}

envPrefix := fmt.Sprintf("/environments/%s", f.envKey)
patch := ldapi.PatchWithComment{
Patch: []ldapi.PatchOperation{
makePatch("replace", envPrefix+"/offVariation", f.offVariation),

Check failure on line 119 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Benchmarks

undefined: makePatch

Check failure on line 119 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Benchmarks

undefined: makePatch

Check failure on line 119 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Unit Tests

undefined: makePatch

Check failure on line 119 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Unit Tests

undefined: makePatch
makePatch("replace", envPrefix+"/fallthrough/variation", f.fallthroughVariation),

Check failure on line 120 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Benchmarks

undefined: makePatch

Check failure on line 120 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Benchmarks

undefined: makePatch

Check failure on line 120 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Unit Tests

undefined: makePatch

Check failure on line 120 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Unit Tests

undefined: makePatch
makePatch("replace", envPrefix+"/on", f.on),

Check failure on line 121 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Benchmarks

undefined: makePatch

Check failure on line 121 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Benchmarks

undefined: makePatch

Check failure on line 121 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Unit Tests

undefined: makePatch

Check failure on line 121 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Unit Tests

undefined: makePatch
makePatch("replace", envPrefix+"/prerequisites", f.prerequisites),

Check failure on line 122 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Benchmarks

undefined: makePatch

Check failure on line 122 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Benchmarks

undefined: makePatch

Check failure on line 122 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.22.8 / Unit Tests

undefined: makePatch

Check failure on line 122 in integrationtests/flag_builder_test.go

View workflow job for this annotation

GitHub Actions / Go 1.23.2 / Unit Tests

undefined: makePatch
},
}

_, _, err = f.helper.apiClient.FeatureFlagsApi.
PatchFeatureFlag(f.helper.apiContext, f.projectKey, f.key).
PatchWithComment(patch).
Execute()

if err != nil {
return f.logAPIError("patch flag", err)
} else {
f.logAPISuccess("patch flag")
}

return nil
}
99 changes: 63 additions & 36 deletions integrationtests/standard_mode_prerequisite_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,26 @@
package integrationtests

import (
"testing"

ldapi "github.com/launchdarkly/api-client-go/v13"
"github.com/launchdarkly/go-sdk-common/v3/ldvalue"
"github.com/stretchr/testify/require"
"testing"
)

// scopedApiHelper is meant to be a wrapper around the base apiHelper which scopes operations to a single
// project/environment. It was created specifically for the prerequisite tests, since we aren't trying to verify
// assertions across projects/environments - just that prerequisites are correct within a single payload.
// This pattern could be extended or refactored into a dedicated helper package if necessary.
type scopedApiHelper struct {
project projectInfo
env environmentInfo
apiHelper *apiHelper
}

// newScopedApiHelper wraps an existing apiHelper, automatically creating a project with a single environment.
// Be sure to call cleanup() when done to delete the project, otherwise orphan projects will accumulate in the
// testing account.
func newScopedApiHelper(apiHelper *apiHelper) (*scopedApiHelper, error) {
project, envs, err := apiHelper.createProject(1)
if err != nil {
Expand All @@ -27,6 +35,8 @@ func newScopedApiHelper(apiHelper *apiHelper) (*scopedApiHelper, error) {
}, nil
}

// envVariables returns all the environment variables needed for Relay to be aware of the environment
// and authenticate with it.
func (s *scopedApiHelper) envVariables() map[string]string {
return map[string]string{
"LD_ENV_" + string(s.env.name): string(s.env.sdkKey),
Expand All @@ -35,54 +45,61 @@ func (s *scopedApiHelper) envVariables() map[string]string {
}
}

// projsAndEnvs returns a map of project -> environment, which is necessary to interact with the integration
// test manager's awaitEnvironments method.
func (s *scopedApiHelper) projAndEnvs() projsAndEnvs {
return projsAndEnvs{
s.project: {s.env},
}
}

// cleanup deletes the project and environment created by this scopedApiHelper. A common pattern in tests would be
// calling newScopedApiHelper, then deferring the cleanup call immediately after.
func (s *scopedApiHelper) cleanup() {
s.apiHelper.deleteProject(s.project)
}

func (s *scopedApiHelper) createFlagWithVariations(key string, on bool, variation1 ldvalue.Value, variation2 ldvalue.Value) error {
return s.apiHelper.createFlagWithVariations(s.project, s.env, key, on, variation1, variation2)
}

func (s *scopedApiHelper) createFlagWithPrerequisites(key string, on bool, variation1 ldvalue.Value, variation2 ldvalue.Value, prereqs []ldapi.Prerequisite) error {
return s.apiHelper.createFlagWithPrerequisites(s.project, s.env, key, on, variation1, variation2, prereqs)
// newFlag creates a new flag in the project. In LaunchDarkly, flags are created across all environments. The flag
// builder allows configuring different aspects of the flag, such as variations and prerequisites - this configuration
// is scoped to the single environment created by the scopedApiHelper.
func (s *scopedApiHelper) newFlag(key string) *flagBuilder {
return newFlagBuilder(s.apiHelper, key, s.project.key, s.env.key)
}

func makeTopLevelPrerequisites(api *scopedApiHelper) (map[string][]string, error) {

// topLevel -> directPrereq1, directPrereq2
// directPrereq1 -> indirectPrereqOf1

if err := api.createFlagWithVariations("indirectPrereqOf1", true, ldvalue.Bool(false), ldvalue.Bool(true)); err != nil {
if err := api.newFlag("indirectPrereqOf1").
On(true).
Variations(ldvalue.Bool(false), ldvalue.Bool(true)).
Create(); err != nil {
return nil, err
}

if err := api.createFlagWithPrerequisites("directPrereq1", true, ldvalue.Bool(false), ldvalue.Bool(true), []ldapi.Prerequisite{
{Key: "indirectPrereqOf1", Variation: 1},
}); err != nil {
if err := api.newFlag("directPrereq1").
On(true).
Variations(ldvalue.Bool(false), ldvalue.Bool(true)).
Prerequisite("indirectPrereqOf1", 1).
Create(); err != nil {
return nil, err
}

if err := api.createFlagWithVariations("directPrereq2", true, ldvalue.Bool(false), ldvalue.Bool(true)); err != nil {
if err := api.newFlag("directPrereq2").
On(true).
Variations(ldvalue.Bool(false), ldvalue.Bool(true)).
Create(); err != nil {
return nil, err
}

// The createFlagWithVariations call sets up two variations, with the second one being used if the flag is on.
// The test here is to see which prerequisites were evaluated for a given flag. If a prerequisite fails, the eval
// algorithm is going to short-circuit and we won't see the other prerequisite. So, we'll have two prerequisites,
// both of which are on, and both of which are satisfied. That way the evaluator will be forced to visit both,
// and we'll see the list of both when we query the eval endpoint.

if err := api.createFlagWithPrerequisites("topLevel", true, ldvalue.Bool(false), ldvalue.Bool(true),
[]ldapi.Prerequisite{
if err := api.newFlag("topLevel").
On(true).
Variations(ldvalue.Bool(false), ldvalue.Bool(true)).
Prerequisites([]ldapi.Prerequisite{
{Key: "directPrereq1", Variation: 1},
{Key: "directPrereq2", Variation: 1},
}); err != nil {
}).Create(); err != nil {
return nil, err
}

Expand All @@ -99,30 +116,43 @@ func makeFailedPrerequisites(api *scopedApiHelper) (map[string][]string, error)
// flagOn -> prereq1
// failedPrereq -> prereq1

if err := api.createFlagWithVariations("prereq1", true, ldvalue.Bool(false), ldvalue.Bool(true)); err != nil {
if err := api.newFlag("prereq1").
On(true).
Variations(ldvalue.Bool(false), ldvalue.Bool(true)).
Create(); err != nil {
return nil, err
}

if err := api.createFlagWithVariations("prereq2", true, ldvalue.Bool(false), ldvalue.Bool(true)); err != nil {
if err := api.newFlag("prereq2").
On(true).
Variations(ldvalue.Bool(false), ldvalue.Bool(true)).
Create(); err != nil {
return nil, err
}

if err := api.createFlagWithPrerequisites("flagOn", true, ldvalue.Bool(false), ldvalue.Bool(true), []ldapi.Prerequisite{
{Key: "prereq1", Variation: 1},
}); err != nil {
if err := api.newFlag("flagOn").
On(true).
Variations(ldvalue.Bool(false), ldvalue.Bool(true)).
Prerequisite("prereq1", 1).
Create(); err != nil {
return nil, err
}

if err := api.createFlagWithPrerequisites("flagOff", false, ldvalue.Bool(false), ldvalue.Bool(true), []ldapi.Prerequisite{
{Key: "prereq1", Variation: 1},
}); err != nil {
if err := api.newFlag("flagOff").
On(false).
Variations(ldvalue.Bool(false), ldvalue.Bool(true)).
Prerequisite("prereq1", 1).
Create(); err != nil {
return nil, err
}

if err := api.createFlagWithPrerequisites("failedPrereq", true, ldvalue.Bool(false), ldvalue.Bool(true), []ldapi.Prerequisite{
{Key: "prereq1", Variation: 0}, // wrong variation!
{Key: "prereq2", Variation: 1}, // correct variation, but we shouldn't see it since the first prereq failed
}); err != nil {
if err := api.newFlag("failedPrereq").
On(true).
Variations(ldvalue.Bool(false), ldvalue.Bool(true)).
Prerequisites([]ldapi.Prerequisite{
{Key: "prereq1", Variation: 0}, // wrong variation!
{Key: "prereq2", Variation: 1}, // correct variation, but we shouldn't see it since the first prereq failed
}).Create(); err != nil {
return nil, err
}

Expand All @@ -133,11 +163,8 @@ func makeFailedPrerequisites(api *scopedApiHelper) (map[string][]string, error)
"prereq1": {},
"prereq2": {},
}, nil

}

// TODO: Make a builder for the API client so that all flag options can be accessed.

func testStandardModeWithPrerequisites(t *testing.T, manager *integrationTestManager) {
t.Run("includes top-level prerequisites", func(t *testing.T) {
api, err := newScopedApiHelper(manager.apiHelper)
Expand Down

0 comments on commit 4b3c33c

Please sign in to comment.