diff --git a/optimizely/client/client.go b/optimizely/client/client.go index c0e5c7e7e..0e8a500c0 100644 --- a/optimizely/client/client.go +++ b/optimizely/client/client.go @@ -22,6 +22,7 @@ import ( "fmt" "reflect" "runtime/debug" + "strconv" "github.com/optimizely/go-sdk/optimizely/event" @@ -78,6 +79,7 @@ func (o *OptimizelyClient) IsFeatureEnabled(featureKey string, userContext entit userID := userContext.ID logger.Debug(fmt.Sprintf(`Evaluating feature "%s" for user "%s".`, featureKey, userID)) featureDecision, err := o.decisionService.GetFeatureDecision(featureDecisionContext, userContext) + if err != nil { logger.Error("Received an error while computing feature decision", err) return result, err @@ -133,6 +135,157 @@ func (o *OptimizelyClient) GetEnabledFeatures(userContext entities.UserContext) return enabledFeatures, nil } +// GetFeatureVariableBoolean returns bool feature variable value +func (o *OptimizelyClient) GetFeatureVariableBoolean(featureKey string, variableKey string, userContext entities.UserContext) (value bool, err error) { + val, err := o.getFeatureVariable(false, featureKey, variableKey, userContext) + if err == nil { + switch val.(type) { + case bool: + return val.(bool), err + default: + break + } + } + return false, err +} + +// GetFeatureVariableDouble returns double feature variable value +func (o *OptimizelyClient) GetFeatureVariableDouble(featureKey string, variableKey string, userContext entities.UserContext) (value float64, err error) { + val, err := o.getFeatureVariable(float64(0), featureKey, variableKey, userContext) + if err == nil { + switch val.(type) { + case float64: + return val.(float64), err + default: + break + } + } + return 0, err +} + +// GetFeatureVariableInteger returns integer feature variable value +func (o *OptimizelyClient) GetFeatureVariableInteger(featureKey string, variableKey string, userContext entities.UserContext) (value int, err error) { + val, err := o.getFeatureVariable(int(0), featureKey, variableKey, userContext) + if err == nil { + switch val.(type) { + case int: + return val.(int), err + default: + break + } + } + return 0, err +} + +// GetFeatureVariableString returns string feature variable value +func (o *OptimizelyClient) GetFeatureVariableString(featureKey string, variableKey string, userContext entities.UserContext) (value string, err error) { + val, err := o.getFeatureVariable("", featureKey, variableKey, userContext) + if err == nil { + switch val.(type) { + case string: + return val.(string), err + default: + break + } + } + return "", err +} + +func (o *OptimizelyClient) getFeatureVariable(valueType interface{}, featureKey string, variableKey string, userContext entities.UserContext) (value interface{}, err error) { + if !o.isValid { + errorMessage := "Optimizely instance is not valid. Failing getFeatureVariable." + err := errors.New(errorMessage) + logger.Error(errorMessage, nil) + return nil, err + } + + defer func() { + if r := recover(); r != nil { + errorMessage := fmt.Sprintf(`Optimizely SDK is panicking with the error "%s"`, string(debug.Stack())) + err = errors.New(errorMessage) + logger.Error(errorMessage, err) + } + }() + + projectConfig := o.configManager.GetConfig() + + if reflect.ValueOf(projectConfig).IsNil() { + return nil, fmt.Errorf("project config is null") + } + + featureFlag, err := projectConfig.GetFeatureFlagByKey(featureKey) + if err != nil { + logger.Error("Error retrieving feature flag", err) + return nil, err + } + variable, err := featureFlag.GetVariable(variableKey) + if err != nil { + logger.Error("Error retrieving variable", err) + return nil, err + } + + var featureValue = variable.DefaultValue + + feature, err := projectConfig.GetFeatureByKey(featureKey) + if err != nil { + logger.Error("Error retrieving feature", err) + return nil, err + } + featureDecisionContext := decision.FeatureDecisionContext{ + Feature: &feature, + ProjectConfig: projectConfig, + } + + featureDecision, err := o.decisionService.GetFeatureDecision(featureDecisionContext, userContext) + if err == nil { + for _, v := range featureDecision.Variation.Variables { + if v.ID == variable.ID && featureDecision.Variation.FeatureEnabled { + featureValue = v.Value + break + } + } + } + + var typeName = "" + var valueParsed interface{} + switch valueType.(type) { + case string: + typeName = "string" + valueParsed = featureValue + break + case int: + typeName = "integer" + convertedValue, err := strconv.Atoi(featureValue) + if err == nil { + valueParsed = convertedValue + } + break + case float64: + typeName = "double" + convertedValue, err := strconv.ParseFloat(featureValue, 64) + if err == nil { + valueParsed = convertedValue + } + break + case bool: + typeName = "boolean" + convertedValue, err := strconv.ParseBool(featureValue) + if err == nil { + valueParsed = convertedValue + } + break + default: + break + } + + if valueParsed == nil || variable.Type != typeName { + return nil, fmt.Errorf("Variable value for key %s is invalid or wrong type", variableKey) + } + + // @TODO(yasir): send decision notification + return valueParsed, nil +} + // Close closes the Optimizely instance and stops any ongoing tasks from its children components func (o *OptimizelyClient) Close() { o.cancelFunc() diff --git a/optimizely/client/client_test.go b/optimizely/client/client_test.go index e282b7125..9a5fd71c2 100644 --- a/optimizely/client/client_test.go +++ b/optimizely/client/client_test.go @@ -23,6 +23,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/optimizely/go-sdk/optimizely" + datafileEntities "github.com/optimizely/go-sdk/optimizely/config/datafileprojectconfig/entities" "github.com/optimizely/go-sdk/optimizely/decision" "github.com/optimizely/go-sdk/optimizely/entities" "github.com/stretchr/testify/mock" @@ -43,6 +44,11 @@ func (c *MockProjectConfig) GetFeatureList() []entities.Feature { return args.Get(0).([]entities.Feature) } +func (c *MockProjectConfig) GetFeatureFlagByKey(featureKey string) (datafileEntities.FeatureFlag, error) { + args := c.Called(featureKey) + return args.Get(0).(datafileEntities.FeatureFlag), args.Error(1) +} + type MockProjectConfigManager struct { mock.Mock } @@ -297,3 +303,797 @@ func TestGetEnabledFeaturesPanic(t *testing.T) { assert.Empty(t, result) assert.True(t, assert.Error(t, err)) } + +func TestGetFeatureVariableBooleanWithValidValue(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "true" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "default", + ID: "1", + Key: "test_feature_flag_key", + Type: "boolean"} + + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, _ := client.GetFeatureVariableBoolean(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, true, result) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableBooleanWithInValidValue(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "testvalue" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "true", + ID: "1", + Key: "test_feature_flag_key", + Type: "boolean"} + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, err := client.GetFeatureVariableBoolean(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, false, result) + assert.NotNil(t, err) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableBooleanWithInvalidValueType(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "testValue" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "default", + ID: "1", + Key: "test_feature_flag_key", + Type: "string"} + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, err := client.GetFeatureVariableBoolean(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, false, result) + assert.NotNil(t, err) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableBooleanReturnsDefaultValueIfFeatureNotEnabled(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "false" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "true", + ID: "1", + Key: "test_feature_flag_key", + Type: "boolean"} + testVariation := getTestVariationWithFeatureVariable(false, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, err := client.GetFeatureVariableBoolean(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, true, result) + assert.Nil(t, err) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableDoubleWithValidValue(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "150" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "default", + ID: "1", + Key: "test_feature_flag_key", + Type: "double"} + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, _ := client.GetFeatureVariableDouble(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, float64(150), result) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableDoubleWithInvalidValue(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "invalid" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "default", + ID: "1", + Key: "test_feature_flag_key", + Type: "double"} + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, err := client.GetFeatureVariableDouble(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, float64(0), result) + assert.NotNil(t, err) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableDoubleWithInvalidValueType(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "true" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "default", + ID: "1", + Key: "test_feature_flag_key", + Type: "boolean"} + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, err := client.GetFeatureVariableDouble(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, float64(0), result) + assert.NotNil(t, err) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableDoubleReturnsDefaultValueIfFeatureNotEnabled(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "150" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "120", + ID: "1", + Key: "test_feature_flag_key", + Type: "double"} + testVariation := getTestVariationWithFeatureVariable(false, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, err := client.GetFeatureVariableDouble(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, float64(120), result) + assert.Nil(t, err) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableIntegerWithValidValue(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "100" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "default", + ID: "1", + Key: "test_feature_flag_key", + Type: "integer"} + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, _ := client.GetFeatureVariableInteger(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, int(100), result) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableIntegerWithInvalidValue(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "teststring" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "default", + ID: "1", + Key: "test_feature_flag_key", + Type: "integer"} + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, err := client.GetFeatureVariableInteger(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, int(0), result) + assert.NotNil(t, err) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableIntegerWithInvalidValueType(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "true" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "default", + ID: "1", + Key: "test_feature_flag_key", + Type: "boolean"} + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, err := client.GetFeatureVariableInteger(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, int(0), result) + assert.NotNil(t, err) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableIntegerReturnsDefaultValueIfFeatureNotEnabled(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "100" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "200", + ID: "1", + Key: "test_feature_flag_key", + Type: "integer"} + testVariation := getTestVariationWithFeatureVariable(false, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, _ := client.GetFeatureVariableInteger(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, int(200), result) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableStringWithValidValue(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "teststring" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "default", + ID: "1", + Key: "test_feature_flag_key", + Type: "string"} + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, _ := client.GetFeatureVariableString(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, testVariableValue, result) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableStringWithInvalidValueType(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "true" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "default", + ID: "1", + Key: "test_feature_flag_key", + Type: "boolean"} + testVariation := getTestVariationWithFeatureVariable(true, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, err := client.GetFeatureVariableString(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, "", result) + assert.NotNil(t, err) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableStringReturnsDefaultValueIfFeatureNotEnabled(t *testing.T) { + testFeatureKey := "test_feature_key" + testFeatureFlagKey := "test_feature_flag_key" + testVariableValue := "teststring" + testUserContext := entities.UserContext{ID: "test_user_1"} + testVariationVariable := datafileEntities.VariationVariable{ + ID: "1", + Value: testVariableValue, + } + testVariable := datafileEntities.Variable{ + DefaultValue: "defaultString", + ID: "1", + Key: "test_feature_flag_key", + Type: "string"} + testVariation := getTestVariationWithFeatureVariable(false, testVariationVariable) + testExperiment := entities.Experiment{ + ID: "111111", + Variations: map[string]entities.Variation{"22222": testVariation}, + } + testFeature := getTestFeature(testFeatureKey, testExperiment) + testFeatureFlag := getTestFeatureFlag(testFeatureFlagKey, testVariable) + mockConfig := getMockConfig(testFeatureKey, testFeature, testFeatureFlag) + mockConfigManager := new(MockProjectConfigManager) + mockConfigManager.On("GetConfig").Return(mockConfig) + + testDecisionContext := decision.FeatureDecisionContext{ + Feature: &testFeature, + ProjectConfig: mockConfig, + } + + expectedFeatureDecision := getTestFeatureDecision(testExperiment, testVariation, true) + mockDecisionService := new(MockDecisionService) + mockDecisionService.On("GetFeatureDecision", testDecisionContext, testUserContext).Return(expectedFeatureDecision, nil) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + result, err := client.GetFeatureVariableString(testFeatureKey, testFeatureFlagKey, testUserContext) + assert.Equal(t, "defaultString", result) + assert.Nil(t, err) + mockConfig.AssertExpectations(t) + mockConfigManager.AssertExpectations(t) + mockDecisionService.AssertExpectations(t) +} + +func TestGetFeatureVariableErrorCases(t *testing.T) { + testUserContext := entities.UserContext{ID: "test_user_1"} + + mockConfigManager := new(MockProjectConfigManager) + mockDecisionService := new(MockDecisionService) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: false, + } + _, err := client.GetFeatureVariableBoolean("test_feature_key", "test_variable_key", testUserContext) + assert.Error(t, err) + _, err1 := client.GetFeatureVariableDouble("test_feature_key", "test_variable_key", testUserContext) + assert.Error(t, err1) + _, err3 := client.GetFeatureVariableInteger("test_feature_key", "test_variable_key", testUserContext) + assert.Error(t, err3) + _, err4 := client.GetFeatureVariableString("test_feature_key", "test_variable_key", testUserContext) + assert.Error(t, err4) + mockConfigManager.AssertNotCalled(t, "GetFeatureFlagByKey") + mockConfigManager.AssertNotCalled(t, "GetFeatureByKey") + mockDecisionService.AssertNotCalled(t, "GetFeatureDecision") +} + +func TestGetFeatureVariableStringPanic(t *testing.T) { + testUserContext := entities.UserContext{ID: "test_user_1"} + testFeatureKey := "test_feature_key" + testVariableKey := "test_variable_key" + + mockConfigManager := new(MockProjectConfigManager) + mockDecisionService := new(MockDecisionService) + + client := OptimizelyClient{ + configManager: mockConfigManager, + decisionService: mockDecisionService, + isValid: true, + } + + // returning an error object will cause the Client to panic + mockConfigManager.On("GetFeatureByKey", testFeatureKey, testUserContext).Return(errors.New("failure")) + + // ensure that the client calms back down and recovers + result, err := client.GetFeatureVariableString(testFeatureKey, testVariableKey, testUserContext) + assert.Equal(t, "", result) + assert.True(t, assert.Error(t, err)) +} + +// Helper Methods + +func getTestFeatureDecision(experiment entities.Experiment, variation entities.Variation, decisionMade bool) decision.FeatureDecision { + return decision.FeatureDecision{ + Experiment: experiment, + Variation: variation, + Decision: decision.Decision{ + DecisionMade: decisionMade, + }, + } +} + +func getTestVariationWithFeatureVariable(featureEnabled bool, featureVariable datafileEntities.VariationVariable) entities.Variation { + return entities.Variation{ + ID: "22222", + Key: "22222", + FeatureEnabled: featureEnabled, + Variables: []datafileEntities.VariationVariable{featureVariable}, + } +} + +func getTestFeatureFlag(featureFlagKey string, variable datafileEntities.Variable) datafileEntities.FeatureFlag { + return datafileEntities.FeatureFlag{ + ID: "21111", + Key: featureFlagKey, + RolloutID: "41111", + ExperimentIDs: []string{"31111", "31112"}, + Variables: []datafileEntities.Variable{variable}, + } +} + +func getMockConfig(featureKey string, feature entities.Feature, featureFlag datafileEntities.FeatureFlag) *MockProjectConfig { + mockConfig := new(MockProjectConfig) + mockConfig.On("GetFeatureByKey", featureKey).Return(feature, nil) + mockConfig.On("GetFeatureFlagByKey", featureKey).Return(featureFlag, nil) + return mockConfig +} + +func getTestFeature(featureKey string, experiment entities.Experiment) entities.Feature { + return entities.Feature{ + ID: "22222", + Key: featureKey, + FeatureExperiments: []entities.Experiment{experiment}, + } +} diff --git a/optimizely/config/datafileprojectconfig/config.go b/optimizely/config/datafileprojectconfig/config.go index 8a38e5165..35447a3f6 100644 --- a/optimizely/config/datafileprojectconfig/config.go +++ b/optimizely/config/datafileprojectconfig/config.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + datafileEntities "github.com/optimizely/go-sdk/optimizely/config/datafileprojectconfig/entities" "github.com/optimizely/go-sdk/optimizely/config/datafileprojectconfig/mappers" "github.com/optimizely/go-sdk/optimizely/entities" "github.com/optimizely/go-sdk/optimizely/logging" @@ -39,6 +40,7 @@ type DatafileProjectConfig struct { experimentKeyToIDMap map[string]string experimentMap map[string]entities.Experiment featureMap map[string]entities.Feature + featureFlagMap map[string]datafileEntities.FeatureFlag groupMap map[string]entities.Group projectID string revision string @@ -94,7 +96,8 @@ func NewDatafileProjectConfig(jsonDatafile []byte) (*DatafileProjectConfig, erro experimentMap: experimentMap, experimentKeyToIDMap: experimentKeyMap, rolloutMap: rolloutMap, - featureMap: mappers.MapFeatureFlags(datafile.FeatureFlags, rolloutMap, experimentMap), + featureMap: mappers.MapFeatures(datafile.FeatureFlags, rolloutMap, experimentMap), + featureFlagMap: mappers.MapFeatureFlags(datafile.FeatureFlags), } logger.Info("Datafile is valid.") @@ -121,6 +124,16 @@ func (c DatafileProjectConfig) GetFeatureByKey(featureKey string) (entities.Feat return entities.Feature{}, errors.New(errMessage) } +// GetFeatureFlagByKey returns the featureflag with the given key +func (c DatafileProjectConfig) GetFeatureFlagByKey(featureKey string) (datafileEntities.FeatureFlag, error) { + if feature, ok := c.featureFlagMap[featureKey]; ok { + return feature, nil + } + + errMessage := fmt.Sprintf("FeatureFlag with key %s not found", featureKey) + return datafileEntities.FeatureFlag{}, errors.New(errMessage) +} + // GetAttributeByKey returns the attribute with the given key func (c DatafileProjectConfig) GetAttributeByKey(key string) (entities.Attribute, error) { if attributeID, ok := c.attributeKeyToIDMap[key]; ok { diff --git a/optimizely/config/datafileprojectconfig/entities/entities.go b/optimizely/config/datafileprojectconfig/entities/entities.go index 1a16c7986..6eb7d286f 100644 --- a/optimizely/config/datafileprojectconfig/entities/entities.go +++ b/optimizely/config/datafileprojectconfig/entities/entities.go @@ -16,6 +16,10 @@ package entities +import ( + "fmt" +) + // Audience represents an Audience object from the Optimizely datafile type Audience struct { ID string `json:"id"` @@ -51,6 +55,20 @@ type FeatureFlag struct { Variables []Variable `json:"variables"` } +// GetVariable returns variable for key from feature flag +func (f *FeatureFlag) GetVariable(key string) (Variable, error) { + var variable Variable + var err = fmt.Errorf("Variable with key %s not found", key) + for _, v := range f.Variables { + if v.Key == key { + variable = v + err = nil + break + } + } + return variable, err +} + // Variable represents a Variable object from the Optimizely datafile type Variable struct { DefaultValue string `json:"defaultValue"` @@ -67,10 +85,16 @@ type trafficAllocation struct { // Variation represents an experiment variation from the Optimizely datafile type Variation struct { - ID string `json:"id"` - // @TODO(mng): include variables - Key string `json:"key"` - FeatureEnabled bool `json:"featureEnabled"` + ID string `json:"id"` + Variables []VariationVariable `json:"variables"` + Key string `json:"key"` + FeatureEnabled bool `json:"featureEnabled"` +} + +// VariationVariable represents a Variable object from the Variation +type VariationVariable struct { + ID string `json:"id"` + Value string `json:"value"` } // Event represents an event from the Optimizely datafile diff --git a/optimizely/config/datafileprojectconfig/mappers/experiment.go b/optimizely/config/datafileprojectconfig/mappers/experiment.go index 5dd553a1e..662802912 100644 --- a/optimizely/config/datafileprojectconfig/mappers/experiment.go +++ b/optimizely/config/datafileprojectconfig/mappers/experiment.go @@ -43,6 +43,7 @@ func mapVariation(rawVariation datafileEntities.Variation) entities.Variation { ID: rawVariation.ID, Key: rawVariation.Key, FeatureEnabled: rawVariation.FeatureEnabled, + Variables: rawVariation.Variables, } return variation } diff --git a/optimizely/config/datafileprojectconfig/mappers/feature.go b/optimizely/config/datafileprojectconfig/mappers/feature.go index 978b1c71c..84c02eff9 100644 --- a/optimizely/config/datafileprojectconfig/mappers/feature.go +++ b/optimizely/config/datafileprojectconfig/mappers/feature.go @@ -21,11 +21,8 @@ import ( "github.com/optimizely/go-sdk/optimizely/entities" ) -// MapFeatureFlags maps the raw datafile feature flag entities to SDK Feature entities -func MapFeatureFlags( - featureFlags []datafileEntities.FeatureFlag, - rolloutMap map[string]entities.Rollout, - experimentMap map[string]entities.Experiment, +// MapFeatures maps the raw datafile feature flag entities to SDK Feature entities +func MapFeatures(featureFlags []datafileEntities.FeatureFlag, rolloutMap map[string]entities.Rollout, experimentMap map[string]entities.Experiment, ) map[string]entities.Feature { featureMap := make(map[string]entities.Feature) @@ -49,3 +46,13 @@ func MapFeatureFlags( } return featureMap } + +// MapFeatureFlags maps the raw datafile feature flag entities to their keys +func MapFeatureFlags(featureFlags []datafileEntities.FeatureFlag) map[string]datafileEntities.FeatureFlag { + + featureFlagsMap := make(map[string]datafileEntities.FeatureFlag) + for _, featureFlag := range featureFlags { + featureFlagsMap[featureFlag.Key] = featureFlag + } + return featureFlagsMap +} diff --git a/optimizely/config/datafileprojectconfig/mappers/feature_test.go b/optimizely/config/datafileprojectconfig/mappers/feature_test.go index 831eac1e9..cd43509d5 100644 --- a/optimizely/config/datafileprojectconfig/mappers/feature_test.go +++ b/optimizely/config/datafileprojectconfig/mappers/feature_test.go @@ -25,6 +25,33 @@ import ( "github.com/stretchr/testify/assert" ) +func TestMapFeatureFlags(t *testing.T) { + const testFeatureFlagString = `{ + "id": "21111", + "key": "test_feature_21111", + "rolloutId": "41111", + "experimentIds": ["31111", "31112"] + }` + + var rawFeatureFlag datafileEntities.FeatureFlag + json.Unmarshal([]byte(testFeatureFlagString), &rawFeatureFlag) + + rawFeatureFlags := []datafileEntities.FeatureFlag{rawFeatureFlag} + featureFlagsMap := MapFeatureFlags(rawFeatureFlags) + assert.Equal(t, len(featureFlagsMap), 1) + + expectedFeatureFlagsMap := map[string]datafileEntities.FeatureFlag{ + "test_feature_21111": datafileEntities.FeatureFlag{ + ID: "21111", + Key: "test_feature_21111", + RolloutID: "41111", + ExperimentIDs: []string{"31111", "31112"}, + }, + } + + assert.Equal(t, expectedFeatureFlagsMap, featureFlagsMap) +} + func TestMapFeatures(t *testing.T) { const testFeatureFlagString = `{ "id": "21111", @@ -47,7 +74,7 @@ func TestMapFeatures(t *testing.T) { "31111": experiment31111, "31112": experiment31112, } - featureMap := MapFeatureFlags(rawFeatureFlags, rolloutMap, experimentMap) + featureMap := MapFeatures(rawFeatureFlags, rolloutMap, experimentMap) expectedFeatureMap := map[string]entities.Feature{ "test_feature_21111": entities.Feature{ ID: "21111", diff --git a/optimizely/entities/experiment.go b/optimizely/entities/experiment.go index c402fb62b..48db7b82b 100644 --- a/optimizely/entities/experiment.go +++ b/optimizely/entities/experiment.go @@ -16,9 +16,14 @@ package entities +import ( + datafileEntities "github.com/optimizely/go-sdk/optimizely/config/datafileprojectconfig/entities" +) + // Variation represents a variation in the experiment type Variation struct { ID string + Variables []datafileEntities.VariationVariable Key string FeatureEnabled bool } diff --git a/optimizely/event/processor_test.go b/optimizely/event/processor_test.go index 2a2ea2e28..5c53a08a9 100644 --- a/optimizely/event/processor_test.go +++ b/optimizely/event/processor_test.go @@ -16,7 +16,7 @@ func TestDefaultEventProcessor_ProcessImpression(t *testing.T) { assert.Equal(t, 1, processor.EventsCount()) - time.Sleep(200 * time.Millisecond) + time.Sleep(2000 * time.Millisecond) assert.NotNil(t, processor.Ticker) diff --git a/optimizely/interface.go b/optimizely/interface.go index 9dc985e9a..734a5bb56 100644 --- a/optimizely/interface.go +++ b/optimizely/interface.go @@ -17,6 +17,7 @@ package optimizely import ( + datafileEntities "github.com/optimizely/go-sdk/optimizely/config/datafileprojectconfig/entities" "github.com/optimizely/go-sdk/optimizely/entities" ) @@ -32,6 +33,7 @@ type ProjectConfig interface { GetEventByKey(string) (entities.Event, error) GetExperimentByKey(string) (entities.Experiment, error) GetFeatureByKey(string) (entities.Feature, error) + GetFeatureFlagByKey(string) (datafileEntities.FeatureFlag, error) GetFeatureList() []entities.Feature GetGroupByID(string) (entities.Group, error) GetProjectID() string