From c9cf05a02b4882ca8266d4c8cfbbb6f5cef2d8cf Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Fri, 7 Jul 2023 14:12:09 -0700 Subject: [PATCH 1/9] basic in-memory provider Signed-off-by: Kavindu Dodanduwa --- pkg/openfeature/testing/in_memory_provider.go | 214 ++++++++++++++++++ .../testing/in_memory_provider_test.go | 167 ++++++++++++++ 2 files changed, 381 insertions(+) create mode 100644 pkg/openfeature/testing/in_memory_provider.go create mode 100644 pkg/openfeature/testing/in_memory_provider_test.go diff --git a/pkg/openfeature/testing/in_memory_provider.go b/pkg/openfeature/testing/in_memory_provider.go new file mode 100644 index 00000000..69aa4bc3 --- /dev/null +++ b/pkg/openfeature/testing/in_memory_provider.go @@ -0,0 +1,214 @@ +package testing + +import ( + "context" + "fmt" + "github.com/open-feature/go-sdk/pkg/openfeature" +) + +const ( + Enabled State = "ENABLED" + Disabled State = "DISABLED" +) + +type InMemoryProvider struct { + flags map[string]InMemoryFlag +} + +func NewInMemoryProvider(from map[string]InMemoryFlag) InMemoryProvider { + return InMemoryProvider{ + flags: from, + } +} + +func (i InMemoryProvider) Metadata() openfeature.Metadata { + return openfeature.Metadata{ + Name: "InMemoryProvider", + } +} + +func (i InMemoryProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { + memoryFlag, ok := i.flags[flag] + if !ok { + return openfeature.BoolResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), + Reason: openfeature.ErrorReason, + }, + } + } + + resolveFlag, detail := memoryFlag.Resolve(evalCtx) + + var result bool + res, ok := resolveFlag.(bool) + if ok { + result = res + } else { + result = defaultValue + } + + return openfeature.BoolResolutionDetail{ + Value: result, + ProviderResolutionDetail: detail, + } +} + +func (i InMemoryProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { + memoryFlag, ok := i.flags[flag] + if !ok { + return openfeature.StringResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), + Reason: openfeature.ErrorReason, + }, + } + } + + resolveFlag, detail := memoryFlag.Resolve(evalCtx) + + var result string + res, ok := resolveFlag.(string) + if ok { + result = res + } else { + result = defaultValue + } + + return openfeature.StringResolutionDetail{ + Value: result, + ProviderResolutionDetail: detail, + } +} + +func (i InMemoryProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { + memoryFlag, ok := i.flags[flag] + if !ok { + return openfeature.FloatResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), + Reason: openfeature.ErrorReason, + }, + } + } + + resolveFlag, detail := memoryFlag.Resolve(evalCtx) + + var result float64 + res, ok := resolveFlag.(float64) + if ok { + result = res + } else { + result = defaultValue + } + + return openfeature.FloatResolutionDetail{ + Value: result, + ProviderResolutionDetail: detail, + } +} + +func (i InMemoryProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { + memoryFlag, ok := i.flags[flag] + if !ok { + return openfeature.IntResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), + Reason: openfeature.ErrorReason, + }, + } + } + + resolveFlag, detail := memoryFlag.Resolve(evalCtx) + + var result int64 + res, ok := resolveFlag.(int64) + if ok { + result = res + } else { + result = defaultValue + } + + return openfeature.IntResolutionDetail{ + Value: result, + ProviderResolutionDetail: detail, + } +} + +func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { + memoryFlag, ok := i.flags[flag] + if !ok { + return openfeature.InterfaceResolutionDetail{ + Value: defaultValue, + ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), + Reason: openfeature.ErrorReason, + }, + } + } + + resolveFlag, detail := memoryFlag.Resolve(evalCtx) + + var result interface{} + if resolveFlag != nil { + result = resolveFlag + } else { + result = defaultValue + } + + return openfeature.InterfaceResolutionDetail{ + Value: result, + ProviderResolutionDetail: detail, + } +} + +func (i InMemoryProvider) Hooks() []openfeature.Hook { + //TODO implement some hooks + return []openfeature.Hook{} +} + +// Type Definitions for InMemoryProvider flag + +// State of the feature flag +type State string + +// ContextEvaluator is a callback to perform openfeature.EvaluationContext backed evaluations. +// This is a callback implemented by the flag definer. +type ContextEvaluator *func(this InMemoryFlag, evalCtx openfeature.FlattenedContext) (interface{}, openfeature.ProviderResolutionDetail) + +// InMemoryFlag is the feature flag representation accepted by InMemoryProvider +type InMemoryFlag struct { + Key string + State State + DefaultVariant string + Variants map[string]interface{} + ContextEvaluator ContextEvaluator +} + +func (flag *InMemoryFlag) Resolve(evalCtx openfeature.FlattenedContext) ( + interface{}, openfeature.ProviderResolutionDetail) { + + // first resolve from context callback + if flag.ContextEvaluator != nil { + return (*flag.ContextEvaluator)(*flag, evalCtx) + } + + // fallback to evaluation + + // check the state + if flag.State == Disabled { + return nil, openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewGeneralResolutionError("flag is disabled"), + Reason: openfeature.DisabledReason, + } + } + + return flag.Variants[flag.DefaultVariant], openfeature.ProviderResolutionDetail{ + Reason: openfeature.StaticReason, + Variant: flag.DefaultVariant, + } +} diff --git a/pkg/openfeature/testing/in_memory_provider_test.go b/pkg/openfeature/testing/in_memory_provider_test.go new file mode 100644 index 00000000..062f701e --- /dev/null +++ b/pkg/openfeature/testing/in_memory_provider_test.go @@ -0,0 +1,167 @@ +package testing + +import ( + "context" + "github.com/open-feature/go-sdk/pkg/openfeature" + "testing" +) + +func TestInMemoryProvider_boolean(t *testing.T) { + memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + "boolFlag": { + Key: "boolFlag", + State: Enabled, + DefaultVariant: "true", + Variants: map[string]interface{}{ + "true": true, + "false": false, + }, + ContextEvaluator: nil, + }, + }) + + ctx := context.Background() + + t.Run("test boolean success", func(t *testing.T) { + evaluation := memoryProvider.BooleanEvaluation(ctx, "boolFlag", false, nil) + + if evaluation.Value != true { + t.Errorf("incorect evaluation, expected %t, got %t", true, evaluation.Value) + } + }) +} + +func TestInMemoryProvider_String(t *testing.T) { + memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + "stringFlag": { + Key: "stringFlag", + State: Enabled, + DefaultVariant: "stringOne", + Variants: map[string]interface{}{ + "stringOne": "hello", + "stringTwo": "GoodBye", + }, + ContextEvaluator: nil, + }, + }) + + ctx := context.Background() + + t.Run("test string success", func(t *testing.T) { + evaluation := memoryProvider.StringEvaluation(ctx, "stringFlag", "none", nil) + + if evaluation.Value != "hello" { + t.Errorf("incorect evaluation, expected %s, got %s", "hello", evaluation.Value) + } + }) +} + +func TestInMemoryProvider_Float(t *testing.T) { + memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + "floatFlag": { + Key: "floatFlag", + State: Enabled, + DefaultVariant: "fOne", + Variants: map[string]interface{}{ + "fOne": 1.1, + "fTwo": 2.2, + }, + ContextEvaluator: nil, + }, + }) + + ctx := context.Background() + + t.Run("test float success", func(t *testing.T) { + evaluation := memoryProvider.FloatEvaluation(ctx, "fOne", 1.0, nil) + + if evaluation.Value != 1.0 { + t.Errorf("incorect evaluation, expected %f, got %f", 1.0, evaluation.Value) + } + }) +} + +func TestInMemoryProvider_Int(t *testing.T) { + memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + "intFlag": { + Key: "intFlag", + State: Enabled, + DefaultVariant: "one", + Variants: map[string]interface{}{ + "one": 1, + "two": 2, + }, + ContextEvaluator: nil, + }, + }) + + ctx := context.Background() + + t.Run("test integer success", func(t *testing.T) { + evaluation := memoryProvider.IntEvaluation(ctx, "IntFlag", 1, nil) + + if evaluation.Value != 1 { + t.Errorf("incorect evaluation, expected %d, got %d", 1, evaluation.Value) + } + }) +} + +func TestInMemoryProvider_Object(t *testing.T) { + memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + "objectFlag": { + Key: "objectFlag", + State: Enabled, + DefaultVariant: "A", + Variants: map[string]interface{}{ + "A": "SomeResult", + "B": "OtherResult", + }, + }, + }) + + ctx := context.Background() + + t.Run("test object success", func(t *testing.T) { + evaluation := memoryProvider.ObjectEvaluation(ctx, "objectFlag", "unknown", nil) + + if evaluation.Value != "SomeResult" { + t.Errorf("incorect evaluation, expected %v, got %v", "SomeResult", evaluation.Value) + } + }) +} + +func TestInMemoryProvider_WithContext(t *testing.T) { + var variantKey = "VariantSelector" + + // simple context handling - variant is selected from key and returned + var evaluator = func(callerFlag InMemoryFlag, evalCtx openfeature.FlattenedContext) (interface{}, openfeature.ProviderResolutionDetail) { + s := evalCtx[variantKey] + return callerFlag.Variants[s.(string)], openfeature.ProviderResolutionDetail{} + } + + memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + "contextFlag": { + Key: "contextFlag", + State: Enabled, + DefaultVariant: "true", + Variants: map[string]interface{}{ + "true": true, + "false": false, + }, + ContextEvaluator: &evaluator, + }, + }) + + ctx := context.Background() + + t.Run("test with context", func(t *testing.T) { + + evaluation := memoryProvider.BooleanEvaluation(ctx, "contextFlag", true, map[string]interface{}{ + variantKey: "false", + }) + + if evaluation.Value != false { + t.Errorf("incorect evaluation, expected %v, got %v", false, evaluation.Value) + } + }) +} From f8a8fcc787f489c71cc9c789207de63026af5a48 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Tue, 11 Jul 2023 13:05:51 -0700 Subject: [PATCH 2/9] fuzz tests to memory provider Signed-off-by: Kavindu Dodanduwa --- e2e/evaluation_fuzz_test.go | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/e2e/evaluation_fuzz_test.go b/e2e/evaluation_fuzz_test.go index d7f3c910..c46a4cd8 100644 --- a/e2e/evaluation_fuzz_test.go +++ b/e2e/evaluation_fuzz_test.go @@ -2,29 +2,21 @@ package e2e_test import ( "context" + "github.com/open-feature/go-sdk/pkg/openfeature" + mp "github.com/open-feature/go-sdk/pkg/openfeature/testing" "strings" "testing" - "time" - - flagd "github.com/open-feature/go-sdk-contrib/providers/flagd/pkg" - "github.com/open-feature/go-sdk/pkg/openfeature" ) func setupFuzzClient(f *testing.F) *openfeature.Client { f.Helper() - provider := flagd.NewProvider(flagd.WithPort(8013), flagd.WithoutCache()) - err := openfeature.SetProvider(provider) + memoryProvider := mp.NewInMemoryProvider(map[string]mp.InMemoryFlag{}) + err := openfeature.SetProvider(memoryProvider) if err != nil { f.Errorf("error setting up provider %v", err) } - select { - case <-provider.IsReady(): - case <-time.After(500 * time.Millisecond): - f.Fatal("provider not ready after 500 milliseconds") - } - return openfeature.NewClient("fuzzing") } From 22595ccfcc94bef5e98dbae87b36541fb1f3ace7 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Tue, 11 Jul 2023 16:02:51 -0700 Subject: [PATCH 3/9] migrating evaluation tests Signed-off-by: Kavindu Dodanduwa --- e2e/evaluation_test.go | 898 +++++++++++++++++- pkg/openfeature/testing/in_memory_provider.go | 14 +- .../testing/in_memory_provider_test.go | 10 +- 3 files changed, 911 insertions(+), 11 deletions(-) diff --git a/e2e/evaluation_test.go b/e2e/evaluation_test.go index 26b32540..9bc4e31e 100644 --- a/e2e/evaluation_test.go +++ b/e2e/evaluation_test.go @@ -1,12 +1,150 @@ package e2e_test import ( - "testing" - + "context" + "errors" + "fmt" "github.com/cucumber/godog" - e2e "github.com/open-feature/go-sdk-contrib/tests/flagd/pkg/integration" + "github.com/open-feature/go-sdk/pkg/openfeature" + mp "github.com/open-feature/go-sdk/pkg/openfeature/testing" + "strconv" + "testing" ) +var client = openfeature.NewClient("evaluation tests") + +var ctxFunction = func(this mp.InMemoryFlag, evalCtx openfeature.FlattenedContext) ( + interface{}, openfeature.ProviderResolutionDetail) { + + defaultValue := this.Variants[this.DefaultVariant] + defaultResolution := openfeature.ProviderResolutionDetail{ + Reason: openfeature.DefaultReason, + Variant: this.DefaultVariant, + } + + // check for expected properties + fn, ok := evalCtx["fn"].(string) + if !ok { + return defaultValue, defaultResolution + } + + if fn != "Sulisław" { + return defaultValue, defaultResolution + } + + ln, ok := evalCtx["ln"].(string) + if !ok { + return defaultValue, defaultResolution + } + + if ln != "Świętopełk" { + return defaultValue, defaultResolution + } + + age, ok := evalCtx["age"].(int64) + if !ok { + return defaultValue, defaultResolution + } + + if age != 29 { + return defaultValue, defaultResolution + } + + customer, ok := evalCtx["customer"].(bool) + if !ok { + return defaultValue, defaultResolution + } + + if customer != false { + return defaultValue, defaultResolution + } + + return this.Variants["internal"], openfeature.ProviderResolutionDetail{ + Reason: openfeature.TargetingMatchReason, + Variant: "internal", + } +} + +var memoryFlags = map[string]mp.InMemoryFlag{ + "boolean-flag": { + Key: "boolean-flag", + State: mp.Enabled, + DefaultVariant: "on", + Variants: map[string]interface{}{ + "on": true, + "off": false, + }, + ContextEvaluator: nil, + }, + "string-flag": { + Key: "string-flag", + State: mp.Enabled, + DefaultVariant: "greeting", + Variants: map[string]interface{}{ + "greeting": "hi", + "parting": "bye", + }, + ContextEvaluator: nil, + }, + "integer-flag": { + Key: "integer-flag", + State: mp.Enabled, + DefaultVariant: "ten", + Variants: map[string]interface{}{ + "one": 1, + "ten": 10, + }, + ContextEvaluator: nil, + }, + "float-flag": { + Key: "float-flag", + State: mp.Enabled, + DefaultVariant: "half", + Variants: map[string]interface{}{ + "tenth": 0.1, + "half": 0.5, + }, + ContextEvaluator: nil, + }, + "object-flag": { + Key: "object-flag", + State: mp.Enabled, + DefaultVariant: "template", + Variants: map[string]interface{}{ + "empty": map[string]interface{}{}, + "template": map[string]interface{}{ + "showImages": true, + "title": "Check out these pics!", + "imagesPerPage": 100, + }, + }, + ContextEvaluator: nil, + }, + "wrong-flag": { + Key: "wrong-flag", + State: mp.Enabled, + DefaultVariant: "one", + Variants: map[string]interface{}{ + "one": "uno", + "two": "dos", + }, + ContextEvaluator: nil, + }, + "context-aware": { + Key: "context-aware", + State: mp.Enabled, + DefaultVariant: "external", + Variants: map[string]interface{}{ + "internal": "INTERNAL", + "external": "EXTERNAL", + }, + ContextEvaluator: &ctxFunction, + }, +} + +// ctxStorageKey is the key used to pass test data across context.Context +type ctxStorageKey struct{} + func TestEvaluation(t *testing.T) { if testing.Short() { t.Skip() @@ -14,7 +152,7 @@ func TestEvaluation(t *testing.T) { suite := godog.TestSuite{ Name: "evaluation.feature", - ScenarioInitializer: e2e.InitializeEvaluationScenario(), + ScenarioInitializer: initializeEvaluationScenario, Options: &godog.Options{ Format: "pretty", Paths: []string{"../test-harness/features/evaluation.feature"}, @@ -26,3 +164,755 @@ func TestEvaluation(t *testing.T) { t.Fatal("non-zero status returned, failed to run evaluation tests") } } + +func initializeEvaluationScenario(ctx *godog.ScenarioContext) { + // setup provider + ctx.Step(`^a provider is registered with cache disabled$`, aProviderIsRegisteredWithCacheDisabled) + + // basic evaluations + ctx.Step(`^a boolean flag with key "([^"]*)" is evaluated with default value "([^"]*)"$`, aBooleanFlagWithKeyIsEvaluatedWithDefaultValue) + ctx.Step(`^the resolved boolean value should be "([^"]*)"$`, theResolvedBooleanValueShouldBe) + + ctx.Step(`^a string flag with key "([^"]*)" is evaluated with default value "([^"]*)"$`, aStringFlagWithKeyIsEvaluatedWithDefaultValue) + ctx.Step(`^the resolved string value should be "([^"]*)"$`, theResolvedStringValueShouldBe) + + ctx.Step(`^an integer flag with key "([^"]*)" is evaluated with default value (\d+)$`, anIntegerFlagWithKeyIsEvaluatedWithDefaultValue) + ctx.Step(`^the resolved integer value should be (\d+)$`, theResolvedIntegerValueShouldBe) + + ctx.Step(`^a float flag with key "([^"]*)" is evaluated with default value (\-*\d+\.\d+)$`, aFloatFlagWithKeyIsEvaluatedWithDefaultValue) + ctx.Step(`^the resolved float value should be (\-*\d+\.\d+)$`, theResolvedFloatValueShouldBe) + + ctx.Step(`^an object flag with key "([^"]*)" is evaluated with a null default value$`, anObjectFlagWithKeyIsEvaluatedWithANullDefaultValue) + ctx.Step(`^the resolved object value should be contain fields "([^"]*)", "([^"]*)", and "([^"]*)", with values "([^"]*)", "([^"]*)" and (\d+), respectively$`, theResolvedObjectValueShouldBeContainFieldsAndWithValuesAndRespectively) + + // evaluation with variant and reason validation + ctx.Step(`^a boolean flag with key "([^"]*)" is evaluated with details and default value "([^"]*)"$`, aBooleanFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue) + ctx.Step(`^the resolved boolean details value should be "([^"]*)", the variant should be "([^"]*)", and the reason should be "([^"]*)"$`, theResolvedBooleanDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe) + + ctx.Step(`^a string flag with key "([^"]*)" is evaluated with details and default value "([^"]*)"$`, aStringFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue) + ctx.Step(`^the resolved string details value should be "([^"]*)", the variant should be "([^"]*)", and the reason should be "([^"]*)"$`, theResolvedStringDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe) + + ctx.Step(`^an integer flag with key "([^"]*)" is evaluated with details and default value (\d+)$`, anIntegerFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue) + ctx.Step(`^the resolved integer details value should be (\d+), the variant should be "([^"]*)", and the reason should be "([^"]*)"$`, theResolvedIntegerDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe) + + ctx.Step(`^a float flag with key "([^"]*)" is evaluated with details and default value (\-*\d+\.\d+)$`, aFloatFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue) + ctx.Step(`^the resolved float details value should be (\-*\d+\.\d+), the variant should be "([^"]*)", and the reason should be "([^"]*)"$`, theResolvedFloatDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe) + + ctx.Step(`^an object flag with key "([^"]*)" is evaluated with details and a null default value$`, anObjectFlagWithKeyIsEvaluatedWithDetailsAndANullDefaultValue) + ctx.Step(`^the resolved object details value should be contain fields "([^"]*)", "([^"]*)", and "([^"]*)", with values "([^"]*)", "([^"]*)" and (\d+), respectively$`, theResolvedObjectDetailsValueShouldBeContainFieldsAndWithValuesAndRespectively) + ctx.Step(`^the variant should be "([^"]*)", and the reason should be "([^"]*)"$`, theVariantShouldBeAndTheReasonShouldBe) + + // context based evaluation + + ctx.Step(`^context contains keys "([^"]*)", "([^"]*)", "([^"]*)", "([^"]*)" with values "([^"]*)", "([^"]*)", (\d+), "([^"]*)"$`, contextContainsKeysWithValues) + ctx.Step(`^a flag with key "([^"]*)" is evaluated with default value "([^"]*)"$`, aFlagWithKeyIsEvaluatedWithDefaultValue) + ctx.Step(`^the resolved string response should be "([^"]*)"$`, theResolvedStringResponseShouldBe) + ctx.Step(`^the resolved flag value is "([^"]*)" when the context is empty$`, theResolvedFlagValueIsWhenTheContextIsEmpty) + + // scenarios + + ctx.Step(`^a non-existent string flag with key "([^"]*)" is evaluated with details and a default value "([^"]*)"$`, aNonexistentStringFlagWithKeyIsEvaluatedWithDetailsAndADefaultValue) + ctx.Step(`^the default string value should be returned$`, theDefaultStringValueShouldBeReturned) + ctx.Step(`^the reason should indicate an error and the error code should indicate a missing flag with "([^"]*)"$`, theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateAMissingFlagWith) + + ctx.Step(`^a string flag with key "([^"]*)" is evaluated as an integer, with details and a default value (\d+)$`, aStringFlagWithKeyIsEvaluatedAsAnIntegerWithDetailsAndADefaultValue) + ctx.Step(`^the default integer value should be returned$`, theDefaultIntegerValueShouldBeReturned) + ctx.Step(`^the reason should indicate an error and the error code should indicate a type mismatch with "([^"]*)"$`, theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateATypeMismatchWith) +} + +func aProviderIsRegisteredWithCacheDisabled(ctx context.Context) error { + memoryProvider := mp.NewInMemoryProvider(memoryFlags) + + err := openfeature.SetProvider(memoryProvider) + if err != nil { + return err + } + + return nil +} + +func aBooleanFlagWithKeyIsEvaluatedWithDefaultValue( + ctx context.Context, flagKey, defaultValueStr string, +) (context.Context, error) { + defaultValue, err := strconv.ParseBool(defaultValueStr) + if err != nil { + return ctx, errors.New("default value must be of type bool") + } + + got, err := client.BooleanValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + + return context.WithValue(ctx, ctxStorageKey{}, got), nil +} + +func theResolvedBooleanValueShouldBe(ctx context.Context, expectedValueStr string) error { + expectedValue, err := strconv.ParseBool(expectedValueStr) + if err != nil { + return errors.New("expected value must be of type bool") + } + + got, ok := ctx.Value(ctxStorageKey{}).(bool) + if !ok { + return errors.New("no flag resolution result") + } + + if got != expectedValue { + return fmt.Errorf("expected resolved boolean value to be %t, got %t", expectedValue, got) + } + + return nil +} + +func aStringFlagWithKeyIsEvaluatedWithDefaultValue( + ctx context.Context, flagKey, defaultValue string, +) (context.Context, error) { + got, err := client.StringValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + + return context.WithValue(ctx, ctxStorageKey{}, got), nil +} + +func theResolvedStringValueShouldBe(ctx context.Context, expectedValue string) error { + got, ok := ctx.Value(ctxStorageKey{}).(string) + if !ok { + return errors.New("no flag resolution result") + } + + if got != expectedValue { + return fmt.Errorf("expected resolved string value to be %s, got %s", expectedValue, got) + } + + return nil +} + +func anIntegerFlagWithKeyIsEvaluatedWithDefaultValue( + ctx context.Context, flagKey string, defaultValue int64, +) (context.Context, error) { + got, err := client.IntValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + + return context.WithValue(ctx, ctxStorageKey{}, got), nil +} + +func theResolvedIntegerValueShouldBe(ctx context.Context, expectedValue int64) error { + got, ok := ctx.Value(ctxStorageKey{}).(int64) + if !ok { + return errors.New("no flag resolution result") + } + + if got != expectedValue { + return fmt.Errorf("expected resolved int value to be %d, got %d", expectedValue, got) + } + + return nil +} + +func aFloatFlagWithKeyIsEvaluatedWithDefaultValue( + ctx context.Context, flagKey string, defaultValue float64, +) (context.Context, error) { + got, err := client.FloatValue(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + + return context.WithValue(ctx, ctxStorageKey{}, got), nil +} + +func theResolvedFloatValueShouldBe(ctx context.Context, expectedValue float64) error { + got, ok := ctx.Value(ctxStorageKey{}).(float64) + if !ok { + return errors.New("no flag resolution result") + } + + if got != expectedValue { + return fmt.Errorf("expected resolved int value to be %f, got %f", expectedValue, got) + } + + return nil +} + +func anObjectFlagWithKeyIsEvaluatedWithANullDefaultValue(ctx context.Context, flagKey string) (context.Context, error) { + got, err := client.ObjectValue(ctx, flagKey, nil, openfeature.EvaluationContext{}) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + + return context.WithValue(ctx, ctxStorageKey{}, got), nil +} + +func theResolvedObjectValueShouldBeContainFieldsAndWithValuesAndRespectively( + ctx context.Context, field1, field2, field3, value1, value2 string, value3 int) error { + + got, ok := ctx.Value(ctxStorageKey{}).(map[string]interface{}) + if !ok { + return errors.New("no flag resolution result") + } + + // field1 - showImages + showImage, ok := got[field1].(bool) + if !ok { + return fmt.Errorf("expected boolean for key %s", field1) + } + + parseBool, err := strconv.ParseBool(value1) + if err != nil { + return err + } + + if showImage != parseBool { + return fmt.Errorf("incorrect comparison for boolean") + } + + // field2 - title + title, ok := got[field2].(string) + if !ok { + return fmt.Errorf("expected string for key %s", field2) + } + + if title != value2 { + return fmt.Errorf("incorrect comparison for string") + } + + // field3 - imagesPerPage + imagesPerPage, ok := got[field3].(int) + if !ok { + return fmt.Errorf("expected int for key %s", field3) + } + + if imagesPerPage != value3 { + return fmt.Errorf("incorrect comparison for int") + } + + return nil +} + +func aBooleanFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( + ctx context.Context, flagKey string, defaultValueStr string, +) (context.Context, error) { + defaultValue, err := strconv.ParseBool(defaultValueStr) + if err != nil { + return ctx, errors.New("default value must be of type bool") + } + + got, err := client.BooleanValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.BooleanEvaluationDetails) + if !ok { + store = make(map[string]openfeature.BooleanEvaluationDetails) + } + + store[flagKey] = got + + return context.WithValue(ctx, ctxStorageKey{}, store), nil +} + +func theResolvedBooleanDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe( + ctx context.Context, valueStr, variant, reason string, +) error { + value, err := strconv.ParseBool(valueStr) + if err != nil { + return errors.New("value must be of type bool") + } + + got, err := getFirstBooleanEvaluationDetails(ctx) + if err != nil { + return err + } + + if got.Value != value { + return fmt.Errorf("expected value to be %t, got %t", value, got.Value) + } + if got.Variant != variant { + return fmt.Errorf("expected variant to be %s, got %s", variant, got.Variant) + } + if string(got.Reason) != reason { + return fmt.Errorf("expected reason to be %s, got %s", reason, got.Reason) + } + + return nil +} + +func aStringFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( + ctx context.Context, flagKey, defaultValue string, +) (context.Context, error) { + got, err := client.StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.StringEvaluationDetails) + if !ok { + store = make(map[string]openfeature.StringEvaluationDetails) + } + + store[flagKey] = got + + return context.WithValue(ctx, ctxStorageKey{}, store), nil +} + +func theResolvedStringDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe( + ctx context.Context, value, variant, reason string, +) error { + got, err := getFirstStringEvaluationDetails(ctx) + if err != nil { + return err + } + + if got.Value != value { + return fmt.Errorf("expected value to be %s, got %s", value, got.Value) + } + if got.Variant != variant { + return fmt.Errorf("expected variant to be %s, got %s", variant, got.Variant) + } + if string(got.Reason) != reason { + return fmt.Errorf("expected reason to be %s, got %s", reason, got.Reason) + } + + return nil +} + +func anIntegerFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( + ctx context.Context, flagKey string, defaultValue int64, +) (context.Context, error) { + got, err := client.IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.IntEvaluationDetails) + if !ok { + store = make(map[string]openfeature.IntEvaluationDetails) + } + + store[flagKey] = got + + return context.WithValue(ctx, ctxStorageKey{}, store), nil +} + +func theResolvedIntegerDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe( + ctx context.Context, value int64, variant, reason string, +) error { + got, err := getFirstIntegerEvaluationDetails(ctx) + if err != nil { + return err + } + + if got.Value != value { + return fmt.Errorf("expected value to be %d, got %d", value, got.Value) + } + if got.Variant != variant { + return fmt.Errorf("expected variant to be %s, got %s", variant, got.Variant) + } + if string(got.Reason) != reason { + return fmt.Errorf("expected reason to be %s, got %s", reason, got.Reason) + } + + return nil +} + +func aFloatFlagWithKeyIsEvaluatedWithDetailsAndDefaultValue( + ctx context.Context, flagKey string, defaultValue float64, +) (context.Context, error) { + got, err := client.FloatValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.FloatEvaluationDetails) + if !ok { + store = make(map[string]openfeature.FloatEvaluationDetails) + } + + store[flagKey] = got + + return context.WithValue(ctx, ctxStorageKey{}, store), nil +} + +func theResolvedFloatDetailsValueShouldBeTheVariantShouldBeAndTheReasonShouldBe( + ctx context.Context, value float64, variant, reason string, +) error { + got, err := getFirstFloatEvaluationDetails(ctx) + if err != nil { + return err + } + + if got.Value != value { + return fmt.Errorf("expected value to be %f, got %f", value, got.Value) + } + if got.Variant != variant { + return fmt.Errorf("expected variant to be %s, got %s", variant, got.Variant) + } + if string(got.Reason) != reason { + return fmt.Errorf("expected reason to be %s, got %s", reason, got.Reason) + } + + return nil +} + +func anObjectFlagWithKeyIsEvaluatedWithDetailsAndANullDefaultValue( + ctx context.Context, flagKey string, +) (context.Context, error) { + got, err := client.ObjectValueDetails(ctx, flagKey, nil, openfeature.EvaluationContext{}) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.InterfaceEvaluationDetails) + if !ok { + store = make(map[string]openfeature.InterfaceEvaluationDetails) + } + + store[flagKey] = got + + return context.WithValue(ctx, ctxStorageKey{}, store), nil +} + +func theResolvedObjectDetailsValueShouldBeContainFieldsAndWithValuesAndRespectively( + ctx context.Context, field1, field2, field3, value1, value2 string, value3 int) error { + + gotResDetail, err := getFirstInterfaceEvaluationDetails(ctx) + if err != nil { + return err + } + + content, ok := gotResDetail.Value.(map[string]interface{}) + if !ok { + return errors.New("unexpected value format") + } + + // field1 - showImages + showImage, ok := content[field1].(bool) + if !ok { + return fmt.Errorf("expected boolean for key %s", field1) + } + + parseBool, err := strconv.ParseBool(value1) + if err != nil { + return err + } + + if showImage != parseBool { + return fmt.Errorf("incorrect comparison for boolean") + } + + // field2 - title + title, ok := content[field2].(string) + if !ok { + return fmt.Errorf("expected string for key %s", field2) + } + + if title != value2 { + return fmt.Errorf("incorrect comparison for string") + } + + // field3 - imagesPerPage + imagesPerPage, ok := content[field3].(int) + if !ok { + return fmt.Errorf("expected int for key %s", field3) + } + + if imagesPerPage != value3 { + return fmt.Errorf("incorrect comparison for int") + } + + return nil +} + +func theVariantShouldBeAndTheReasonShouldBe(ctx context.Context, variant, reason string) error { + got, err := getFirstInterfaceEvaluationDetails(ctx) + if err != nil { + return err + } + + if got.Variant != variant { + return fmt.Errorf("expected variant to be %s, got %s", variant, got.Variant) + } + if string(got.Reason) != reason { + return fmt.Errorf("expected reason to be %s, got %s", reason, got.Reason) + } + + return nil +} + +func contextContainsKeysWithValues( + ctx context.Context, ctxKey1, ctxKey2, ctxKey3, ctxKey4, ctxValue1, ctxValue2 string, ctxValue3 int64, ctxValue4 string, +) (context.Context, error) { + evalCtx := openfeature.NewEvaluationContext("", map[string]interface{}{ + ctxKey1: boolOrString(ctxValue1), + ctxKey2: boolOrString(ctxValue2), + ctxKey3: ctxValue3, + ctxKey4: boolOrString(ctxValue4), + }) + + data := contextAwareEvaluationData{ + evaluationContext: evalCtx, + } + + return context.WithValue(ctx, ctxStorageKey{}, data), nil +} + +func aFlagWithKeyIsEvaluatedWithDefaultValue( + ctx context.Context, flagKey, defaultValue string, +) (context.Context, error) { + ctxAwareEvalData, ok := ctx.Value(ctxStorageKey{}).(contextAwareEvaluationData) + if !ok { + return ctx, errors.New("no contextAwareEvaluationData found") + } + + got, err := client.StringValue(ctx, flagKey, defaultValue, ctxAwareEvalData.evaluationContext) + if err != nil { + return ctx, fmt.Errorf("openfeature client: %w", err) + } + ctxAwareEvalData.flagKey = flagKey + ctxAwareEvalData.defaultValue = defaultValue + ctxAwareEvalData.response = got + + return context.WithValue(ctx, ctxStorageKey{}, ctxAwareEvalData), nil +} + +func theResolvedStringResponseShouldBe(ctx context.Context, expectedResponse string) (context.Context, error) { + ctxAwareEvalData, ok := ctx.Value(ctxStorageKey{}).(contextAwareEvaluationData) + if !ok { + return ctx, errors.New("no contextAwareEvaluationData found") + } + + if ctxAwareEvalData.response != expectedResponse { + return ctx, fmt.Errorf("expected response of '%s', got '%s'", expectedResponse, ctxAwareEvalData.response) + } + + return ctx, nil +} + +func theResolvedFlagValueIsWhenTheContextIsEmpty(ctx context.Context, expectedResponse string) error { + ctxAwareEvalData, ok := ctx.Value(ctxStorageKey{}).(contextAwareEvaluationData) + if !ok { + return errors.New("no contextAwareEvaluationData found") + } + + got, err := client.StringValue( + ctx, ctxAwareEvalData.flagKey, ctxAwareEvalData.defaultValue, openfeature.EvaluationContext{}, + ) + if err != nil { + return fmt.Errorf("openfeature client: %w", err) + } + + if got != expectedResponse { + return fmt.Errorf("expected response of '%s', got '%s'", expectedResponse, got) + } + + return nil +} + +func aNonexistentStringFlagWithKeyIsEvaluatedWithDetailsAndADefaultValue( + ctx context.Context, flagKey, defaultValue string, +) (context.Context, error) { + got, err := client.StringValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + + return context.WithValue(ctx, ctxStorageKey{}, stringFlagNotFoundData{ + evalDetails: got, + defaultValue: defaultValue, + err: err, + }), nil +} + +func theDefaultStringValueShouldBeReturned(ctx context.Context) (context.Context, error) { + strNotFoundData, ok := ctx.Value(ctxStorageKey{}).(stringFlagNotFoundData) + if !ok { + return ctx, errors.New("no stringFlagNotFoundData found") + } + + if strNotFoundData.evalDetails.Value != strNotFoundData.defaultValue { + return ctx, fmt.Errorf( + "expected default value '%s', got '%s'", + strNotFoundData.defaultValue, strNotFoundData.evalDetails.Value, + ) + } + + return ctx, nil +} + +func theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateAMissingFlagWith( + ctx context.Context, errorCode string, +) error { + strNotFoundData, ok := ctx.Value(ctxStorageKey{}).(stringFlagNotFoundData) + if !ok { + return errors.New("no stringFlagNotFoundData found") + } + + if strNotFoundData.evalDetails.Reason != openfeature.ErrorReason { + return fmt.Errorf( + "expected reason '%s', got '%s'", + openfeature.ErrorReason, strNotFoundData.evalDetails.Reason, + ) + } + + if string(strNotFoundData.evalDetails.ErrorCode) != errorCode { + return fmt.Errorf( + "expected error code '%s', got '%s'", + errorCode, strNotFoundData.evalDetails.ErrorCode, + ) + } + + if strNotFoundData.err == nil { + return errors.New("expected flag evaluation to return an error, got nil") + } + + return nil +} + +func aStringFlagWithKeyIsEvaluatedAsAnIntegerWithDetailsAndADefaultValue( + ctx context.Context, flagKey string, defaultValue int64, +) (context.Context, error) { + got, err := client.IntValueDetails(ctx, flagKey, defaultValue, openfeature.EvaluationContext{}) + + return context.WithValue(ctx, ctxStorageKey{}, typeErrorData{ + evalDetails: got, + defaultValue: defaultValue, + err: err, + }), nil +} + +func theDefaultIntegerValueShouldBeReturned(ctx context.Context) (context.Context, error) { + typeErrData, ok := ctx.Value(ctxStorageKey{}).(typeErrorData) + if !ok { + return ctx, errors.New("no typeErrorData found") + } + + if typeErrData.evalDetails.Value != typeErrData.defaultValue { + return ctx, fmt.Errorf( + "expected default value %d, got %d", + typeErrData.defaultValue, typeErrData.evalDetails.Value, + ) + } + + return ctx, nil +} + +func theReasonShouldIndicateAnErrorAndTheErrorCodeShouldIndicateATypeMismatchWith( + ctx context.Context, expectedErrorCode string, +) error { + typeErrData, ok := ctx.Value(ctxStorageKey{}).(typeErrorData) + if !ok { + return errors.New("no typeErrorData found") + } + + if typeErrData.evalDetails.Reason != openfeature.ErrorReason { + return fmt.Errorf( + "expected reason '%s', got '%s'", + openfeature.ErrorReason, typeErrData.evalDetails.Reason, + ) + } + + if typeErrData.evalDetails.ErrorCode != openfeature.TypeMismatchCode { + return fmt.Errorf( + "expected error code '%s', got '%s'", + openfeature.TypeMismatchCode, typeErrData.evalDetails.ErrorCode, + ) + } + + return nil +} + +// helpers + +type contextAwareEvaluationData struct { + flagKey string + defaultValue string + evaluationContext openfeature.EvaluationContext + response string +} + +type stringFlagNotFoundData struct { + evalDetails openfeature.StringEvaluationDetails + defaultValue string + err error +} + +type typeErrorData struct { + evalDetails openfeature.IntEvaluationDetails + defaultValue int64 + err error +} + +func getFirstBooleanEvaluationDetails(ctx context.Context) (openfeature.BooleanEvaluationDetails, error) { + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.BooleanEvaluationDetails) + if !ok { + return openfeature.BooleanEvaluationDetails{}, errors.New("no flag resolution result") + } + + for _, evalDetails := range store { + return evalDetails, nil + } + + return openfeature.BooleanEvaluationDetails{}, errors.New("no evaluation detail found in context") +} + +func getFirstStringEvaluationDetails(ctx context.Context) (openfeature.StringEvaluationDetails, error) { + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.StringEvaluationDetails) + if !ok { + return openfeature.StringEvaluationDetails{}, errors.New("no flag resolution result") + } + + for _, evalDetails := range store { + return evalDetails, nil + } + + return openfeature.StringEvaluationDetails{}, errors.New("no evaluation detail found in context") +} + +func getFirstIntegerEvaluationDetails(ctx context.Context) (openfeature.IntEvaluationDetails, error) { + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.IntEvaluationDetails) + if !ok { + return openfeature.IntEvaluationDetails{}, errors.New("no flag resolution result") + } + + for _, evalDetails := range store { + return evalDetails, nil + } + + return openfeature.IntEvaluationDetails{}, errors.New("no evaluation detail found in context") +} + +func getFirstFloatEvaluationDetails(ctx context.Context) (openfeature.FloatEvaluationDetails, error) { + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.FloatEvaluationDetails) + if !ok { + return openfeature.FloatEvaluationDetails{}, errors.New("no flag resolution result") + } + + for _, evalDetails := range store { + return evalDetails, nil + } + + return openfeature.FloatEvaluationDetails{}, errors.New("no evaluation detail found in context") +} + +func getFirstInterfaceEvaluationDetails(ctx context.Context) (openfeature.InterfaceEvaluationDetails, error) { + store, ok := ctx.Value(ctxStorageKey{}).(map[string]openfeature.InterfaceEvaluationDetails) + if !ok { + return openfeature.InterfaceEvaluationDetails{}, errors.New("no flag resolution result") + } + + for _, evalDetails := range store { + return evalDetails, nil + } + + return openfeature.InterfaceEvaluationDetails{}, errors.New("no evaluation detail found in context") +} + +func boolOrString(str string) interface{} { + boolean, err := strconv.ParseBool(str) + if err != nil { + return str + } + + return boolean +} diff --git a/pkg/openfeature/testing/in_memory_provider.go b/pkg/openfeature/testing/in_memory_provider.go index 69aa4bc3..28c6e940 100644 --- a/pkg/openfeature/testing/in_memory_provider.go +++ b/pkg/openfeature/testing/in_memory_provider.go @@ -47,6 +47,8 @@ func (i InMemoryProvider) BooleanEvaluation(ctx context.Context, flag string, de result = res } else { result = defaultValue + detail.Reason = openfeature.ErrorReason + detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") } return openfeature.BoolResolutionDetail{ @@ -75,6 +77,8 @@ func (i InMemoryProvider) StringEvaluation(ctx context.Context, flag string, def result = res } else { result = defaultValue + detail.Reason = openfeature.ErrorReason + detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") } return openfeature.StringResolutionDetail{ @@ -103,6 +107,8 @@ func (i InMemoryProvider) FloatEvaluation(ctx context.Context, flag string, defa result = res } else { result = defaultValue + detail.Reason = openfeature.ErrorReason + detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") } return openfeature.FloatResolutionDetail{ @@ -126,11 +132,13 @@ func (i InMemoryProvider) IntEvaluation(ctx context.Context, flag string, defaul resolveFlag, detail := memoryFlag.Resolve(evalCtx) var result int64 - res, ok := resolveFlag.(int64) + res, ok := resolveFlag.(int) if ok { - result = res + result = int64(res) } else { result = defaultValue + detail.Reason = openfeature.ErrorReason + detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") } return openfeature.IntResolutionDetail{ @@ -158,6 +166,8 @@ func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, def result = resolveFlag } else { result = defaultValue + detail.Reason = openfeature.ErrorReason + detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") } return openfeature.InterfaceResolutionDetail{ diff --git a/pkg/openfeature/testing/in_memory_provider_test.go b/pkg/openfeature/testing/in_memory_provider_test.go index 062f701e..54e9d4db 100644 --- a/pkg/openfeature/testing/in_memory_provider_test.go +++ b/pkg/openfeature/testing/in_memory_provider_test.go @@ -86,10 +86,10 @@ func TestInMemoryProvider_Int(t *testing.T) { "intFlag": { Key: "intFlag", State: Enabled, - DefaultVariant: "one", + DefaultVariant: "max", Variants: map[string]interface{}{ - "one": 1, - "two": 2, + "min": -9223372036854775808, + "max": 9223372036854775807, }, ContextEvaluator: nil, }, @@ -98,9 +98,9 @@ func TestInMemoryProvider_Int(t *testing.T) { ctx := context.Background() t.Run("test integer success", func(t *testing.T) { - evaluation := memoryProvider.IntEvaluation(ctx, "IntFlag", 1, nil) + evaluation := memoryProvider.IntEvaluation(ctx, "intFlag", 1, nil) - if evaluation.Value != 1 { + if evaluation.Value != 9223372036854775807 { t.Errorf("incorect evaluation, expected %d, got %d", 1, evaluation.Value) } }) From d73843c46bd59135640af519ebbd5684465ffb2f Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Wed, 12 Jul 2023 10:41:45 -0700 Subject: [PATCH 4/9] restructure and remove caching tests Signed-off-by: Kavindu Dodanduwa --- e2e/caching_test.go | 36 ----------- e2e/common_test.go | 136 +++++++++++++++++++++++++++++++++++++++++ e2e/evaluation_test.go | 129 -------------------------------------- 3 files changed, 136 insertions(+), 165 deletions(-) delete mode 100644 e2e/caching_test.go create mode 100644 e2e/common_test.go diff --git a/e2e/caching_test.go b/e2e/caching_test.go deleted file mode 100644 index ba80c17c..00000000 --- a/e2e/caching_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package e2e_test - -import ( - "testing" - - "github.com/open-feature/go-sdk-contrib/tests/flagd/pkg/integration" - - "github.com/cucumber/godog" -) - -const flagConfigurationPath = "../test-harness/testing-flags.json" - -func TestCaching(t *testing.T) { - if testing.Short() { - t.Skip() - } - - initializeCachingScenario, err := integration.InitializeCachingScenario(flagConfigurationPath) - if err != nil { - t.Fatal(err) - } - - suite := godog.TestSuite{ - Name: "caching.feature", - ScenarioInitializer: initializeCachingScenario, - Options: &godog.Options{ - Format: "pretty", - Paths: []string{"../test-harness/features/caching.feature"}, - TestingT: t, // Testing instance that will run subtests. - }, - } - - if suite.Run() != 0 { - t.Fatal("non-zero status returned, failed to run caching tests") - } -} diff --git a/e2e/common_test.go b/e2e/common_test.go new file mode 100644 index 00000000..5ff62b76 --- /dev/null +++ b/e2e/common_test.go @@ -0,0 +1,136 @@ +package e2e_test + +import ( + "github.com/open-feature/go-sdk/pkg/openfeature" + mp "github.com/open-feature/go-sdk/pkg/openfeature/testing" +) + +// ctxFunction is a context based evaluation callback +var ctxFunction = func(this mp.InMemoryFlag, evalCtx openfeature.FlattenedContext) ( + interface{}, openfeature.ProviderResolutionDetail) { + + defaultValue := this.Variants[this.DefaultVariant] + defaultResolution := openfeature.ProviderResolutionDetail{ + Reason: openfeature.DefaultReason, + Variant: this.DefaultVariant, + } + + // check for expected properties + fn, ok := evalCtx["fn"].(string) + if !ok { + return defaultValue, defaultResolution + } + + if fn != "Sulisław" { + return defaultValue, defaultResolution + } + + ln, ok := evalCtx["ln"].(string) + if !ok { + return defaultValue, defaultResolution + } + + if ln != "Świętopełk" { + return defaultValue, defaultResolution + } + + age, ok := evalCtx["age"].(int64) + if !ok { + return defaultValue, defaultResolution + } + + if age != 29 { + return defaultValue, defaultResolution + } + + customer, ok := evalCtx["customer"].(bool) + if !ok { + return defaultValue, defaultResolution + } + + if customer != false { + return defaultValue, defaultResolution + } + + return this.Variants["internal"], openfeature.ProviderResolutionDetail{ + Reason: openfeature.TargetingMatchReason, + Variant: "internal", + } +} + +var memoryFlags = map[string]mp.InMemoryFlag{ + "boolean-flag": { + Key: "boolean-flag", + State: mp.Enabled, + DefaultVariant: "on", + Variants: map[string]interface{}{ + "on": true, + "off": false, + }, + ContextEvaluator: nil, + }, + "string-flag": { + Key: "string-flag", + State: mp.Enabled, + DefaultVariant: "greeting", + Variants: map[string]interface{}{ + "greeting": "hi", + "parting": "bye", + }, + ContextEvaluator: nil, + }, + "integer-flag": { + Key: "integer-flag", + State: mp.Enabled, + DefaultVariant: "ten", + Variants: map[string]interface{}{ + "one": 1, + "ten": 10, + }, + ContextEvaluator: nil, + }, + "float-flag": { + Key: "float-flag", + State: mp.Enabled, + DefaultVariant: "half", + Variants: map[string]interface{}{ + "tenth": 0.1, + "half": 0.5, + }, + ContextEvaluator: nil, + }, + "object-flag": { + Key: "object-flag", + State: mp.Enabled, + DefaultVariant: "template", + Variants: map[string]interface{}{ + "empty": map[string]interface{}{}, + "template": map[string]interface{}{ + "showImages": true, + "title": "Check out these pics!", + "imagesPerPage": 100, + }, + }, + ContextEvaluator: nil, + }, + "wrong-flag": { + Key: "wrong-flag", + State: mp.Enabled, + DefaultVariant: "one", + Variants: map[string]interface{}{ + "one": "uno", + "two": "dos", + }, + ContextEvaluator: nil, + }, + "context-aware": { + Key: "context-aware", + State: mp.Enabled, + DefaultVariant: "external", + Variants: map[string]interface{}{ + "internal": "INTERNAL", + "external": "EXTERNAL", + }, + ContextEvaluator: &ctxFunction, + }, +} diff --git a/e2e/evaluation_test.go b/e2e/evaluation_test.go index 9bc4e31e..b3e7863a 100644 --- a/e2e/evaluation_test.go +++ b/e2e/evaluation_test.go @@ -13,135 +13,6 @@ import ( var client = openfeature.NewClient("evaluation tests") -var ctxFunction = func(this mp.InMemoryFlag, evalCtx openfeature.FlattenedContext) ( - interface{}, openfeature.ProviderResolutionDetail) { - - defaultValue := this.Variants[this.DefaultVariant] - defaultResolution := openfeature.ProviderResolutionDetail{ - Reason: openfeature.DefaultReason, - Variant: this.DefaultVariant, - } - - // check for expected properties - fn, ok := evalCtx["fn"].(string) - if !ok { - return defaultValue, defaultResolution - } - - if fn != "Sulisław" { - return defaultValue, defaultResolution - } - - ln, ok := evalCtx["ln"].(string) - if !ok { - return defaultValue, defaultResolution - } - - if ln != "Świętopełk" { - return defaultValue, defaultResolution - } - - age, ok := evalCtx["age"].(int64) - if !ok { - return defaultValue, defaultResolution - } - - if age != 29 { - return defaultValue, defaultResolution - } - - customer, ok := evalCtx["customer"].(bool) - if !ok { - return defaultValue, defaultResolution - } - - if customer != false { - return defaultValue, defaultResolution - } - - return this.Variants["internal"], openfeature.ProviderResolutionDetail{ - Reason: openfeature.TargetingMatchReason, - Variant: "internal", - } -} - -var memoryFlags = map[string]mp.InMemoryFlag{ - "boolean-flag": { - Key: "boolean-flag", - State: mp.Enabled, - DefaultVariant: "on", - Variants: map[string]interface{}{ - "on": true, - "off": false, - }, - ContextEvaluator: nil, - }, - "string-flag": { - Key: "string-flag", - State: mp.Enabled, - DefaultVariant: "greeting", - Variants: map[string]interface{}{ - "greeting": "hi", - "parting": "bye", - }, - ContextEvaluator: nil, - }, - "integer-flag": { - Key: "integer-flag", - State: mp.Enabled, - DefaultVariant: "ten", - Variants: map[string]interface{}{ - "one": 1, - "ten": 10, - }, - ContextEvaluator: nil, - }, - "float-flag": { - Key: "float-flag", - State: mp.Enabled, - DefaultVariant: "half", - Variants: map[string]interface{}{ - "tenth": 0.1, - "half": 0.5, - }, - ContextEvaluator: nil, - }, - "object-flag": { - Key: "object-flag", - State: mp.Enabled, - DefaultVariant: "template", - Variants: map[string]interface{}{ - "empty": map[string]interface{}{}, - "template": map[string]interface{}{ - "showImages": true, - "title": "Check out these pics!", - "imagesPerPage": 100, - }, - }, - ContextEvaluator: nil, - }, - "wrong-flag": { - Key: "wrong-flag", - State: mp.Enabled, - DefaultVariant: "one", - Variants: map[string]interface{}{ - "one": "uno", - "two": "dos", - }, - ContextEvaluator: nil, - }, - "context-aware": { - Key: "context-aware", - State: mp.Enabled, - DefaultVariant: "external", - Variants: map[string]interface{}{ - "internal": "INTERNAL", - "external": "EXTERNAL", - }, - ContextEvaluator: &ctxFunction, - }, -} - // ctxStorageKey is the key used to pass test data across context.Context type ctxStorageKey struct{} From 299f4a93d908d298078f3db03572e61354f0cc7b Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Wed, 12 Jul 2023 11:29:21 -0700 Subject: [PATCH 5/9] documentation and workflows Signed-off-by: Kavindu Dodanduwa --- .github/workflows/pr-checks.yml | 2 -- CONTRIBUTING.md | 27 +++++++++++++-------------- Makefile | 6 ++---- e2e/README.md | 5 +++++ 4 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 e2e/README.md diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 2dcd55f9..cc538dfe 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -44,8 +44,6 @@ jobs: uses: actions/checkout@v3 with: submodules: recursive - - name: Run flagd-testbed - run: docker run -d -p 8013:8013 -v ${{ github.workspace }}/test-harness/testing-flags.json:/testing-flags.json ghcr.io/open-feature/flagd-testbed:v0.2.2 - name: Setup Environment run: | echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70a31106..faf11074 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,16 +34,15 @@ Run unit tests with `make test`. #### End-to-End tests -The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features) using the [flagd provider](https://github.com/open-feature/go-sdk-contrib/tree/main/providers/flagd), [flagd](https://github.com/open-feature/flagd) and [the flagd test module](https://github.com/open-feature/go-sdk-contrib/tree/main/tests/flagd). +The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/test-harness/blob/main/features). + If you'd like to run them locally, first pull the `test-harness` git submodule + ``` git submodule update --init --recursive ``` -then start the flagd testbed with -``` -docker run -p 8013:8013 -v $PWD/test-harness/testing-flags.json:/testing-flags.json ghcr.io/open-feature/flagd-testbed:latest -``` - and finally run + +and run tests with, ``` make e2e-test ``` @@ -51,20 +50,20 @@ make e2e-test #### Fuzzing [Go supports fuzzing natively as of 1.18](https://go.dev/security/fuzz/). -The fuzzing suite is implemented as an integration of `go-sdk` with the [flagd provider](https://github.com/open-feature/go-sdk-contrib/tree/main/providers/flagd) and [flagd](https://github.com/open-feature/flagd). -The fuzzing tests are found in [./integration/evaluation_fuzz_test.go](./integration/evaluation_fuzz_test.go), they are dependent on the flagd testbed running, you can start it with -``` -docker run -p 8013:8013 ghcr.io/open-feature/flagd-testbed:latest -``` -then, to execute a fuzzing test, run the following +The fuzzing suite is implemented as an integration of `go-sdk`. +The fuzzing tests are found in [./integration/evaluation_fuzz_test.go](./e2e/evaluation_fuzz_test.go). + + +To execute a fuzzing test, run the following ``` -go test -fuzz=FuzzBooleanEvaluation ./integration/evaluation_fuzz_test.go +go test -fuzz=FuzzBooleanEvaluation ./e2e/evaluation_fuzz_test.go ``` substituting the name of the fuzz as appropriate. ### Releases -This repo uses Release Please to release packages. Release Please sets up a running PR that tracks all changes for the library components, and maintains the versions according to conventional commits, generated when PRs are merged. When Release Please's running PR is merged, any changed artifacts are published. +This repo uses Release Please to release packages. Release Please set up a running PR that tracks all changes for the library components, and maintains the versions according to conventional commits, generated when PRs are merged. +When Release Please PR is merged, any changed artifacts will be published. ## Contacting us diff --git a/Makefile b/Makefile index dbc0ee73..2d9aabf8 100644 --- a/Makefile +++ b/Makefile @@ -4,10 +4,8 @@ mockgen: mockgen -source=pkg/openfeature/hooks.go -destination=pkg/openfeature/hooks_mock_test.go -package=openfeature test: go test --short -cover ./... -e2e-test: # dependent on: docker run -p 8013:8013 -v $PWD/test-harness/testing-flags.json:/testing-flags.json ghcr.io/open-feature/flagd-testbed:latest - go test -cover ./... - cd test-harness; git restore testing-flags.json; cd .. # reset testing-flags.json - +e2e-test: + go test -race -cover ./e2e/... lint: go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest ${GOPATH}/bin/golangci-lint run --deadline=3m --timeout=3m ./... # Run linters diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000..86623312 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,5 @@ +## end-to-end tests + +This folder contains e2e tests for go-sdk. Tests written here rely on `InMemoryProvider` to perform flag evaluations. +Some tests require `test-harness` Git submodule and use behaviour driven tests defined with [Gherkin](https://cucumber.io/docs/gherkin/reference/) syntax. + From 2d9ec680222d4f5a72da4482d9fe48a274bf4dac Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Wed, 12 Jul 2023 12:44:19 -0700 Subject: [PATCH 6/9] generic resolving and tests Signed-off-by: Kavindu Dodanduwa --- pkg/openfeature/testing/in_memory_provider.go | 90 +++++++------------ .../testing/in_memory_provider_test.go | 80 +++++++++++++++++ 2 files changed, 114 insertions(+), 56 deletions(-) diff --git a/pkg/openfeature/testing/in_memory_provider.go b/pkg/openfeature/testing/in_memory_provider.go index 28c6e940..0be33ac7 100644 --- a/pkg/openfeature/testing/in_memory_provider.go +++ b/pkg/openfeature/testing/in_memory_provider.go @@ -39,17 +39,8 @@ func (i InMemoryProvider) BooleanEvaluation(ctx context.Context, flag string, de } } - resolveFlag, detail := memoryFlag.Resolve(evalCtx) - - var result bool - res, ok := resolveFlag.(bool) - if ok { - result = res - } else { - result = defaultValue - detail.Reason = openfeature.ErrorReason - detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") - } + resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) + result := genericResolve[bool](resolveFlag, defaultValue, &detail) return openfeature.BoolResolutionDetail{ Value: result, @@ -69,17 +60,8 @@ func (i InMemoryProvider) StringEvaluation(ctx context.Context, flag string, def } } - resolveFlag, detail := memoryFlag.Resolve(evalCtx) - - var result string - res, ok := resolveFlag.(string) - if ok { - result = res - } else { - result = defaultValue - detail.Reason = openfeature.ErrorReason - detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") - } + resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) + result := genericResolve[string](resolveFlag, defaultValue, &detail) return openfeature.StringResolutionDetail{ Value: result, @@ -99,17 +81,8 @@ func (i InMemoryProvider) FloatEvaluation(ctx context.Context, flag string, defa } } - resolveFlag, detail := memoryFlag.Resolve(evalCtx) - - var result float64 - res, ok := resolveFlag.(float64) - if ok { - result = res - } else { - result = defaultValue - detail.Reason = openfeature.ErrorReason - detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") - } + resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) + result := genericResolve[float64](resolveFlag, defaultValue, &detail) return openfeature.FloatResolutionDetail{ Value: result, @@ -129,20 +102,11 @@ func (i InMemoryProvider) IntEvaluation(ctx context.Context, flag string, defaul } } - resolveFlag, detail := memoryFlag.Resolve(evalCtx) - - var result int64 - res, ok := resolveFlag.(int) - if ok { - result = int64(res) - } else { - result = defaultValue - detail.Reason = openfeature.ErrorReason - detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") - } + resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) + result := genericResolve[int](resolveFlag, int(defaultValue), &detail) return openfeature.IntResolutionDetail{ - Value: result, + Value: int64(result), ProviderResolutionDetail: detail, } } @@ -159,7 +123,7 @@ func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, def } } - resolveFlag, detail := memoryFlag.Resolve(evalCtx) + resolveFlag, detail := memoryFlag.Resolve(defaultValue, evalCtx) var result interface{} if resolveFlag != nil { @@ -177,10 +141,24 @@ func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, def } func (i InMemoryProvider) Hooks() []openfeature.Hook { - //TODO implement some hooks return []openfeature.Hook{} } +// helpers + +// genericResolve is a helper to extract type verified evaluation and fill openfeature.ProviderResolutionDetail +func genericResolve[T comparable](value interface{}, defaultValue T, detail *openfeature.ProviderResolutionDetail) T { + v, ok := value.(T) + + if ok { + return v + } + + detail.Reason = openfeature.ErrorReason + detail.ResolutionError = openfeature.NewTypeMismatchResolutionError("incorrect type association") + return defaultValue +} + // Type Definitions for InMemoryProvider flag // State of the feature flag @@ -199,24 +177,24 @@ type InMemoryFlag struct { ContextEvaluator ContextEvaluator } -func (flag *InMemoryFlag) Resolve(evalCtx openfeature.FlattenedContext) ( +func (flag *InMemoryFlag) Resolve(defaultValue interface{}, evalCtx openfeature.FlattenedContext) ( interface{}, openfeature.ProviderResolutionDetail) { - // first resolve from context callback - if flag.ContextEvaluator != nil { - return (*flag.ContextEvaluator)(*flag, evalCtx) - } - - // fallback to evaluation - // check the state if flag.State == Disabled { - return nil, openfeature.ProviderResolutionDetail{ + return defaultValue, openfeature.ProviderResolutionDetail{ ResolutionError: openfeature.NewGeneralResolutionError("flag is disabled"), Reason: openfeature.DisabledReason, } } + // first resolve from context callback + if flag.ContextEvaluator != nil { + return (*flag.ContextEvaluator)(*flag, evalCtx) + } + + // fallback to evaluation + return flag.Variants[flag.DefaultVariant], openfeature.ProviderResolutionDetail{ Reason: openfeature.StaticReason, Variant: flag.DefaultVariant, diff --git a/pkg/openfeature/testing/in_memory_provider_test.go b/pkg/openfeature/testing/in_memory_provider_test.go index 54e9d4db..a2d09de7 100644 --- a/pkg/openfeature/testing/in_memory_provider_test.go +++ b/pkg/openfeature/testing/in_memory_provider_test.go @@ -165,3 +165,83 @@ func TestInMemoryProvider_WithContext(t *testing.T) { } }) } + +func TestInMemoryProvider_MissingFlag(t *testing.T) { + memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{}) + + ctx := context.Background() + + t.Run("test missing flag", func(t *testing.T) { + evaluation := memoryProvider.StringEvaluation(ctx, "missing-flag", "GoodBye", nil) + + if evaluation.Value != "GoodBye" { + t.Errorf("incorect evaluation, expected %v, got %v", "SomeResult", evaluation.Value) + } + + if evaluation.Reason != openfeature.ErrorReason { + t.Errorf("incorect reason, expected %v, got %v", openfeature.ErrorReason, evaluation.Reason) + } + + if evaluation.ResolutionDetail().ErrorCode != openfeature.FlagNotFoundCode { + t.Errorf("incorect reason, expected %v, got %v", openfeature.ErrorReason, evaluation.ResolutionDetail().ErrorCode) + } + }) +} + +func TestInMemoryProvider_TypeMismatch(t *testing.T) { + memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + "boolFlag": { + Key: "boolFlag", + State: Enabled, + DefaultVariant: "true", + Variants: map[string]interface{}{ + "true": true, + "false": false, + }, + ContextEvaluator: nil, + }, + }) + + ctx := context.Background() + + t.Run("test missing flag", func(t *testing.T) { + evaluation := memoryProvider.StringEvaluation(ctx, "boolFlag", "GoodBye", nil) + + if evaluation.Value != "GoodBye" { + t.Errorf("incorect evaluation, expected %v, got %v", "SomeResult", evaluation.Value) + } + + if evaluation.ResolutionDetail().ErrorCode != openfeature.TypeMismatchCode { + t.Errorf("incorect reason, expected %v, got %v", openfeature.ErrorReason, evaluation.Reason) + } + }) +} + +func TestInMemoryProvider_Disabled(t *testing.T) { + memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{ + "boolFlag": { + Key: "boolFlag", + State: Disabled, + DefaultVariant: "true", + Variants: map[string]interface{}{ + "true": true, + "false": false, + }, + ContextEvaluator: nil, + }, + }) + + ctx := context.Background() + + t.Run("test missing flag", func(t *testing.T) { + evaluation := memoryProvider.BooleanEvaluation(ctx, "boolFlag", false, nil) + + if evaluation.Value != false { + t.Errorf("incorect evaluation, expected %v, got %v", false, evaluation.Value) + } + + if evaluation.Reason != openfeature.DisabledReason { + t.Errorf("incorect reason, expected %v, got %v", openfeature.ErrorReason, evaluation.Reason) + } + }) +} From 4d651dabc5c517d1f9c0c1fe3d471bde1f118373 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Wed, 12 Jul 2023 13:52:59 -0700 Subject: [PATCH 7/9] improvements Signed-off-by: Kavindu Dodanduwa --- e2e/common_test.go | 42 +++----------- pkg/openfeature/testing/in_memory_provider.go | 58 +++++++++---------- 2 files changed, 37 insertions(+), 63 deletions(-) diff --git a/e2e/common_test.go b/e2e/common_test.go index 5ff62b76..46898aa8 100644 --- a/e2e/common_test.go +++ b/e2e/common_test.go @@ -15,41 +15,17 @@ var ctxFunction = func(this mp.InMemoryFlag, evalCtx openfeature.FlattenedContex Variant: this.DefaultVariant, } - // check for expected properties - fn, ok := evalCtx["fn"].(string) - if !ok { - return defaultValue, defaultResolution + expects := openfeature.FlattenedContext{ + "fn": "Sulisław", + "ln": "Świętopełk", + "age": int64(29), + "customer": false, } - if fn != "Sulisław" { - return defaultValue, defaultResolution - } - - ln, ok := evalCtx["ln"].(string) - if !ok { - return defaultValue, defaultResolution - } - - if ln != "Świętopełk" { - return defaultValue, defaultResolution - } - - age, ok := evalCtx["age"].(int64) - if !ok { - return defaultValue, defaultResolution - } - - if age != 29 { - return defaultValue, defaultResolution - } - - customer, ok := evalCtx["customer"].(bool) - if !ok { - return defaultValue, defaultResolution - } - - if customer != false { - return defaultValue, defaultResolution + for k, v := range expects { + if v != evalCtx[k] { + return defaultValue, defaultResolution + } } return this.Variants["internal"], openfeature.ProviderResolutionDetail{ diff --git a/pkg/openfeature/testing/in_memory_provider.go b/pkg/openfeature/testing/in_memory_provider.go index 0be33ac7..8ad037bf 100644 --- a/pkg/openfeature/testing/in_memory_provider.go +++ b/pkg/openfeature/testing/in_memory_provider.go @@ -28,14 +28,11 @@ func (i InMemoryProvider) Metadata() openfeature.Metadata { } func (i InMemoryProvider) BooleanEvaluation(ctx context.Context, flag string, defaultValue bool, evalCtx openfeature.FlattenedContext) openfeature.BoolResolutionDetail { - memoryFlag, ok := i.flags[flag] + memoryFlag, details, ok := i.find(flag) if !ok { return openfeature.BoolResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ - ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), - Reason: openfeature.ErrorReason, - }, + Value: defaultValue, + ProviderResolutionDetail: *details, } } @@ -49,14 +46,11 @@ func (i InMemoryProvider) BooleanEvaluation(ctx context.Context, flag string, de } func (i InMemoryProvider) StringEvaluation(ctx context.Context, flag string, defaultValue string, evalCtx openfeature.FlattenedContext) openfeature.StringResolutionDetail { - memoryFlag, ok := i.flags[flag] + memoryFlag, details, ok := i.find(flag) if !ok { return openfeature.StringResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ - ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), - Reason: openfeature.ErrorReason, - }, + Value: defaultValue, + ProviderResolutionDetail: *details, } } @@ -70,14 +64,11 @@ func (i InMemoryProvider) StringEvaluation(ctx context.Context, flag string, def } func (i InMemoryProvider) FloatEvaluation(ctx context.Context, flag string, defaultValue float64, evalCtx openfeature.FlattenedContext) openfeature.FloatResolutionDetail { - memoryFlag, ok := i.flags[flag] + memoryFlag, details, ok := i.find(flag) if !ok { return openfeature.FloatResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ - ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), - Reason: openfeature.ErrorReason, - }, + Value: defaultValue, + ProviderResolutionDetail: *details, } } @@ -91,14 +82,11 @@ func (i InMemoryProvider) FloatEvaluation(ctx context.Context, flag string, defa } func (i InMemoryProvider) IntEvaluation(ctx context.Context, flag string, defaultValue int64, evalCtx openfeature.FlattenedContext) openfeature.IntResolutionDetail { - memoryFlag, ok := i.flags[flag] + memoryFlag, details, ok := i.find(flag) if !ok { return openfeature.IntResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ - ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), - Reason: openfeature.ErrorReason, - }, + Value: defaultValue, + ProviderResolutionDetail: *details, } } @@ -112,14 +100,11 @@ func (i InMemoryProvider) IntEvaluation(ctx context.Context, flag string, defaul } func (i InMemoryProvider) ObjectEvaluation(ctx context.Context, flag string, defaultValue interface{}, evalCtx openfeature.FlattenedContext) openfeature.InterfaceResolutionDetail { - memoryFlag, ok := i.flags[flag] + memoryFlag, details, ok := i.find(flag) if !ok { return openfeature.InterfaceResolutionDetail{ - Value: defaultValue, - ProviderResolutionDetail: openfeature.ProviderResolutionDetail{ - ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), - Reason: openfeature.ErrorReason, - }, + Value: defaultValue, + ProviderResolutionDetail: *details, } } @@ -144,6 +129,19 @@ func (i InMemoryProvider) Hooks() []openfeature.Hook { return []openfeature.Hook{} } +func (i InMemoryProvider) find(flag string) (*InMemoryFlag, *openfeature.ProviderResolutionDetail, bool) { + memoryFlag, ok := i.flags[flag] + if !ok { + return nil, + &openfeature.ProviderResolutionDetail{ + ResolutionError: openfeature.NewFlagNotFoundResolutionError(fmt.Sprintf("flag for key %s not found", flag)), + Reason: openfeature.ErrorReason, + }, false + } + + return &memoryFlag, nil, true +} + // helpers // genericResolve is a helper to extract type verified evaluation and fill openfeature.ProviderResolutionDetail From 6f84b9b153581c38f72bee5810e4cc0e7c0b55fa Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Thu, 13 Jul 2023 12:51:31 -0700 Subject: [PATCH 8/9] correct and enhance tests Signed-off-by: Kavindu Dodanduwa --- .../testing/in_memory_provider_test.go | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pkg/openfeature/testing/in_memory_provider_test.go b/pkg/openfeature/testing/in_memory_provider_test.go index a2d09de7..2a4ba66b 100644 --- a/pkg/openfeature/testing/in_memory_provider_test.go +++ b/pkg/openfeature/testing/in_memory_provider_test.go @@ -73,10 +73,10 @@ func TestInMemoryProvider_Float(t *testing.T) { ctx := context.Background() t.Run("test float success", func(t *testing.T) { - evaluation := memoryProvider.FloatEvaluation(ctx, "fOne", 1.0, nil) + evaluation := memoryProvider.FloatEvaluation(ctx, "floatFlag", 1.0, nil) - if evaluation.Value != 1.0 { - t.Errorf("incorect evaluation, expected %f, got %f", 1.0, evaluation.Value) + if evaluation.Value != 1.1 { + t.Errorf("incorect evaluation, expected %f, got %f", 1.1, evaluation.Value) } }) } @@ -204,7 +204,7 @@ func TestInMemoryProvider_TypeMismatch(t *testing.T) { ctx := context.Background() - t.Run("test missing flag", func(t *testing.T) { + t.Run("test type mismatch flag", func(t *testing.T) { evaluation := memoryProvider.StringEvaluation(ctx, "boolFlag", "GoodBye", nil) if evaluation.Value != "GoodBye" { @@ -233,7 +233,7 @@ func TestInMemoryProvider_Disabled(t *testing.T) { ctx := context.Background() - t.Run("test missing flag", func(t *testing.T) { + t.Run("test disabled flag", func(t *testing.T) { evaluation := memoryProvider.BooleanEvaluation(ctx, "boolFlag", false, nil) if evaluation.Value != false { @@ -245,3 +245,17 @@ func TestInMemoryProvider_Disabled(t *testing.T) { } }) } + +func TestInMemoryProvider_Metadata(t *testing.T) { + memoryProvider := NewInMemoryProvider(map[string]InMemoryFlag{}) + + metadata := memoryProvider.Metadata() + + if metadata.Name == "" { + t.Errorf("expected non-empty name for in-memory provider") + } + + if metadata.Name != "InMemoryProvider" { + t.Errorf("incorrect name for in-memory provider") + } +} From b2d2084a97190370898ba6b3ec61ced1ca55f130 Mon Sep 17 00:00:00 2001 From: Kavindu Dodanduwa Date: Mon, 17 Jul 2023 12:19:08 -0700 Subject: [PATCH 9/9] packaging and comparison update Signed-off-by: Kavindu Dodanduwa --- e2e/common_test.go | 20 +++++++++---------- e2e/evaluation_fuzz_test.go | 4 ++-- e2e/evaluation_test.go | 4 ++-- pkg/openfeature/event_executor.go | 9 +++++---- pkg/openfeature/memprovider/README.md | 5 +++++ .../in_memory_provider.go | 2 +- .../in_memory_provider_test.go | 2 +- 7 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 pkg/openfeature/memprovider/README.md rename pkg/openfeature/{testing => memprovider}/in_memory_provider.go (99%) rename pkg/openfeature/{testing => memprovider}/in_memory_provider_test.go (99%) diff --git a/e2e/common_test.go b/e2e/common_test.go index 46898aa8..60875d2f 100644 --- a/e2e/common_test.go +++ b/e2e/common_test.go @@ -2,11 +2,11 @@ package e2e_test import ( "github.com/open-feature/go-sdk/pkg/openfeature" - mp "github.com/open-feature/go-sdk/pkg/openfeature/testing" + "github.com/open-feature/go-sdk/pkg/openfeature/memprovider" ) // ctxFunction is a context based evaluation callback -var ctxFunction = func(this mp.InMemoryFlag, evalCtx openfeature.FlattenedContext) ( +var ctxFunction = func(this memprovider.InMemoryFlag, evalCtx openfeature.FlattenedContext) ( interface{}, openfeature.ProviderResolutionDetail) { defaultValue := this.Variants[this.DefaultVariant] @@ -34,10 +34,10 @@ var ctxFunction = func(this mp.InMemoryFlag, evalCtx openfeature.FlattenedContex } } -var memoryFlags = map[string]mp.InMemoryFlag{ +var memoryFlags = map[string]memprovider.InMemoryFlag{ "boolean-flag": { Key: "boolean-flag", - State: mp.Enabled, + State: memprovider.Enabled, DefaultVariant: "on", Variants: map[string]interface{}{ "on": true, @@ -47,7 +47,7 @@ var memoryFlags = map[string]mp.InMemoryFlag{ }, "string-flag": { Key: "string-flag", - State: mp.Enabled, + State: memprovider.Enabled, DefaultVariant: "greeting", Variants: map[string]interface{}{ "greeting": "hi", @@ -57,7 +57,7 @@ var memoryFlags = map[string]mp.InMemoryFlag{ }, "integer-flag": { Key: "integer-flag", - State: mp.Enabled, + State: memprovider.Enabled, DefaultVariant: "ten", Variants: map[string]interface{}{ "one": 1, @@ -67,7 +67,7 @@ var memoryFlags = map[string]mp.InMemoryFlag{ }, "float-flag": { Key: "float-flag", - State: mp.Enabled, + State: memprovider.Enabled, DefaultVariant: "half", Variants: map[string]interface{}{ "tenth": 0.1, @@ -77,7 +77,7 @@ var memoryFlags = map[string]mp.InMemoryFlag{ }, "object-flag": { Key: "object-flag", - State: mp.Enabled, + State: memprovider.Enabled, DefaultVariant: "template", Variants: map[string]interface{}{ "empty": map[string]interface{}{}, @@ -91,7 +91,7 @@ var memoryFlags = map[string]mp.InMemoryFlag{ }, "wrong-flag": { Key: "wrong-flag", - State: mp.Enabled, + State: memprovider.Enabled, DefaultVariant: "one", Variants: map[string]interface{}{ "one": "uno", @@ -101,7 +101,7 @@ var memoryFlags = map[string]mp.InMemoryFlag{ }, "context-aware": { Key: "context-aware", - State: mp.Enabled, + State: memprovider.Enabled, DefaultVariant: "external", Variants: map[string]interface{}{ "internal": "INTERNAL", diff --git a/e2e/evaluation_fuzz_test.go b/e2e/evaluation_fuzz_test.go index c46a4cd8..c6a199d6 100644 --- a/e2e/evaluation_fuzz_test.go +++ b/e2e/evaluation_fuzz_test.go @@ -3,7 +3,7 @@ package e2e_test import ( "context" "github.com/open-feature/go-sdk/pkg/openfeature" - mp "github.com/open-feature/go-sdk/pkg/openfeature/testing" + "github.com/open-feature/go-sdk/pkg/openfeature/memprovider" "strings" "testing" ) @@ -11,7 +11,7 @@ import ( func setupFuzzClient(f *testing.F) *openfeature.Client { f.Helper() - memoryProvider := mp.NewInMemoryProvider(map[string]mp.InMemoryFlag{}) + memoryProvider := memprovider.NewInMemoryProvider(map[string]memprovider.InMemoryFlag{}) err := openfeature.SetProvider(memoryProvider) if err != nil { f.Errorf("error setting up provider %v", err) diff --git a/e2e/evaluation_test.go b/e2e/evaluation_test.go index b3e7863a..99c38b88 100644 --- a/e2e/evaluation_test.go +++ b/e2e/evaluation_test.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/cucumber/godog" "github.com/open-feature/go-sdk/pkg/openfeature" - mp "github.com/open-feature/go-sdk/pkg/openfeature/testing" + "github.com/open-feature/go-sdk/pkg/openfeature/memprovider" "strconv" "testing" ) @@ -92,7 +92,7 @@ func initializeEvaluationScenario(ctx *godog.ScenarioContext) { } func aProviderIsRegisteredWithCacheDisabled(ctx context.Context) error { - memoryProvider := mp.NewInMemoryProvider(memoryFlags) + memoryProvider := memprovider.NewInMemoryProvider(memoryFlags) err := openfeature.SetProvider(memoryProvider) if err != nil { diff --git a/pkg/openfeature/event_executor.go b/pkg/openfeature/event_executor.go index 281c4621..6db3d628 100644 --- a/pkg/openfeature/event_executor.go +++ b/pkg/openfeature/event_executor.go @@ -2,6 +2,7 @@ package openfeature import ( "fmt" + "reflect" "sync" "time" @@ -260,7 +261,7 @@ func (e *eventExecutor) startListeningAndShutdownOld(newProvider providerReferen // drop from active references for i, r := range e.activeSubscriptions { - if r == oldReference { + if reflect.DeepEqual(oldReference.featureProvider, r.featureProvider) { e.activeSubscriptions = append(e.activeSubscriptions[:i], e.activeSubscriptions[i+1:]...) } } @@ -355,7 +356,7 @@ func (e *eventExecutor) executeHandler(f func(details EventDetails), event Event // isRunning is a helper till we bump to the latest go version with slices.contains support func isRunning(provider providerReference, activeProviders []providerReference) bool { for _, activeProvider := range activeProviders { - if provider.featureProvider == activeProvider.featureProvider { + if reflect.DeepEqual(activeProvider.featureProvider, provider.featureProvider) { return true } } @@ -365,12 +366,12 @@ func isRunning(provider providerReference, activeProviders []providerReference) // isRunning is a helper to check if given provider is already in use func isBound(provider providerReference, defaultProvider providerReference, namedProviders []providerReference) bool { - if provider.featureProvider == defaultProvider.featureProvider { + if reflect.DeepEqual(provider.featureProvider, defaultProvider.featureProvider) { return true } for _, namedProvider := range namedProviders { - if provider.featureProvider == namedProvider.featureProvider { + if reflect.DeepEqual(provider.featureProvider, namedProvider.featureProvider) { return true } } diff --git a/pkg/openfeature/memprovider/README.md b/pkg/openfeature/memprovider/README.md new file mode 100644 index 00000000..8751e849 --- /dev/null +++ b/pkg/openfeature/memprovider/README.md @@ -0,0 +1,5 @@ +# In-memory provider + +`InMemoryProvider` is an OpenFeature compliant provider implementation with an in-memory flag storage. + +While the main usage of this provider is SDK testing, you may use it for minimal OpenFeature use cases where appropriate. \ No newline at end of file diff --git a/pkg/openfeature/testing/in_memory_provider.go b/pkg/openfeature/memprovider/in_memory_provider.go similarity index 99% rename from pkg/openfeature/testing/in_memory_provider.go rename to pkg/openfeature/memprovider/in_memory_provider.go index 8ad037bf..81bcf5b2 100644 --- a/pkg/openfeature/testing/in_memory_provider.go +++ b/pkg/openfeature/memprovider/in_memory_provider.go @@ -1,4 +1,4 @@ -package testing +package memprovider import ( "context" diff --git a/pkg/openfeature/testing/in_memory_provider_test.go b/pkg/openfeature/memprovider/in_memory_provider_test.go similarity index 99% rename from pkg/openfeature/testing/in_memory_provider_test.go rename to pkg/openfeature/memprovider/in_memory_provider_test.go index 2a4ba66b..48224d04 100644 --- a/pkg/openfeature/testing/in_memory_provider_test.go +++ b/pkg/openfeature/memprovider/in_memory_provider_test.go @@ -1,4 +1,4 @@ -package testing +package memprovider import ( "context"