diff --git a/pkg/decision/experiment_override_service.go b/pkg/decision/experiment_override_service.go new file mode 100644 index 000000000..9386d1fc8 --- /dev/null +++ b/pkg/decision/experiment_override_service.go @@ -0,0 +1,93 @@ +/**************************************************************************** + * Copyright 2019, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +// Package decision // +package decision + +import ( + "errors" + "fmt" + + "github.com/optimizely/go-sdk/pkg/decision/reasons" + "github.com/optimizely/go-sdk/pkg/entities" + "github.com/optimizely/go-sdk/pkg/logging" +) + +var eosLogger = logging.GetLogger("ExperimentOverrideService") + +// ExperimentOverrideKey represents the user ID and experiment associated with an override variation +type ExperimentOverrideKey struct { + ExperimentKey, UserID string +} + +// ExperimentOverrideStore provides read access to overrides +type ExperimentOverrideStore interface { + // Returns a variation associated with overrideKey + GetVariation(overrideKey ExperimentOverrideKey) (string, bool) +} + +// MapOverridesStore is a map-based implementation of OverrideStore +type MapOverridesStore struct { + overridesMap map[ExperimentOverrideKey]string +} + +// GetVariation returns the override associated with the given key in the map +func (m *MapOverridesStore) GetVariation(overrideKey ExperimentOverrideKey) (string, bool) { + variationKey, ok := m.overridesMap[overrideKey] + return variationKey, ok +} + +// ExperimentOverrideService makes a decision using an ExperimentOverridesStore +// Implements the ExperimentService interface +type ExperimentOverrideService struct { + Overrides ExperimentOverrideStore +} + +// NewExperimentOverrideService returns a pointer to an initialized ExperimentOverrideService +func NewExperimentOverrideService(overrides ExperimentOverrideStore) *ExperimentOverrideService { + return &ExperimentOverrideService{ + Overrides: overrides, + } +} + +// GetDecision returns a decision with a variation when the store returns a variation assignment for the given user and experiment +func (s ExperimentOverrideService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (ExperimentDecision, error) { + decision := ExperimentDecision{} + + if decisionContext.Experiment == nil { + return decision, errors.New("decisionContext Experiment is nil") + } + + variationKey, ok := s.Overrides.GetVariation(ExperimentOverrideKey{ExperimentKey: decisionContext.Experiment.Key, UserID: userContext.ID}) + if !ok { + decision.Reason = reasons.NoOverrideVariationAssignment + return decision, nil + } + + // TODO(Matt): Implement and use a way to access variations by key + for _, variation := range decisionContext.Experiment.Variations { + variation := variation + if variation.Key == variationKey { + decision.Variation = &variation + decision.Reason = reasons.OverrideVariationAssignmentFound + eosLogger.Debug(fmt.Sprintf("Override variation %v found for user %v", variationKey, userContext.ID)) + return decision, nil + } + } + + decision.Reason = reasons.InvalidOverrideVariationAssignment + return decision, nil +} diff --git a/pkg/decision/experiment_override_service_test.go b/pkg/decision/experiment_override_service_test.go new file mode 100644 index 000000000..ca50b84e1 --- /dev/null +++ b/pkg/decision/experiment_override_service_test.go @@ -0,0 +1,137 @@ +/**************************************************************************** + * Copyright 2019, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +// Package decision // +package decision + +import ( + "testing" + + "github.com/optimizely/go-sdk/pkg/decision/reasons" + "github.com/optimizely/go-sdk/pkg/entities" + "github.com/stretchr/testify/suite" +) + +type ExperimentOverrideServiceTestSuite struct { + suite.Suite + mockConfig *mockProjectConfig + overrides map[ExperimentOverrideKey]string + overrideService *ExperimentOverrideService +} + +func (s *ExperimentOverrideServiceTestSuite) SetupTest() { + s.mockConfig = new(mockProjectConfig) + s.overrides = make(map[ExperimentOverrideKey]string) + s.overrideService = NewExperimentOverrideService(&MapOverridesStore{ + overridesMap: s.overrides, + }) +} + +func (s *ExperimentOverrideServiceTestSuite) TestOverridesIncludeVariation() { + testDecisionContext := ExperimentDecisionContext{ + Experiment: &testExp1111, + ProjectConfig: s.mockConfig, + } + testUserContext := entities.UserContext{ + ID: "test_user_1", + } + s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}] = testExp1111Var2222.Key + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + s.NoError(err) + s.NotNil(decision.Variation) + s.Exactly(testExp1111Var2222.Key, decision.Variation.Key) + s.Exactly(reasons.OverrideVariationAssignmentFound, decision.Reason) +} + +func (s *ExperimentOverrideServiceTestSuite) TestNilDecisionContextExperiment() { + testDecisionContext := ExperimentDecisionContext{ + ProjectConfig: s.mockConfig, + } + testUserContext := entities.UserContext{ + ID: "test_user_1", + } + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + s.Error(err) + s.Nil(decision.Variation) +} + +func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForExperiment() { + testDecisionContext := ExperimentDecisionContext{ + Experiment: &testExp1111, + ProjectConfig: s.mockConfig, + } + testUserContext := entities.UserContext{ + ID: "test_user_1", + } + // The decision context refers to testExp1111, but this override is for another experiment + s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_1"}] = testExp1113Var2224.Key + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + s.NoError(err) + s.Nil(decision.Variation) + s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason) +} + +func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForUser() { + testDecisionContext := ExperimentDecisionContext{ + Experiment: &testExp1111, + ProjectConfig: s.mockConfig, + } + testUserContext := entities.UserContext{ + ID: "test_user_1", + } + // The user context refers to "test_user_1", but this override is for another user + s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_2"}] = testExp1111Var2222.Key + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + s.NoError(err) + s.Nil(decision.Variation) + s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason) +} + +func (s *ExperimentOverrideServiceTestSuite) TestNoOverrideForUserOrExperiment() { + testDecisionContext := ExperimentDecisionContext{ + Experiment: &testExp1111, + ProjectConfig: s.mockConfig, + } + testUserContext := entities.UserContext{ + ID: "test_user_1", + } + // This override is for both a different user and a different experiment than the ones in the contexts above + s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1113.Key, UserID: "test_user_3"}] = testExp1111Var2222.Key + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + s.NoError(err) + s.Nil(decision.Variation) + s.Exactly(reasons.NoOverrideVariationAssignment, decision.Reason) +} + +func (s *ExperimentOverrideServiceTestSuite) TestInvalidVariationInOverride() { + testDecisionContext := ExperimentDecisionContext{ + Experiment: &testExp1111, + ProjectConfig: s.mockConfig, + } + testUserContext := entities.UserContext{ + ID: "test_user_1", + } + // This override variation key does not exist in the experiment + s.overrides[ExperimentOverrideKey{ExperimentKey: testExp1111.Key, UserID: "test_user_1"}] = "invalid_variation_key" + decision, err := s.overrideService.GetDecision(testDecisionContext, testUserContext) + s.NoError(err) + s.Nil(decision.Variation) + s.Exactly(reasons.InvalidOverrideVariationAssignment, decision.Reason) +} + +func TestExperimentOverridesTestSuite(t *testing.T) { + suite.Run(t, new(ExperimentOverrideServiceTestSuite)) +} diff --git a/pkg/decision/reasons/reason.go b/pkg/decision/reasons/reason.go index cb01775ea..24a8b0f92 100644 --- a/pkg/decision/reasons/reason.go +++ b/pkg/decision/reasons/reason.go @@ -43,4 +43,10 @@ const ( InvalidWhitelistVariationAssignment Reason = "Invalid whitelist variation assignment" // WhitelistVariationAssignmentFound - a valid variation assignment was found for the given user and experiment WhitelistVariationAssignmentFound Reason = "Whitelist variation assignment found" + // NoOverrideVariationAssignment - No override variation was found for the given user and experiment + NoOverrideVariationAssignment Reason = "No override variation assignment" + // InvalidOverrideVariationAssignment - An override variation was found for the given user and experiment, but no variation with that key exists in the given experiment + InvalidOverrideVariationAssignment Reason = "Invalid override variation assignment" + // OverrideVariationAssignmentFound - A valid override variation was found for the given user and experiment + OverrideVariationAssignmentFound Reason = "Override variation assignment found" )