diff --git a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj index 7ffcc618..c4eaf327 100644 --- a/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj +++ b/OptimizelySDK.Net40/OptimizelySDK.Net40.csproj @@ -234,6 +234,9 @@ Config\FallbackProjectConfigManager.cs + + OptimizelyFactory.cs + diff --git a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj index e1648e1d..28d326e4 100644 --- a/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj +++ b/OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj @@ -76,6 +76,9 @@ AtomicProjectConfigManager.cs + + OptimizelyFactory.cs + diff --git a/OptimizelySDK.Tests/DatafileManagement Tests/AtomicProjectConfigManagerTest.cs b/OptimizelySDK.Tests/ConfigTest/FallbackProjectConfigManagerTest.cs similarity index 88% rename from OptimizelySDK.Tests/DatafileManagement Tests/AtomicProjectConfigManagerTest.cs rename to OptimizelySDK.Tests/ConfigTest/FallbackProjectConfigManagerTest.cs index 036bdab2..5610ad09 100644 --- a/OptimizelySDK.Tests/DatafileManagement Tests/AtomicProjectConfigManagerTest.cs +++ b/OptimizelySDK.Tests/ConfigTest/FallbackProjectConfigManagerTest.cs @@ -22,13 +22,14 @@ namespace OptimizelySDK.Tests.DatafileManagement_Tests [TestFixture] public class AtomicProjectConfigManagerTest { - private FallbackProjectConfigManager ConfigManager = new FallbackProjectConfigManager(); + private FallbackProjectConfigManager ConfigManager; [Test] public void TestStaticProjectConfigManagerReturnsCorrectProjectConfig() { var expectedConfig = DatafileProjectConfig.Create(TestData.TypedAudienceDatafile, null, null); - ConfigManager.SetConfig(expectedConfig); + ConfigManager = new FallbackProjectConfigManager(expectedConfig); + Assert.True(TestData.CompareObjects(expectedConfig, ConfigManager.GetConfig())); } } diff --git a/OptimizelySDK.Tests/DatafileManagement Tests/HttpProjectConfigManagerTest.cs b/OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs similarity index 100% rename from OptimizelySDK.Tests/DatafileManagement Tests/HttpProjectConfigManagerTest.cs rename to OptimizelySDK.Tests/ConfigTest/HttpProjectConfigManagerTest.cs diff --git a/OptimizelySDK.Tests/DatafileManagement Tests/PollingProjectConfigManagerTest.cs b/OptimizelySDK.Tests/ConfigTest/PollingProjectConfigManagerTest.cs similarity index 100% rename from OptimizelySDK.Tests/DatafileManagement Tests/PollingProjectConfigManagerTest.cs rename to OptimizelySDK.Tests/ConfigTest/PollingProjectConfigManagerTest.cs diff --git a/OptimizelySDK.Tests/DatafileManagement Tests/TestPollingProjectConfigManager.cs b/OptimizelySDK.Tests/ConfigTest/TestPollingProjectConfigManager.cs similarity index 100% rename from OptimizelySDK.Tests/DatafileManagement Tests/TestPollingProjectConfigManager.cs rename to OptimizelySDK.Tests/ConfigTest/TestPollingProjectConfigManager.cs diff --git a/OptimizelySDK.Tests/DecisionServiceTest.cs b/OptimizelySDK.Tests/DecisionServiceTest.cs index 66fa958f..d92dd495 100644 --- a/OptimizelySDK.Tests/DecisionServiceTest.cs +++ b/OptimizelySDK.Tests/DecisionServiceTest.cs @@ -37,7 +37,7 @@ public class DecisionServiceTest private Mock UserProfileServiceMock; private Mock BucketerMock; private Mock DecisionServiceMock; - + private ProjectConfig ProjectConfig; private Experiment WhitelistedExperiment; private Variation WhitelistedVariation; @@ -46,36 +46,40 @@ public class DecisionServiceTest private Variation VariationWithKeyControl; private Variation VariationWithKeyVariation; + private ProjectConfig Config; + [SetUp] public void SetUp() { - LoggerMock = new Mock(); - ErrorHandlerMock = new Mock(); - UserProfileServiceMock = new Mock(); - BucketerMock = new Mock(LoggerMock.Object); - - ProjectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, ErrorHandlerMock.Object); - WhitelistedExperiment = ProjectConfig.ExperimentIdMap["224"]; - WhitelistedVariation = WhitelistedExperiment.VariationKeyToVariationMap["vtag5"]; - - DecisionService = new DecisionService(new Bucketer(LoggerMock.Object), ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); - DecisionServiceMock = new Mock(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object) { CallBase = true }; - - VariationWithKeyControl = ProjectConfig.GetVariationFromKey("test_experiment", "control"); - VariationWithKeyVariation = ProjectConfig.GetVariationFromKey("test_experiment", "variation"); + LoggerMock = new Mock(); + ErrorHandlerMock = new Mock(); + UserProfileServiceMock = new Mock(); + BucketerMock = new Mock(LoggerMock.Object); + + ProjectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, ErrorHandlerMock.Object); + WhitelistedExperiment = ProjectConfig.ExperimentIdMap["224"]; + WhitelistedVariation = WhitelistedExperiment.VariationKeyToVariationMap["vtag5"]; + + DecisionService = new DecisionService(new Bucketer(LoggerMock.Object), ErrorHandlerMock.Object, null, LoggerMock.Object); + DecisionServiceMock = new Mock(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object) { CallBase = true }; + + VariationWithKeyControl = ProjectConfig.GetVariationFromKey("test_experiment", "control"); + VariationWithKeyVariation = ProjectConfig.GetVariationFromKey("test_experiment", "variation"); + + Config = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, ErrorHandlerMock.Object); } [Test] public void TestGetVariationForcedVariationPrecedesAudienceEval() { - DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); + DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); Experiment experiment = ProjectConfig.Experiments[8]; Variation expectedVariation = experiment.Variations[0]; // user excluded without audiences and whitelisting - Assert.IsNull(decisionService.GetVariation(experiment, GenericUserId, new UserAttributes())); + Assert.IsNull(decisionService.GetVariation(experiment, GenericUserId, ProjectConfig, new UserAttributes())); - var actualVariation = decisionService.GetVariation(experiment, WhitelistedUserId, new UserAttributes()); + var actualVariation = decisionService.GetVariation(experiment, WhitelistedUserId, ProjectConfig, new UserAttributes()); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"vtag5\".", WhitelistedUserId)), Times.Once); // no attributes provided for a experiment that has an audience @@ -97,15 +101,15 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting() UserProfileServiceMock.Setup(up => up.Lookup(WhitelistedUserId)).Returns(userProfile.ToMap()); - DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, UserProfileServiceMock.Object, LoggerMock.Object); + DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); - decisionService.GetVariation(experiment, GenericUserId, new UserAttributes()); + decisionService.GetVariation(experiment, GenericUserId, ProjectConfig, new UserAttributes()); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" does not meet conditions to be in experiment \"{1}\".", GenericUserId, experiment.Key)), Times.Once); // ensure that a user with a saved user profile, sees the same variation regardless of audience evaluation - decisionService.GetVariation(experiment, UserProfileId, new UserAttributes()); + decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes()); BucketerMock.Verify(_ => _.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); } @@ -113,7 +117,7 @@ public void TestGetVariationEvaluatesUserProfileBeforeAudienceTargeting() [Test] public void TestGetForcedVariationReturnsForcedVariation() { - DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); + DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); Assert.IsTrue(TestData.CompareObjects(WhitelistedVariation, decisionService.GetWhitelistedVariation(WhitelistedExperiment, WhitelistedUserId))); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" is forced in variation \"{1}\".", @@ -128,7 +132,7 @@ public void TestGetForcedVariationWithInvalidVariation() string userId = "testUser1"; string invalidVariationKey = "invalidVarKey"; - DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); + DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); var variations = new Variation[] { @@ -144,8 +148,7 @@ public void TestGetForcedVariationWithInvalidVariation() {userId, invalidVariationKey } }; - var experiment = new Experiment - { + var experiment = new Experiment { Id = "1234", Key = "exp_key", Status = "Running", @@ -168,7 +171,7 @@ public void TestGetForcedVariationWithInvalidVariation() public void TestGetForcedVariationReturnsNullWhenUserIsNotWhitelisted() { Bucketer bucketer = new Bucketer(LoggerMock.Object); - DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); + DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, null, LoggerMock.Object); Assert.IsNull(decisionService.GetWhitelistedVariation(WhitelistedExperiment, GenericUserId)); } @@ -188,9 +191,9 @@ public void TestBucketReturnsVariationStoredInUserProfile() UserProfileServiceMock.Setup(_ => _.Lookup(UserProfileId)).Returns(userProfile.ToMap()); - DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, UserProfileServiceMock.Object, LoggerMock.Object); + DecisionService decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); - Assert.IsTrue(TestData.CompareObjects(variation, decisionService.GetVariation(experiment, UserProfileId, new UserAttributes()))); + Assert.IsTrue(TestData.CompareObjects(variation, decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes()))); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("Returning previously activated variation \"{0}\" of experiment \"{1}\" for user \"{2}\" from user profile.", variation.Key, experiment.Key, UserProfileId))); @@ -211,9 +214,9 @@ public void TestGetStoredVariationLogsWhenLookupReturnsNull() UserProfileServiceMock.Setup(_ => _.Lookup(UserProfileId)).Returns(userProfile.ToMap()); DecisionService decisionService = new DecisionService(bucketer, - ErrorHandlerMock.Object, ProjectConfig, userProfileService, LoggerMock.Object); + ErrorHandlerMock.Object, userProfileService, LoggerMock.Object); - Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile)); + Assert.IsNull(decisionService.GetStoredVariation(experiment, userProfile, ProjectConfig)); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("No previously activated variation of experiment \"{0}\" for user \"{1}\" found in user profile." , experiment.Key, UserProfileId)), Times.Once); @@ -236,9 +239,9 @@ public void TestGetStoredVariationReturnsNullWhenVariationIsNoLongerInConfig() UserProfileServiceMock.Setup(up => up.Lookup(UserProfileId)).Returns(storedUserProfile.ToMap()); - DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, ProjectConfig, + DecisionService decisionService = new DecisionService(bucketer, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); - Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile)); + Assert.IsNull(decisionService.GetStoredVariation(experiment, storedUserProfile, ProjectConfig)); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("User \"{0}\" was previously bucketed into variation with ID \"{1}\" for experiment \"{2}\", but no matching variation was found for that user. We will re-bucket the user." , UserProfileId, storedVariationId, experiment.Id)), Times.Once); } @@ -264,9 +267,9 @@ public void TestGetVariationSavesBucketedVariationIntoUserProfile() var mockBucketer = new Mock(LoggerMock.Object); mockBucketer.Setup(m => m.Bucket(ProjectConfig, experiment, UserProfileId, UserProfileId)).Returns(variation); - DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, ProjectConfig, UserProfileServiceMock.Object, LoggerMock.Object); + DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); - Assert.IsTrue(TestData.CompareObjects(variation, decisionService.GetVariation(experiment, UserProfileId, new UserAttributes()))); + Assert.IsTrue(TestData.CompareObjects(variation, decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes()))); LoggerMock.Verify(l => l.Log(LogLevel.INFO, string.Format("Saved variation \"{0}\" of experiment \"{1}\" for user \"{2}\".", variation.Id, experiment.Id, UserProfileId)), Times.Once); @@ -291,7 +294,7 @@ public void TestBucketLogsCorrectlyWhenUserProfileFailsToSave() UserProfile saveUserProfile = new UserProfile(UserProfileId, new Dictionary()); DecisionService decisionService = new DecisionService(bucketer, - ErrorHandlerMock.Object, ProjectConfig, UserProfileServiceMock.Object, LoggerMock.Object); + ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); decisionService.SaveVariation(experiment, variation, saveUserProfile); @@ -320,10 +323,10 @@ public void TestGetVariationSavesANewUserProfile() UserProfileServiceMock.Setup(up => up.Lookup(UserProfileId)).Returns(userProfile); - DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, ProjectConfig, + DecisionService decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); - Assert.IsTrue(TestData.CompareObjects(variation, decisionService.GetVariation(experiment, UserProfileId, new UserAttributes()))); + Assert.IsTrue(TestData.CompareObjects(variation, decisionService.GetVariation(experiment, UserProfileId, ProjectConfig, new UserAttributes()))); UserProfileServiceMock.Verify(_ => _.Save(It.IsAny>()), Times.Once); } @@ -452,7 +455,7 @@ public void TestGetVariationWithBucketingId() }); UserProfileServiceMock.Setup(up => up.Lookup(userId)).Returns(storedUserProfile.ToMap()); - DecisionService decisionService = new DecisionService(bucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, UserProfileServiceMock.Object, LoggerMock.Object); + DecisionService decisionService = new DecisionService(bucketerMock.Object, ErrorHandlerMock.Object, UserProfileServiceMock.Object, LoggerMock.Object); actualVariation = optlyObject.GetVariation(experimentKey, userId, userAttributesWithBucketingId); Assert.IsTrue(TestData.CompareObjects(VariationWithKeyControl, actualVariation), string.Format("Variation \"{0}\" does not match expected user profile variation \"{1}\".", actualVariation?.Key, variationKeyControl)); @@ -465,7 +468,7 @@ public void TestGetVariationWithBucketingId() public void TestGetVariationForFeatureExperimentGivenNullExperimentIds() { var featureFlag = ProjectConfig.GetFeatureFlagFromKey("empty_feature"); - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); Assert.IsNull(decision); @@ -477,15 +480,14 @@ public void TestGetVariationForFeatureExperimentGivenNullExperimentIds() public void TestGetVariationForFeatureExperimentGivenExperimentNotInDataFile() { var booleanFeature = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var featureFlag = new FeatureFlag - { + var featureFlag = new FeatureFlag { Id = booleanFeature.Id, Key = booleanFeature.Key, RolloutId = booleanFeature.RolloutId, ExperimentIds = new List { "29039203" } }; - var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }); + var decision = DecisionService.GetVariationForFeatureExperiment(featureFlag, GenericUserId, new UserAttributes() { }, ProjectConfig); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Experiment ID \"29039203\" is not in datafile.")); @@ -497,10 +499,10 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBuck { var multiVariateExp = ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"); - DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", null)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariation(multiVariateExp, "user1", ProjectConfig, null)).Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes()); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig); Assert.IsNull(decision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into any of the experiments on the feature \"multi_variate_feature\".")); @@ -515,11 +517,11 @@ public void TestGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucke var expectedDecision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); var userAttributes = new UserAttributes(); - DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"), - "user1", userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("test_experiment_multivariate"), + "user1", ProjectConfig, userAttributes)).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("multi_variate_feature"); - var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes); + var decision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); Assert.IsTrue(TestData.CompareObjects(expectedDecision, decision)); @@ -535,11 +537,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed var userAttributes = new UserAttributes(); var expectedDecision = new FeatureDecision(mutexExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("group_experiment_1"), "user1", + DecisionServiceMock.Setup(ds => ds.GetVariation(ProjectConfig.GetExperimentFromKey("group_experiment_1"), "user1", ProjectConfig, userAttributes)).Returns(variation); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -551,11 +553,11 @@ public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed public void TestGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed() { var mutexExperiment = ProjectConfig.GetExperimentFromKey("group_experiment_1"); - DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), It.IsAny())). + DecisionServiceMock.Setup(ds => ds.GetVariation(It.IsAny(), It.IsAny(), ProjectConfig, It.IsAny())). Returns(null); var featureFlag = ProjectConfig.GetFeatureFlagFromKey("boolean_feature"); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes()); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", new UserAttributes(), ProjectConfig); Assert.IsNull(actualDecision); @@ -577,16 +579,16 @@ public void TestGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() Key = featureFlag.Key, ExperimentIds = new List(featureFlag.ExperimentIds), Variables = featureFlag.Variables - }; + }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes()); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", new UserAttributes(), ProjectConfig); Assert.IsNull(actualDecision); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The feature flag \"boolean_feature\" is not used in a rollout.")); } - + // Should return the variation the user is bucketed into when the user is bucketed into the targeting rule [Test] public void TestGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRule() @@ -602,11 +604,11 @@ public void TestGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRul { "browser_type", "chrome" } }; - BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), + BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(variation); - var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); + var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -629,9 +631,9 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargeting BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), experiment, It.IsAny(), It.IsAny())).Returns(null); BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny())).Returns(variation); - var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); + var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -648,12 +650,12 @@ public void TestGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTarge }; BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); - var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); + var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", userAttributes, ProjectConfig); Assert.IsNull(actualDecision); } - + // Should return expected variation when the user is attempted to be bucketed into all targeting rules // including Everyone Else rule [Test] @@ -669,17 +671,17 @@ public void TestGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargeti var expectedDecision = new FeatureDecision(everyoneElseRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), It.IsAny())).Returns(variation); - var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); + var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Provide null attributes so that user does not qualify for audience. - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, "user_1", null, ProjectConfig); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"User \"user_1\" does not meet the conditions to be in rollout rule for audience \"{ProjectConfig.AudienceIdMap[experiment0.AudienceIds[0]].Name}\".")); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, $"User \"user_1\" does not meet the conditions to be in rollout rule for audience \"{ProjectConfig.AudienceIdMap[experiment1.AudienceIds[0]].Name}\".")); } - + [Test] public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck() { @@ -695,14 +697,14 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( var mockBucketer = new Mock(LoggerMock.Object) { CallBase = true }; mockBucketer.Setup(bm => bm.GenerateBucketValue(It.IsAny())).Returns(980); - var decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); + var decisionService = new DecisionService(mockBucketer.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Calling with audience iPhone users in San Francisco. var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "device_type", "iPhone" }, { "location", "San Francisco" } - }); + }, ProjectConfig); // Returned variation id should be '177773' because of audience 'iPhone users in San Francisco'. var expectedDecision = new FeatureDecision(expWithAudienceiPhoneUsers, varWithAudienceiPhoneUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -712,7 +714,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "browser_type", "chrome" } - }); + }, ProjectConfig); // Returned variation id should be '177771' because of audience 'Chrome users'. expectedDecision = new FeatureDecision(expWithAudienceChromeUsers, varWithAudienceChromeUsers, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -720,7 +722,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( // Calling with no audience. mockBucketer.Setup(bm => bm.GenerateBucketValue(It.IsAny())).Returns(8000); - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes()); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes(), ProjectConfig); // Returned variation id should be of everyone else rule because of no audience. expectedDecision = new FeatureDecision(expWithNoAudience, varWithNoAudience, FeatureDecision.DECISION_SOURCE_ROLLOUT); @@ -731,7 +733,7 @@ public void TestGetVariationForFeatureRolloutAudienceAndTrafficeAllocationCheck( actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, new UserAttributes { { "browser_type", "chrome" } - }); + }, ProjectConfig); // Returned decision entity should be null because bucket value exceeds traffice allocation of everyone else rule. Assert.Null(actualDecision); @@ -749,19 +751,19 @@ public void TestGetVariationForFeatureRolloutCheckAudienceInEveryoneElseRule() BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), WhitelistedUserId)).Returns(variation); BucketerMock.Setup(bm => bm.Bucket(It.IsAny(), everyoneElseRule, It.IsAny(), GenericUserId)).Returns(null); - var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, ProjectConfig, null, LoggerMock.Object); + var decisionService = new DecisionService(BucketerMock.Object, ErrorHandlerMock.Object, null, LoggerMock.Object); // Returned variation id should be of everyone else rule as it passes audience Id checking. - var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null); + var actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, WhitelistedUserId, null, ProjectConfig); Assert.True(TestData.CompareObjects(expectedDecision, actualDecision)); // Returned variation id should be null. - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig); Assert.Null(actualDecision); // Returned variation id should be null as it fails audience Id checking. everyoneElseRule.AudienceIds = new string[] { ProjectConfig.Audiences[0].Id }; - actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null); + actualDecision = decisionService.GetVariationForFeatureRollout(featureFlag, GenericUserId, null, ProjectConfig); Assert.Null(actualDecision); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, "User \"testUser1\" does not meet the conditions to be in rollout rule for audience \"Chrome users\"."), Times.Once); @@ -784,10 +786,10 @@ public void TestGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment var variation = expectedExperiment.Variations[0]; var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny())).Returns(expectedDecision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), + It.IsAny(), ProjectConfig)).Returns(expectedDecision); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", new UserAttributes()); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); } @@ -803,12 +805,12 @@ public void TestGetVariationForFeatureWhenTheUserIsNotBucketedIntoFeatureExperim var variation = expectedExperiment.Variations[0]; var expectedDecision = new FeatureDecision(expectedExperiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), - It.IsAny())).Returns(null); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), - It.IsAny())).Returns(expectedDecision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), + It.IsAny(), ProjectConfig)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), + It.IsAny(), ProjectConfig)).Returns(expectedDecision); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", new UserAttributes()); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -822,10 +824,10 @@ public void TestGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExp var featureFlag = ProjectConfig.GetFeatureFlagFromKey("string_single_variable_feature"); var expectedDecision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny())).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureExperiment(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeatureRollout(It.IsAny(), It.IsAny(), It.IsAny(), ProjectConfig)).Returns(null); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", new UserAttributes()); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, new UserAttributes()); Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); LoggerMock.Verify(l => l.Log(LogLevel.INFO, "The user \"user1\" is not bucketed into a rollout for feature flag \"string_single_variable_feature\".")); @@ -844,8 +846,8 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR { "browser_type", "chrome" } }; - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", userAttributes)).Returns(variation); - var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, "user1", ProjectConfig, userAttributes)).Returns(variation); + var actualDecision = DecisionServiceMock.Object.GetVariationForFeatureExperiment(featureFlag, "user1", userAttributes, ProjectConfig); // The user is bucketed into feature experiment's variation. Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); @@ -856,13 +858,13 @@ public void TestGetVariationForFeatureWhenTheUserIsBuckedtedInBothExperimentAndR var expectedRolloutDecision = new FeatureDecision(rolloutExperiment, rolloutVariation, FeatureDecision.DECISION_SOURCE_ROLLOUT); BucketerMock.Setup(bm => bm.Bucket(ProjectConfig, rolloutExperiment, It.IsAny(), It.IsAny())).Returns(rolloutVariation); - var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes); + var actualRolloutDecision = DecisionServiceMock.Object.GetVariationForFeatureRollout(featureFlag, "user1", userAttributes, ProjectConfig); // The user is bucketed into feature rollout's variation. Assert.IsTrue(TestData.CompareObjects(expectedRolloutDecision, actualRolloutDecision)); - actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", userAttributes); + actualDecision = DecisionServiceMock.Object.GetVariationForFeature(featureFlag, "user1", ProjectConfig, userAttributes); // The user is bucketed into feature experiment's variation and not the rollout's variation. Assert.IsTrue(TestData.CompareObjects(expectedDecision, actualDecision)); diff --git a/OptimizelySDK.Tests/EntityTests/FeatureVariableTest.cs b/OptimizelySDK.Tests/EntityTests/FeatureVariableTest.cs new file mode 100644 index 00000000..86965648 --- /dev/null +++ b/OptimizelySDK.Tests/EntityTests/FeatureVariableTest.cs @@ -0,0 +1,19 @@ +using System; +using NUnit.Framework; +using OptimizelySDK.Entity; + +namespace OptimizelySDK.Tests.EntityTests +{ + [TestFixture] + public class FeatureVariableTest + { + [Test] + public void TestFeatureVariableTypeName() + { + Assert.AreEqual(FeatureVariable.GetFeatureVariableTypeName(FeatureVariable.VariableType.BOOLEAN), "GetFeatureVariableBoolean"); + Assert.AreEqual(FeatureVariable.GetFeatureVariableTypeName(FeatureVariable.VariableType.DOUBLE), "GetFeatureVariableDouble"); + Assert.AreEqual(FeatureVariable.GetFeatureVariableTypeName(FeatureVariable.VariableType.INTEGER), "GetFeatureVariableInteger"); + Assert.AreEqual(FeatureVariable.GetFeatureVariableTypeName(FeatureVariable.VariableType.STRING), "GetFeatureVariableString"); + } + } +} diff --git a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj index 25e09f67..553ff22b 100644 --- a/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj +++ b/OptimizelySDK.Tests/OptimizelySDK.Tests.csproj @@ -72,9 +72,9 @@ - - - + + + @@ -95,7 +95,8 @@ - + + diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index 0cb88ba4..38714a19 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -37,6 +37,7 @@ namespace OptimizelySDK.Tests public class OptimizelyTest { private Mock LoggerMock; + private ProjectConfigManager ConfigManager; private ProjectConfig Config; private Mock EventBuilderMock; private Mock ErrorHandlerMock; @@ -76,11 +77,12 @@ public void Initialize() EventBuilderMock.Setup(b => b.CreateConversionEvent(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())); - Config = DatafileProjectConfig.Create( + var config = DatafileProjectConfig.Create( content: TestData.Datafile, logger: LoggerMock.Object, errorHandler: new NoOpErrorHandler()); - + ConfigManager = new FallbackProjectConfigManager(config); + Config = ConfigManager.GetConfig(); EventDispatcherMock = new Mock(); Optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); @@ -100,7 +102,7 @@ public void Initialize() }; DecisionServiceMock = new Mock(new Bucketer(LoggerMock.Object), ErrorHandlerMock.Object, - Config, null, LoggerMock.Object); + null, LoggerMock.Object); NotificationCenter = new NotificationCenter(LoggerMock.Object); NotificationCallbackMock = new Mock(); @@ -180,6 +182,34 @@ public PrivateObject CreatePrivateOptimizely() #region Test Validate [Test] + public void TestInvalidInstanceLogMessages() + { + string datafile = "{\"name\":\"optimizely\"}"; + var optimizely = new Optimizely(datafile, null, LoggerMock.Object); + + Assert.IsNull(optimizely.GetVariation(string.Empty, string.Empty)); + Assert.IsNull(optimizely.Activate(string.Empty, string.Empty)); + optimizely.Track(string.Empty, string.Empty); + Assert.IsFalse(optimizely.IsFeatureEnabled(string.Empty, string.Empty)); + Assert.AreEqual(optimizely.GetEnabledFeatures(string.Empty).Count, 0); + Assert.IsNull(optimizely.GetFeatureVariableBoolean(string.Empty, string.Empty, string.Empty)); + Assert.IsNull(optimizely.GetFeatureVariableString(string.Empty, string.Empty, string.Empty)); + Assert.IsNull(optimizely.GetFeatureVariableDouble(string.Empty, string.Empty, string.Empty)); + Assert.IsNull(optimizely.GetFeatureVariableInteger(string.Empty, string.Empty, string.Empty)); + + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Provided 'datafile' has invalid schema."), Times.Once); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetVariation'."), Times.Once); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Activate'."), Times.Once); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Track'."), Times.Once); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'IsFeatureEnabled'."), Times.Once); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetEnabledFeatures'."), Times.Once); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetFeatureVariableBoolean'."), Times.Once); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetFeatureVariableString'."), Times.Once); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetFeatureVariableDouble'."), Times.Once); + LoggerMock.Verify(log => log.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetFeatureVariableInteger'."), Times.Once); + + } + [Test] public void TestValidateInputsInvalidFileJsonValidationNotSkipped() { string datafile = "{\"name\":\"optimizely\"}"; @@ -223,9 +253,12 @@ public void TestErrorHandlingWithUnsupportedConfigVersion() public void TestValidatePreconditionsExperimentNotRunning() { var optly = Helper.CreatePrivateOptimizely(); + + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); + bool result = (bool)optly.Invoke("ValidatePreconditions", Config.GetExperimentFromKey("paused_experiment"), - TestUserId, new UserAttributes { }); + TestUserId, ConfigManager.GetConfig(), new UserAttributes { }); Assert.IsFalse(result); } @@ -236,6 +269,7 @@ public void TestValidatePreconditionsExperimentRunning() bool result = (bool)optly.Invoke("ValidatePreconditions", Config.GetExperimentFromKey("test_experiment"), TestUserId, + ConfigManager.GetConfig(), new UserAttributes { { "device_type", "iPhone" }, @@ -251,7 +285,7 @@ public void TestValidatePreconditionsUserInForcedVariationNotInExperiment() var optly = Helper.CreatePrivateOptimizely(); bool result = (bool)optly.Invoke("ValidatePreconditions", Config.GetExperimentFromKey("test_experiment"), - "user1", new UserAttributes { }); + "user1", ConfigManager.GetConfig(), new UserAttributes { }); Assert.IsTrue(result); } @@ -261,7 +295,7 @@ public void TestValidatePreconditionsUserInForcedVariationInExperiment() var optly = Helper.CreatePrivateOptimizely(); bool result = (bool)optly.Invoke("ValidatePreconditions", Config.GetExperimentFromKey("test_experiment"), - "user1", new UserAttributes { }); + "user1", ConfigManager.GetConfig(), new UserAttributes { }); Assert.IsTrue(result); } @@ -271,7 +305,7 @@ public void TestValidatePreconditionsUserNotInForcedVariationNotInExperiment() var optly = Helper.CreatePrivateOptimizely(); bool result = (bool)optly.Invoke("ValidatePreconditions", Config.GetExperimentFromKey("test_experiment"), - TestUserId, new UserAttributes { }); + TestUserId, ConfigManager.GetConfig(), new UserAttributes { }); Assert.IsFalse(result); } @@ -497,7 +531,7 @@ public void TestGetVariationInvalidOptimizelyObject() var optly = new Optimizely("Random datafile", null, LoggerMock.Object); var variationkey = optly.Activate("some_experiment", "some_user"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'activate'."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Activate'."), Times.Once); //Assert.IsNull(variationkey); } @@ -556,7 +590,7 @@ public void TestTrackInvalidOptimizelyObject() var optly = new Optimizely("Random datafile", null, LoggerMock.Object); optly.Track("some_event", "some_user"); LoggerMock.Verify(l => l.Log(It.IsAny(), It.IsAny()), Times.Exactly(2)); - LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'track'."), Times.Once); + LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Track'."), Times.Once); } #endregion @@ -1100,7 +1134,7 @@ public void TestActivateNoAudienceNoAttributesAfterSetForcedVariation() var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("EventBuilder", EventBuilderMock.Object); - optly.SetFieldOrProperty("Config", Config); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); // Set forced variation Assert.True((bool)optly.Invoke("SetForcedVariation", experimentKey, userId, variationKey), "Set variation for paused experiment should have failed."); @@ -1272,7 +1306,7 @@ public void TestGetFeatureVariableIntegerReturnsCorrectValue() Assert.Null(OptimizelyMock.Object.GetFeatureVariableInteger(featureKey, variableNonInt, TestUserId, null)); OptimizelyMock.Setup(om => om.GetFeatureVariableValueForType(It.IsAny(), variableKeyNull, It.IsAny(), - It.IsAny(), featureVariableType)).Returns(null); + It.IsAny(), featureVariableType)).Returns(null); Assert.Null(OptimizelyMock.Object.GetFeatureVariableInteger(featureKey, variableKeyNull, TestUserId, null)); } @@ -1311,11 +1345,12 @@ public void TestGetFeatureVariableDoubleReturnsRightValueWhenUserBuckedIntoFeatu var variation = Config.GetVariationFromKey("test_experiment_double_feature", "control"); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); + var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); @@ -1339,11 +1374,12 @@ public void TestGetFeatureVariableIntegerReturnsRightValueWhenUserBuckedIntoFeat { "location", "San Francisco" } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, userAttributes)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); + var variableValue = (int)optly.Invoke("GetFeatureVariableInteger", featureKey, variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedValue, variableValue); @@ -1361,11 +1397,12 @@ public void TestGetFeatureVariableDoubleReturnsDefaultValueWhenUserBuckedIntoFea var variation = Config.GetVariationFromKey("test_experiment_double_feature", "variation"); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); + var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); @@ -1389,10 +1426,11 @@ public void TestGetFeatureVariableIntegerReturnsDefaultValueWhenUserBuckedIntoFe { "location", "San Francisco" } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, userAttributes)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var variableValue = (int)optly.Invoke("GetFeatureVariableInteger", featureKey, variableKey, TestUserId, userAttributes); Assert.AreEqual(expectedValue, variableValue); @@ -1411,11 +1449,11 @@ public void TestGetFeatureVariableBooleanReturnsRightValueWhenUserBuckedIntoRoll var variation = Config.GetVariationFromKey(experiment.Key, "177771"); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); @@ -1439,9 +1477,11 @@ public void TestGetFeatureVariableStringReturnsRightValueWhenUserBuckedIntoRollo { "location", "San Francisco" } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, userAttributes)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); + + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); var variableValue = (string)optly.Invoke("GetFeatureVariableString", featureKey, variableKey, TestUserId, userAttributes); @@ -1461,11 +1501,11 @@ public void TestGetFeatureVariableBooleanReturnsDefaultValueWhenUserBuckedIntoRo var variation = Config.GetVariationFromKey(experiment.Key, "177782"); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); @@ -1489,9 +1529,10 @@ public void TestGetFeatureVariableStringReturnsDefaultValueWhenUserBuckedIntoRol { "location", "San Francisco" } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, userAttributes)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); var variableValue = (string)optly.Invoke("GetFeatureVariableString", featureKey, variableKey, TestUserId, userAttributes); @@ -1509,10 +1550,11 @@ public void TestGetFeatureVariableDoubleReturnsDefaultValueWhenUserNotBuckedInto var expectedValue = 14.99; var decision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); Assert.AreEqual(expectedValue, variableValue); @@ -1605,9 +1647,11 @@ public void TestGetFeatureVariableValueForTypeGivenFeatureFlagIsNotEnabledForUse var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); + + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); var variableValue = (double?)optly.InvokeGeneric("GetFeatureVariableValueForType", new Type[] { typeof(double?) }, featureKey, variableKey, TestUserId, null, variableType); @@ -1632,9 +1676,11 @@ public void TestGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAn var expectedValue = 14.99; // Mock GetVariationForFeature method to return variation of different feature. - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(expectedDecision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(expectedDecision); var optly = Helper.CreatePrivateOptimizely(); + + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); var variableValue = (double?)optly.InvokeGeneric("GetFeatureVariableValueForType", new Type[] { typeof(double?) }, featureKey, variableKey, TestUserId, null, variableType); @@ -1658,11 +1704,13 @@ public void TestGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAn var variation = Config.GetVariationFromKey("test_experiment_double_feature", "control"); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); + + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); - + var variableValue = (double?)optly.InvokeGeneric("GetFeatureVariableValueForType", new Type[] { typeof(double?) }, featureKey, variableKey, TestUserId, null, variableType); Assert.AreEqual(expectedValue, variableValue); @@ -1683,9 +1731,11 @@ public void TestGetFeatureVariableValueForTypeWithRolloutRule() var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); var expectedVariableValue = false; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); + + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); // Calling GetFeatureVariableBoolean to get GetFeatureVariableValueForType returned value casted in bool. @@ -1731,10 +1781,11 @@ public void TestIsFeatureEnabledGivenFeatureFlagNotFound() public void TestIsFeatureEnabledGivenFeatureFlagContainsInvalidExperiment() { var tempConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, new NoOpErrorHandler()); + var tempConfigManager = new FallbackProjectConfigManager(tempConfig); var featureFlag = tempConfig.GetFeatureFlagFromKey("multi_variate_feature"); var optly = Helper.CreatePrivateOptimizely(); - optly.SetFieldOrProperty("Config", tempConfig); + optly.SetFieldOrProperty("ProjectConfigManager", tempConfigManager); // Set such an experiment to the list of experiment ids, that does not belong to the feature. featureFlag.ExperimentIds = new List { "4209211" }; @@ -1755,10 +1806,11 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser() var variation = Config.GetVariationFromKey("test_experiment_double_feature", "variation"); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, null); Assert.False(result); @@ -1779,10 +1831,11 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingExperi var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, null); Assert.True(result); @@ -1806,10 +1859,11 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExperimen var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, null); Assert.True(result); @@ -1833,10 +1887,11 @@ public void TestIsFeatureEnabledGivenFeatureFlagIsNotEnabledAndUserIsBeingExperi var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, null); Assert.False(result); @@ -1884,8 +1939,8 @@ public void TestIsFeatureEnabledWithFeatureEnabledPropertyGivenFeatureExperiment var decisionTrue = new FeatureDecision(experiment, featureEnabledTrue, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); var decisionFalse = new FeatureDecision(experiment, featureEnabledFalse, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decisionTrue); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, userId, null)).Returns(decisionFalse); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decisionTrue); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, userId, Config, null)).Returns(decisionFalse); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -1907,7 +1962,7 @@ public void TestIsFeatureEnabledWithFeatureEnabledPropertyGivenRolloutRule() // Verify that IsFeatureEnabled returns true when user is bucketed into the rollout rule's variation. Assert.True(Optimizely.IsFeatureEnabled("boolean_single_variable_feature", TestUserId)); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(null); var optly = Helper.CreatePrivateOptimizely(); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -1960,8 +2015,8 @@ public void TestActivateListener(UserAttributes userAttributes) It.IsAny(), It.IsAny(), It.IsAny())); EventBuilderMock.Setup(ebm => ebm.CreateImpressionEvent(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(logEvent); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, userAttributes)).Returns(variation); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, It.IsAny(), userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, It.IsAny(), userAttributes)).Returns(decision); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; @@ -2041,7 +2096,7 @@ public void TestTrackListener(UserAttributes userAttributes, EventTags eventTags EventBuilderMock.Setup(ebm => ebm.CreateConversionEvent(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(logEvent); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, Config, userAttributes)).Returns(variation); // Adding notification listeners. var notificationType = NotificationCenter.NotificationType.Track; @@ -2078,9 +2133,10 @@ public void TestActivateSendsDecisionNotificationWithActualVariationKey() // Mocking objects. NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, Config, userAttributes)).Returns(variation); var optly = Helper.CreatePrivateOptimizely(); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); @@ -2113,9 +2169,10 @@ public void TestActivateSendsDecisionNotificationWithVariationKeyAndTypeFeatureT // Mocking objects. NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, Config, userAttributes)).Returns(variation); var optly = Helper.CreatePrivateOptimizely(); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); @@ -2139,7 +2196,7 @@ public void TestActivateSendsDecisionNotificationWithNullVariationKey() NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, null)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, Config, null)).Returns(null); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; @@ -2174,9 +2231,11 @@ public void TestGetVariationSendsDecisionNotificationWithActualVariationKey() // Mocking objects. NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, Config, userAttributes)).Returns(variation); var optly = Helper.CreatePrivateOptimizely(); + + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); @@ -2209,13 +2268,14 @@ public void TestGetVariationSendsDecisionNotificationWithVariationKeyAndTypeFeat // Mocking objects. NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, userAttributes)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, Config, userAttributes)).Returns(variation); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optly.Invoke("GetVariation", experimentKey, TestUserId, userAttributes); var decisionInfo = new Dictionary @@ -2235,7 +2295,7 @@ public void TestGetVariationSendsDecisionNotificationWithNullVariationKey() NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, null)).Returns(null); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, Config, null)).Returns(null); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; @@ -2260,10 +2320,10 @@ public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledTrueF var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, null)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, ConfigManager.GetConfig(), null)).Returns(variation); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; @@ -2295,16 +2355,17 @@ public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalse var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); - DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, null)).Returns(variation); + DecisionServiceMock.Setup(ds => ds.GetVariation(experiment, TestUserId, Config, null)).Returns(variation); var optly = Helper.CreatePrivateOptimizely(); var optStronglyTyped = optly.GetObject() as Optimizely; optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, null); Assert.False(result); @@ -2339,11 +2400,12 @@ public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledTrueF var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, userAttributes)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); @@ -2377,7 +2439,7 @@ public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalse var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, userAttributes)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); @@ -2386,6 +2448,7 @@ public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalse optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, userAttributes); Assert.False(result); @@ -2408,7 +2471,7 @@ public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalse var featureFlag = Config.GetFeatureFlagFromKey(featureKey); var decision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); @@ -2417,6 +2480,7 @@ public void TestIsFeatureEnabledSendsDecisionNotificationWithFeatureEnabledFalse optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); bool result = (bool)optly.Invoke("IsFeatureEnabled", featureKey, TestUserId, null); Assert.False(result); @@ -2547,7 +2611,7 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatu var variation = Config.GetVariationFromKey("test_experiment_double_feature", "control"); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); @@ -2555,6 +2619,8 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatu var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); + optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); @@ -2595,11 +2661,13 @@ public void TestGetFeatureVariableIntegerSendsNotificationWhenUserBuckedIntoFeat { "location", "San Francisco" } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, userAttributes)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); + var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2637,7 +2705,7 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatu var variation = Config.GetVariationFromKey("test_experiment_double_feature", "variation"); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_FEATURE_TEST); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); @@ -2645,6 +2713,7 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserBuckedIntoFeatu var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); @@ -2685,11 +2754,12 @@ public void TestGetFeatureVariableIntegerSendsNotificationWhenUserBuckedIntoFeat { "location", "San Francisco" } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, userAttributes)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2727,11 +2797,12 @@ public void TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRoll var variation = Config.GetVariationFromKey(experiment.Key, "177771"); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2770,11 +2841,13 @@ public void TestGetFeatureVariableStringSendsNotificationWhenUserBuckedIntoRollo { "location", "San Francisco" } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, userAttributes)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); + + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2807,7 +2880,7 @@ public void TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRoll var variation = Config.GetVariationFromKey(experiment.Key, "177782"); var decision = new FeatureDecision(experiment, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); @@ -2815,6 +2888,7 @@ public void TestGetFeatureVariableBooleanSendsNotificationWhenUserBuckedIntoRoll var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); var variableValue = (bool)optly.Invoke("GetFeatureVariableBoolean", featureKey, variableKey, TestUserId, null); @@ -2850,11 +2924,12 @@ public void TestGetFeatureVariableStringSendsNotificationWhenUserBuckedIntoRollo { "location", "San Francisco" } }; - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, userAttributes)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, userAttributes)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); var optly = Helper.CreatePrivateOptimizely(); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); @@ -2885,7 +2960,7 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserNotBuckedIntoBo var expectedValue = 14.99; var decision = new FeatureDecision(null, null, FeatureDecision.DECISION_SOURCE_ROLLOUT); - DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, null)).Returns(decision); + DecisionServiceMock.Setup(ds => ds.GetVariationForFeature(featureFlag, TestUserId, Config, null)).Returns(decision); NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())); @@ -2893,6 +2968,7 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserNotBuckedIntoBo var optStronglyTyped = optly.GetObject() as Optimizely; optly.SetFieldOrProperty("DecisionService", DecisionServiceMock.Object); + optly.SetFieldOrProperty("ProjectConfigManager", ConfigManager); optStronglyTyped.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); var variableValue = (double)optly.Invoke("GetFeatureVariableDouble", featureKey, variableKey, TestUserId, null); @@ -2921,6 +2997,7 @@ public void TestGetFeatureVariableDoubleSendsNotificationWhenUserNotBuckedIntoBo public void TestGetEnabledFeaturesWithInvalidDatafile() { var optly = new Optimizely("Random datafile", null, LoggerMock.Object); + Assert.IsEmpty(optly.GetEnabledFeatures("some_user", null)); LoggerMock.Verify(l => l.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetEnabledFeatures'."), Times.Once); diff --git a/OptimizelySDK.Tests/ProjectConfigTest.cs b/OptimizelySDK.Tests/ProjectConfigTest.cs index 2ee770c2..47f47d2b 100644 --- a/OptimizelySDK.Tests/ProjectConfigTest.cs +++ b/OptimizelySDK.Tests/ProjectConfigTest.cs @@ -669,41 +669,41 @@ public void TestSetGetForcedVariation() var optlyObject = new Optimizely(TestData.Datafile, new ValidEventDispatcher(), LoggerMock.Object); optlyObject.Activate("test_experiment", "test_user", userAttributes); - // invalid experiment key should return a null variation + // invalid experiment key should return a null variation Assert.False(Config.SetForcedVariation(invalidExperimentKey, userId, expectedVariationKey)); Assert.Null(Config.GetForcedVariation(invalidExperimentKey, userId)); - // setting a null variation should return a null variation + // setting a null variation should return a null variation Assert.True(Config.SetForcedVariation(experimentKey, userId, null)); Assert.Null(Config.GetForcedVariation(experimentKey, userId)); - // setting an invalid variation should return a null variation + // setting an invalid variation should return a null variation Assert.False(Config.SetForcedVariation(experimentKey, userId, invalidVariationKey)); Assert.Null(Config.GetForcedVariation(experimentKey, userId)); - // confirm the forced variation is returned after a set + // confirm the forced variation is returned after a set Assert.True(Config.SetForcedVariation(experimentKey, userId, expectedVariationKey)); var actualForcedVariation = Config.GetForcedVariation(experimentKey, userId); Assert.AreEqual(expectedVariationKey, actualForcedVariation.Key); - // check multiple sets + // check multiple sets Assert.True(Config.SetForcedVariation(experimentKey2, userId, expectedVariationKey2)); var actualForcedVariation2 = Config.GetForcedVariation(experimentKey2, userId); Assert.AreEqual(expectedVariationKey2, actualForcedVariation2.Key); - // make sure the second set does not overwrite the first set + // make sure the second set does not overwrite the first set actualForcedVariation = Config.GetForcedVariation(experimentKey, userId); Assert.AreEqual(expectedVariationKey, actualForcedVariation.Key); - // make sure unsetting the second experiment-to-variation mapping does not unset the - // first experiment-to-variation mapping + // make sure unsetting the second experiment-to-variation mapping does not unset the + // first experiment-to-variation mapping Assert.True(Config.SetForcedVariation(experimentKey2, userId, null)); actualForcedVariation = Config.GetForcedVariation(experimentKey, userId); Assert.AreEqual(expectedVariationKey, actualForcedVariation.Key); - // an invalid user ID should return a null variation + // an invalid user ID should return a null variation Assert.Null(Config.GetForcedVariation(experimentKey, invalidUserId)); } - // test that all the logs in setForcedVariation are getting called + // test that all the logs in setForcedVariation are getting called [Test] public void TestSetForcedVariationLogs() { @@ -714,7 +714,7 @@ public void TestSetForcedVariationLogs() var variationKey = "control"; var variationId = "7722370027"; var invalidVariationKey = "invalid_variation"; - + Config.SetForcedVariation(invalidExperimentKey, userId, variationKey); Config.SetForcedVariation(experimentKey, userId, null); Config.SetForcedVariation(experimentKey, userId, invalidVariationKey); @@ -727,7 +727,7 @@ public void TestSetForcedVariationLogs() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId))); } - // test that all the logs in getForcedVariation are getting called + // test that all the logs in getForcedVariation are getting called [Test] public void TestGetForcedVariationLogs() { @@ -753,30 +753,30 @@ public void TestGetForcedVariationLogs() LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"No experiment ""{0}"" mapped to user ""{1}"" in the forced variation map.", pausedExperimentKey, userId))); LoggerMock.Verify(l => l.Log(LogLevel.DEBUG, string.Format(@"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", variationKey, experimentKey, userId))); } - + [Test] public void TestSetForcedVariationMultipleSets() { Assert.True(Config.SetForcedVariation("test_experiment", "test_user_1", "variation")); Assert.AreEqual(Config.GetForcedVariation("test_experiment", "test_user_1").Key, "variation"); - - // same user, same experiment, different variation + + // same user, same experiment, different variation Assert.True(Config.SetForcedVariation("test_experiment", "test_user_1", "control")); Assert.AreEqual(Config.GetForcedVariation("test_experiment", "test_user_1").Key, "control"); - - // same user, different experiment + + // same user, different experiment Assert.True(Config.SetForcedVariation("group_experiment_1", "test_user_1", "group_exp_1_var_1")); Assert.AreEqual(Config.GetForcedVariation("group_experiment_1", "test_user_1").Key, "group_exp_1_var_1"); - // different user + // different user Assert.True(Config.SetForcedVariation("test_experiment", "test_user_2", "variation")); Assert.AreEqual(Config.GetForcedVariation("test_experiment", "test_user_2").Key, "variation"); - - // different user, different experiment + + // different user, different experiment Assert.True(Config.SetForcedVariation("group_experiment_1", "test_user_2", "group_exp_1_var_1")); Assert.AreEqual(Config.GetForcedVariation("group_experiment_1", "test_user_2").Key, "group_exp_1_var_1"); - // make sure the first user forced variations are still valid + // make sure the first user forced variations are still valid Assert.AreEqual(Config.GetForcedVariation("test_experiment", "test_user_1").Key, "control"); Assert.AreEqual(Config.GetForcedVariation("group_experiment_1", "test_user_1").Key, "group_exp_1_var_1"); } diff --git a/OptimizelySDK/Bucketing/DecisionService.cs b/OptimizelySDK/Bucketing/DecisionService.cs index e9369a26..1fd75b15 100644 --- a/OptimizelySDK/Bucketing/DecisionService.cs +++ b/OptimizelySDK/Bucketing/DecisionService.cs @@ -37,7 +37,6 @@ public class DecisionService { private Bucketer Bucketer; private IErrorHandler ErrorHandler; - private ProjectConfig ProjectConfig; private UserProfileService UserProfileService; private ILogger Logger; @@ -46,14 +45,12 @@ public class DecisionService /// /// Base bucketer to allocate new users to an experiment. /// The error handler of the Optimizely client. - /// Optimizely Project Config representing the datafile. /// /// < param name= "logger" > UserProfileService implementation for storing user info. - public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, ProjectConfig projectConfig, UserProfileService userProfileService, ILogger logger) + public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, UserProfileService userProfileService, ILogger logger) { Bucketer = bucketer; ErrorHandler = errorHandler; - ProjectConfig = projectConfig; UserProfileService = userProfileService; Logger = logger; } @@ -65,12 +62,12 @@ public DecisionService(Bucketer bucketer, IErrorHandler errorHandler, ProjectCon /// The userId of the user. /// The user's attributes. This should be filtered to just attributes in the Datafile. /// The Variation the user is allocated into. - public virtual Variation GetVariation(Experiment experiment, string userId, UserAttributes filteredAttributes) + public virtual Variation GetVariation(Experiment experiment, string userId, ProjectConfig config, UserAttributes filteredAttributes) { if (!ExperimentUtils.IsExperimentActive(experiment, Logger)) return null; // check if a forced variation is set - var forcedVariation = ProjectConfig.GetForcedVariation(experiment.Key, userId); + var forcedVariation = config.GetForcedVariation(experiment.Key, userId); if (forcedVariation != null) return forcedVariation; @@ -86,7 +83,7 @@ public virtual Variation GetVariation(Experiment experiment, string userId, User if (userProfileMap != null && UserProfileUtil.IsValidUserProfileMap(userProfileMap)) { userProfile = UserProfileUtil.ConvertMapToUserProfile(userProfileMap); - variation = GetStoredVariation(experiment, userProfile); + variation = GetStoredVariation(experiment, userProfile, config); if (variation != null) return variation; } else if (userProfileMap == null) @@ -105,12 +102,12 @@ public virtual Variation GetVariation(Experiment experiment, string userId, User } } - if (ExperimentUtils.IsUserInExperiment(ProjectConfig, experiment, filteredAttributes, Logger)) + if (ExperimentUtils.IsUserInExperiment(config, experiment, filteredAttributes, Logger)) { // Get Bucketing ID from user attributes. string bucketingId = GetBucketingId(userId, filteredAttributes); - variation = Bucketer.Bucket(ProjectConfig, experiment, bucketingId, userId); + variation = Bucketer.Bucket(config, experiment, bucketingId, userId); if (variation != null && variation.Key != null) { @@ -165,7 +162,7 @@ public Variation GetWhitelistedVariation(Experiment experiment, string userId) /// which the user was bucketed /// User profile of the user /// The user was previously bucketed into. - public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile) + public Variation GetStoredVariation(Experiment experiment, UserProfile userProfile, ProjectConfig config) { // ---------- Check User Profile for Sticky Bucketing ---------- // If a user profile instance is present then check it for a saved variation @@ -185,8 +182,8 @@ public Variation GetStoredVariation(Experiment experiment, UserProfile userProfi { string variationId = decision.VariationId; - Variation savedVariation = ProjectConfig.ExperimentIdMap[experimentId].VariationIdToVariationMap.ContainsKey(variationId) - ? ProjectConfig.ExperimentIdMap[experimentId].VariationIdToVariationMap[variationId] + Variation savedVariation = config.ExperimentIdMap[experimentId].VariationIdToVariationMap.ContainsKey(variationId) + ? config.ExperimentIdMap[experimentId].VariationIdToVariationMap[variationId] : null; if (savedVariation == null) @@ -255,7 +252,7 @@ public void SaveVariation(Experiment experiment, Variation variation, UserProfil /// The user's attributes. This should be filtered to just attributes in the Datafile. /// null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// otherwise the FeatureDecision entity - public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes) + public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) { if (featureFlag == null) { @@ -269,7 +266,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature return null; } - Rollout rollout = ProjectConfig.GetRolloutFromId(featureFlag.RolloutId); + Rollout rollout = config.GetRolloutFromId(featureFlag.RolloutId); if (string.IsNullOrEmpty(rollout.Id)) { @@ -287,9 +284,9 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature for (int i=0; i < rolloutRulesLength - 1; i++) { var rolloutRule = rollout.Experiments[i]; - if (ExperimentUtils.IsUserInExperiment(ProjectConfig, rolloutRule, filteredAttributes, Logger)) + if (ExperimentUtils.IsUserInExperiment(config, rolloutRule, filteredAttributes, Logger)) { - variation = Bucketer.Bucket(ProjectConfig, rolloutRule, bucketingId, userId); + variation = Bucketer.Bucket(config, rolloutRule, bucketingId, userId); if (variation == null || string.IsNullOrEmpty(variation.Id)) break; @@ -297,22 +294,22 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature } else { - var audience = ProjectConfig.GetAudience(rolloutRule.AudienceIds[0]); + var audience = config.GetAudience(rolloutRule.AudienceIds[0]); Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" does not meet the conditions to be in rollout rule for audience \"{audience.Name}\"."); } } // Get the last rule which is everyone else rule. var everyoneElseRolloutRule = rollout.Experiments[rolloutRulesLength - 1]; - if (ExperimentUtils.IsUserInExperiment(ProjectConfig, everyoneElseRolloutRule, filteredAttributes, Logger)) + if (ExperimentUtils.IsUserInExperiment(config, everyoneElseRolloutRule, filteredAttributes, Logger)) { - variation = Bucketer.Bucket(ProjectConfig, everyoneElseRolloutRule, bucketingId, userId); + variation = Bucketer.Bucket(config, everyoneElseRolloutRule, bucketingId, userId); if (variation != null && !string.IsNullOrEmpty(variation.Id)) return new FeatureDecision(everyoneElseRolloutRule, variation, FeatureDecision.DECISION_SOURCE_ROLLOUT); } else { - var audience = ProjectConfig.GetAudience(everyoneElseRolloutRule.AudienceIds[0]); + var audience = config.GetAudience(everyoneElseRolloutRule.AudienceIds[0]); Logger.Log(LogLevel.DEBUG, $"User \"{userId}\" does not meet the conditions to be in rollout rule for audience \"{audience.Name}\"."); } @@ -327,7 +324,7 @@ public virtual FeatureDecision GetVariationForFeatureRollout(FeatureFlag feature /// The user's attributes. This should be filtered to just attributes in the Datafile. /// null if the user is not bucketed into the rollout or if the feature flag was not attached to a rollout. /// Otherwise the FeatureDecision entity - public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes) + public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes, ProjectConfig config) { if (featureFlag == null) { @@ -343,12 +340,12 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat foreach (var experimentId in featureFlag.ExperimentIds) { - var experiment = ProjectConfig.GetExperimentFromId(experimentId); + var experiment = config.GetExperimentFromId(experimentId); if (string.IsNullOrEmpty(experiment.Key)) continue; - var variation = GetVariation(experiment, userId, filteredAttributes); + var variation = GetVariation(experiment, userId, config, filteredAttributes); if (variation != null && !string.IsNullOrEmpty(variation.Id)) { @@ -369,16 +366,16 @@ public virtual FeatureDecision GetVariationForFeatureExperiment(FeatureFlag feat /// The user's attributes. This should be filtered to just attributes in the Datafile. /// null if the user is not bucketed into any variation or the FeatureDecision entity if the user is /// successfully bucketed. - public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, UserAttributes filteredAttributes) + public virtual FeatureDecision GetVariationForFeature(FeatureFlag featureFlag, string userId, ProjectConfig config, UserAttributes filteredAttributes) { // Check if the feature flag has an experiment and the user is bucketed into that experiment. - var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes); + var decision = GetVariationForFeatureExperiment(featureFlag, userId, filteredAttributes, config); if (decision != null) return decision; // Check if the feature flag has rollout and the the user is bucketed into one of its rules. - decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes); + decision = GetVariationForFeatureRollout(featureFlag, userId, filteredAttributes, config); if (decision != null) { diff --git a/OptimizelySDK/Config/DatafileProjectConfig.cs b/OptimizelySDK/Config/DatafileProjectConfig.cs index 82c2878d..bd9b6e87 100644 --- a/OptimizelySDK/Config/DatafileProjectConfig.cs +++ b/OptimizelySDK/Config/DatafileProjectConfig.cs @@ -24,8 +24,14 @@ namespace OptimizelySDK.Config { + /// + /// Implementation of ProjectConfig interface backed by a JSON data file. + /// public class DatafileProjectConfig : ProjectConfig { + /// + /// Datafile versions. + /// public enum OPTLYSDKVersion { V2 = 2, @@ -33,6 +39,9 @@ public enum OPTLYSDKVersion V4 = 4 } + /// + /// Prefix used for reserved attributes. + /// public const string RESERVED_ATTRIBUTE_PREFIX = "$opt_"; /// @@ -68,7 +77,9 @@ public enum OPTLYSDKVersion /// public bool? BotFiltering { get; set; } - + /// + /// Supported datafile versions list. + /// private static List SupportedVersions = new List { OPTLYSDKVersion.V2, OPTLYSDKVersion.V3, @@ -128,12 +139,12 @@ private Dictionary> _VariationIdMap private Dictionary _AudienceIdMap; public Dictionary AudienceIdMap { get { return _AudienceIdMap; } } - /// - /// Associative array of user IDs to an associative array - /// of experiments to variations.This contains all the forced variations - /// set by the user by calling setForcedVariation (it is not the same as the - /// whitelisting forcedVariations data structure in the Experiments class). - /// + /// + /// Associative array of user IDs to an associative array + /// of experiments to variations.This contains all the forced variations + /// set by the user by calling setForcedVariation (it is not the same as the + /// whitelisting forcedVariations data structure in the Experiments class). + /// private Dictionary> _ForcedVariationMap; public Dictionary> ForcedVariationMap { get { return _ForcedVariationMap; } } @@ -307,6 +318,13 @@ private void Initialize() } } + /// + /// Parse datafile string to create ProjectConfig instance. + /// + /// datafile + /// Logger instance + /// ErrorHandler instance + /// ProjectConfig instance created from datafile string public static ProjectConfig Create(string content, ILogger logger, IErrorHandler errorHandler) { DatafileProjectConfig config = GetConfig(content); @@ -473,102 +491,102 @@ public Variation GetVariationFromId(string experimentKey, string variationId) return new Variation(); } - /// - /// Gets the forced variation for the given user and experiment. - /// - /// The experiment key - /// The user ID - /// Variation entity which the given user and experiment should be forced into. - public Variation GetForcedVariation(string experimentKey, string userId) - { - if (_ForcedVariationMap.ContainsKey(userId) == false) - { - Logger.Log(LogLevel.DEBUG, string.Format(@"User ""{0}"" is not in the forced variation map.", userId)); - return null; - } - - Dictionary experimentToVariationMap = _ForcedVariationMap[userId]; - - string experimentId = GetExperimentFromKey(experimentKey).Id; - - // this case is logged in getExperimentFromKey - if (string.IsNullOrEmpty(experimentId)) - return null; - - if (experimentToVariationMap.ContainsKey(experimentId) == false) - { - Logger.Log(LogLevel.DEBUG, string.Format(@"No experiment ""{0}"" mapped to user ""{1}"" in the forced variation map.", experimentKey, userId)); - return null; - } - - string variationId = experimentToVariationMap[experimentId]; - - if (string.IsNullOrEmpty(variationId)) - { - Logger.Log(LogLevel.DEBUG, string.Format(@"No variation mapped to experiment ""{0}"" in the forced variation map.", experimentKey)); - return null; - } - - string variationKey = GetVariationFromId(experimentKey, variationId).Key; - - // this case is logged in getVariationFromKey - if (string.IsNullOrEmpty(variationKey)) - return null; - - Logger.Log(LogLevel.DEBUG, string.Format(@"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", variationKey, experimentKey, userId)); - - Variation variation = GetVariationFromKey(experimentKey, variationKey); - - return variation; - } - - /// - /// Sets an associative array of user IDs to an associative array of experiments to forced variations. - /// - /// The experiment key - /// The user ID - /// The variation key - /// A boolean value that indicates if the set completed successfully. - public bool SetForcedVariation(string experimentKey, string userId, string variationKey) - { - // Empty variation key is considered as invalid. - if (variationKey != null && variationKey.Length == 0) - { - Logger.Log(LogLevel.DEBUG, "Variation key is invalid."); - return false; - } - - var experimentId = GetExperimentFromKey(experimentKey).Id; - - // this case is logged in getExperimentFromKey - if (string.IsNullOrEmpty(experimentId)) - return false; - - // clear the forced variation if the variation key is null - if (variationKey == null) - { - if (_ForcedVariationMap.ContainsKey(userId) && _ForcedVariationMap[userId].ContainsKey(experimentId)) - _ForcedVariationMap[userId].Remove(experimentId); - - Logger.Log(LogLevel.DEBUG, string.Format(@"Variation mapped to experiment ""{0}"" has been removed for user ""{1}"".", experimentKey, userId)); - return true; - } - - string variationId = GetVariationFromKey(experimentKey, variationKey).Id; - - // this case is logged in getVariationFromKey - if (string.IsNullOrEmpty(variationId)) - return false; - - // Add User if not exist. - if (_ForcedVariationMap.ContainsKey(userId) == false) - _ForcedVariationMap[userId] = new Dictionary(); - - // Add/Replace Experiment to Variation ID map. - _ForcedVariationMap[userId][experimentId] = variationId; - - Logger.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId)); - return true; + /// + /// Gets the forced variation for the given user and experiment. + /// + /// The experiment key + /// The user ID + /// Variation entity which the given user and experiment should be forced into. + public Variation GetForcedVariation(string experimentKey, string userId) + { + if (_ForcedVariationMap.ContainsKey(userId) == false) + { + Logger.Log(LogLevel.DEBUG, string.Format(@"User ""{0}"" is not in the forced variation map.", userId)); + return null; + } + + Dictionary experimentToVariationMap = _ForcedVariationMap[userId]; + + string experimentId = GetExperimentFromKey(experimentKey).Id; + + // this case is logged in getExperimentFromKey + if (string.IsNullOrEmpty(experimentId)) + return null; + + if (experimentToVariationMap.ContainsKey(experimentId) == false) + { + Logger.Log(LogLevel.DEBUG, string.Format(@"No experiment ""{0}"" mapped to user ""{1}"" in the forced variation map.", experimentKey, userId)); + return null; + } + + string variationId = experimentToVariationMap[experimentId]; + + if (string.IsNullOrEmpty(variationId)) + { + Logger.Log(LogLevel.DEBUG, string.Format(@"No variation mapped to experiment ""{0}"" in the forced variation map.", experimentKey)); + return null; + } + + string variationKey = GetVariationFromId(experimentKey, variationId).Key; + + // this case is logged in getVariationFromKey + if (string.IsNullOrEmpty(variationKey)) + return null; + + Logger.Log(LogLevel.DEBUG, string.Format(@"Variation ""{0}"" is mapped to experiment ""{1}"" and user ""{2}"" in the forced variation map", variationKey, experimentKey, userId)); + + Variation variation = GetVariationFromKey(experimentKey, variationKey); + + return variation; + } + + /// + /// Sets an associative array of user IDs to an associative array of experiments to forced variations. + /// + /// The experiment key + /// The user ID + /// The variation key + /// A boolean value that indicates if the set completed successfully. + public bool SetForcedVariation(string experimentKey, string userId, string variationKey) + { + // Empty variation key is considered as invalid. + if (variationKey != null && variationKey.Length == 0) + { + Logger.Log(LogLevel.DEBUG, "Variation key is invalid."); + return false; + } + + var experimentId = GetExperimentFromKey(experimentKey).Id; + + // this case is logged in getExperimentFromKey + if (string.IsNullOrEmpty(experimentId)) + return false; + + // clear the forced variation if the variation key is null + if (variationKey == null) + { + if (_ForcedVariationMap.ContainsKey(userId) && _ForcedVariationMap[userId].ContainsKey(experimentId)) + _ForcedVariationMap[userId].Remove(experimentId); + + Logger.Log(LogLevel.DEBUG, string.Format(@"Variation mapped to experiment ""{0}"" has been removed for user ""{1}"".", experimentKey, userId)); + return true; + } + + string variationId = GetVariationFromKey(experimentKey, variationKey).Id; + + // this case is logged in getVariationFromKey + if (string.IsNullOrEmpty(variationId)) + return false; + + // Add User if not exist. + if (_ForcedVariationMap.ContainsKey(userId) == false) + _ForcedVariationMap[userId] = new Dictionary(); + + // Add/Replace Experiment to Variation ID map. + _ForcedVariationMap[userId][experimentId] = variationId; + + Logger.Log(LogLevel.DEBUG, string.Format(@"Set variation ""{0}"" for experiment ""{1}"" and user ""{2}"" in the forced variation map.", variationId, experimentId, userId)); + return true; } /// diff --git a/OptimizelySDK/Config/FallbackProjectConfigManager.cs b/OptimizelySDK/Config/FallbackProjectConfigManager.cs index 5b08d23f..6f0d7b71 100644 --- a/OptimizelySDK/Config/FallbackProjectConfigManager.cs +++ b/OptimizelySDK/Config/FallbackProjectConfigManager.cs @@ -16,20 +16,31 @@ namespace OptimizelySDK.Config { - /// TODO: Cleanup done in other PR. sohail/optimizelydfm + /// + /// Implementation of ProjectConfigManager interface that simply + /// returns the stored ProjectConfig instance which is immmutable. + /// public class FallbackProjectConfigManager : ProjectConfigManager { private ProjectConfig ProjectConfig; - public ProjectConfig GetConfig() + /// + /// Initializes a new instance of the FallbackProjectConfigManager class + /// with the given ProjectConfig instance. + /// + /// Config. + public FallbackProjectConfigManager(ProjectConfig config) { - return ProjectConfig; + ProjectConfig = config; } - public bool SetConfig(ProjectConfig projectConfig) + /// + /// Returns the stored ProjectConfig instance. + /// + /// ProjectConfig instance + public ProjectConfig GetConfig() { - ProjectConfig = projectConfig; - return true; + return ProjectConfig; } } } diff --git a/OptimizelySDK/Config/PollingProjectConfigManager.cs b/OptimizelySDK/Config/PollingProjectConfigManager.cs index 24c621b1..f61334c5 100644 --- a/OptimizelySDK/Config/PollingProjectConfigManager.cs +++ b/OptimizelySDK/Config/PollingProjectConfigManager.cs @@ -23,6 +23,12 @@ namespace OptimizelySDK.Config { + /// + /// Abstract class that implements ProjectConfigManager interface and provides + /// basic scheduling and caching. + /// Instances of this class, must implement the method + /// which is responsible for fetching a given ProjectConfig. + /// public abstract class PollingProjectConfigManager : ProjectConfigManager { private TimeSpan PollingInterval; @@ -57,8 +63,15 @@ public PollingProjectConfigManager(TimeSpan period, TimeSpan blockingTimeout, IL } + /// + /// Abstract method for fetching ProjectConfig instance. + /// + /// ProjectConfig instance protected abstract ProjectConfig Poll(); + /// + /// Starts datafile scheduler. + /// public void Start() { if (IsStarted) @@ -72,6 +85,9 @@ public void Start() IsStarted = true; } + /// + /// Stops datafile scheduler. + /// public void Stop() { // don't call now and onwards. @@ -81,6 +97,11 @@ public void Stop() Logger.Log(LogLevel.WARN, $"Stopping Config scheduler."); } + /// + /// Retrieve ProjectConfig instance and waits untill the instance + /// gets available or blocking timeout expires. + /// + /// ProjectConfig public ProjectConfig GetConfig() { if (IsStarted) @@ -108,6 +129,11 @@ public ProjectConfig GetConfig() return projectConfig ?? CurrentProjectConfig; } + /// + /// Sets the latest available ProjectConfig valid instance. + /// + /// ProjectConfig + /// true if the ProjectConfig saved successfully, false otherwise public bool SetConfig(ProjectConfig projectConfig) { // trigger now, due because of delayed latency response @@ -134,7 +160,11 @@ public bool SetConfig(ProjectConfig projectConfig) return true; } - + + /// + /// Scheduler method that periodically runs on provided + /// polling interval. + /// public virtual void Run() { if (Monitor.TryEnter(mutex)){ diff --git a/OptimizelySDK/Config/ProjectConfigManager.cs b/OptimizelySDK/Config/ProjectConfigManager.cs index 59fc25a2..c6f94e7b 100644 --- a/OptimizelySDK/Config/ProjectConfigManager.cs +++ b/OptimizelySDK/Config/ProjectConfigManager.cs @@ -16,8 +16,16 @@ namespace OptimizelySDK.Config { + /// + /// Interface for fetching ProjectConfig instance. + /// public interface ProjectConfigManager { + + /// + /// Implementations of this method should block until a datafile is available. + /// + /// ProjectConfig instance ProjectConfig GetConfig(); } } diff --git a/OptimizelySDK/Entity/FeatureVariable.cs b/OptimizelySDK/Entity/FeatureVariable.cs index 0a71e78e..1d2d10fa 100644 --- a/OptimizelySDK/Entity/FeatureVariable.cs +++ b/OptimizelySDK/Entity/FeatureVariable.cs @@ -1,5 +1,5 @@ /* - * Copyright 2017, Optimizely + * Copyright 2017, 2019, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,26 @@ public enum VariableType public string DefaultValue { get; set; } public VariableType Type { get; set; } public VariableStatus Status { get; set; } - + + /// + /// Returns the feature variable api name based on VariableType. + /// + /// The feature variable type name. + /// Variable type. + public static string GetFeatureVariableTypeName(VariableType variableType) + { + switch (variableType) { + case FeatureVariable.VariableType.BOOLEAN: + return "GetFeatureVariableBoolean"; + case FeatureVariable.VariableType.DOUBLE: + return "GetFeatureVariableDouble"; + case FeatureVariable.VariableType.INTEGER: + return "GetFeatureVariableInteger"; + case FeatureVariable.VariableType.STRING: + return "GetFeatureVariableString"; + default: + return null; + } + } } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index 232b5cc8..e1f44c0a 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -36,8 +36,6 @@ public class Optimizely : IOptimizely private EventBuilder EventBuilder; private IEventDispatcher EventDispatcher; - - private ProjectConfig Config; private ILogger Logger; @@ -49,7 +47,17 @@ public class Optimizely : IOptimizely public NotificationCenter NotificationCenter; - public bool IsValid { get; private set; } + public ProjectConfigManager ProjectConfigManager; + + /// + /// It returns true if the ProjectConfig is valid otherwise false. + /// Also, it may block execution if GetConfig() blocks execution to get ProjectConfig. + /// + public bool IsValid { + get { + return ProjectConfigManager?.GetConfig() != null; + } + } public static String SDK_VERSION { get { @@ -94,30 +102,17 @@ public Optimizely(string datafile, UserProfileService userProfileService = null, bool skipJsonValidation = false) { - IsValid = false; // invalid until proven valid - Logger = logger ?? new NoOpLogger(); - EventDispatcher = eventDispatcher ?? new DefaultEventDispatcher(Logger); - ErrorHandler = errorHandler ?? new NoOpErrorHandler(); - Bucketer = new Bucketer(Logger); - EventBuilder = new EventBuilder(Bucketer, Logger); - UserProfileService = userProfileService; - NotificationCenter = new NotificationCenter(Logger); - try { + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService); - if (!ValidateInputs(datafile, skipJsonValidation)) - { + if (ValidateInputs(datafile, skipJsonValidation)) { + var config = DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); + ProjectConfigManager = new FallbackProjectConfigManager(config); + } else { Logger.Log(LogLevel.ERROR, "Provided 'datafile' has invalid schema."); - return; } - Config = DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); - IsValid = true; - - DecisionService = new DecisionService(Bucketer, ErrorHandler, Config, userProfileService, Logger); - } - catch (Exception ex) - { + } catch (Exception ex) { string error = String.Empty; if (ex.GetType() == typeof(ConfigParseException)) error = ex.Message; @@ -129,6 +124,38 @@ public Optimizely(string datafile, } } + /// + /// Initializes a new instance of the class. + /// + /// Config manager. + /// Event dispatcher. + /// Logger. + /// Error handler. + /// User profile service. + public Optimizely(ProjectConfigManager configManager, + IEventDispatcher eventDispatcher = null, + ILogger logger = null, + IErrorHandler errorHandler = null, + UserProfileService userProfileService = null) + { + ProjectConfigManager = configManager; + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService); + } + + private void InitializeComponents(IEventDispatcher eventDispatcher = null, + ILogger logger = null, + IErrorHandler errorHandler = null, + UserProfileService userProfileService = null) + { + Logger = logger ?? new NoOpLogger(); + EventDispatcher = eventDispatcher ?? new DefaultEventDispatcher(Logger); + ErrorHandler = errorHandler ?? new NoOpErrorHandler(); + Bucketer = new Bucketer(Logger); + EventBuilder = new EventBuilder(Bucketer, Logger); + UserProfileService = userProfileService; + NotificationCenter = new NotificationCenter(Logger); + DecisionService = new DecisionService(Bucketer, ErrorHandler, userProfileService, Logger); + } /// /// Helper function to validate all required conditions before performing activate or track. @@ -136,7 +163,7 @@ public Optimizely(string datafile, /// Experiment Object representing experiment /// string ID for user /// associative array of Attributes for the user - private bool ValidatePreconditions(Experiment experiment, string userId, UserAttributes userAttributes = null) + private bool ValidatePreconditions(Experiment experiment, string userId, ProjectConfig config, UserAttributes userAttributes = null) { if (!experiment.IsExperimentRunning) { @@ -149,7 +176,7 @@ private bool ValidatePreconditions(Experiment experiment, string userId, UserAtt return true; } - if (!ExperimentUtils.IsUserInExperiment(Config, experiment, userAttributes, Logger)) + if (!ExperimentUtils.IsUserInExperiment(config, experiment, userAttributes, Logger)) { Logger.Log(LogLevel.INFO, string.Format("User \"{0}\" does not meet conditions to be in experiment \"{1}\".", userId, experiment.Key)); return false; @@ -168,9 +195,10 @@ private bool ValidatePreconditions(Experiment experiment, string userId, UserAtt /// null|Variation Representing variation public Variation Activate(string experimentKey, string userId, UserAttributes userAttributes = null) { - if (!IsValid) + var config = ProjectConfigManager?.GetConfig(); + if (config == null) { - Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'activate'."); + Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Activate'."); return null; } @@ -183,7 +211,7 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us if (!ValidateStringInputs(inputValues)) return null; - var experiment = Config.GetExperimentFromKey(experimentKey); + var experiment = config.GetExperimentFromKey(experimentKey); if (experiment.Key == null) { @@ -191,7 +219,7 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us return null; } - var variation = GetVariation(experimentKey, userId, userAttributes); + var variation = GetVariation(experimentKey, userId, config, userAttributes); if (variation == null || variation.Key == null) { @@ -199,7 +227,7 @@ public Variation Activate(string experimentKey, string userId, UserAttributes us return null; } - SendImpressionEvent(experiment, variation, userId, userAttributes); + SendImpressionEvent(experiment, variation, userId, userAttributes, config); return variation; } @@ -225,9 +253,10 @@ private bool ValidateInputs(string datafile, bool skipJsonValidation) /// eventTags array Hash representing metadata associated with the event. public void Track(string eventKey, string userId, UserAttributes userAttributes = null, EventTags eventTags = null) { - if (!IsValid) + var config = ProjectConfigManager?.GetConfig(); + if (config == null) { - Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'track'."); + Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'Track'."); return; } @@ -240,7 +269,7 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes if (!ValidateStringInputs(inputValues)) return; - var eevent = Config.GetEvent(eventKey); + var eevent = config.GetEvent(eventKey); if (eevent.Key == null) { @@ -254,7 +283,7 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes eventTags = eventTags.FilterNullValues(Logger); } - var conversionEvent = EventBuilder.CreateConversionEvent(Config, eventKey, + var conversionEvent = EventBuilder.CreateConversionEvent(config, eventKey, userId, userAttributes, eventTags); Logger.Log(LogLevel.INFO, string.Format("Tracking event {0} for user {1}.", eventKey, userId)); Logger.Log(LogLevel.DEBUG, string.Format("Dispatching conversion event to URL {0} with params {1}.", @@ -283,8 +312,21 @@ public void Track(string eventKey, string userId, UserAttributes userAttributes /// null|Variation Representing variation public Variation GetVariation(string experimentKey, string userId, UserAttributes userAttributes = null) { - if (!IsValid) - { + var config = ProjectConfigManager?.GetConfig(); + return GetVariation(experimentKey, userId, config, userAttributes); + } + + /// + /// Get variation where user will be bucketed from the given ProjectConfig. + /// + /// experimentKey string Key identifying the experiment + /// ID for the user + /// ProjectConfig to be used for variation + /// Attributes for the users + /// null|Variation Representing variation + private Variation GetVariation(string experimentKey, string userId, ProjectConfig config, UserAttributes userAttributes = null) + { + if (config == null) { Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetVariation'."); return null; } @@ -296,13 +338,13 @@ public Variation GetVariation(string experimentKey, string userId, UserAttribute }; if (!ValidateStringInputs(inputValues)) - return null; + return null; - Experiment experiment = Config.GetExperimentFromKey(experimentKey); + Experiment experiment = config.GetExperimentFromKey(experimentKey); if (experiment.Key == null) return null; - var variation = DecisionService.GetVariation(experiment, userId, userAttributes); + var variation = DecisionService.GetVariation(experiment, userId, config, userAttributes); var decisionInfo = new Dictionary { { "experimentKey", experimentKey }, @@ -310,7 +352,7 @@ public Variation GetVariation(string experimentKey, string userId, UserAttribute }; userAttributes = userAttributes ?? new UserAttributes(); - var decisionNotificationType = Config.IsFeatureExperiment(experiment.Id) ? DecisionNotificationTypes.FEATURE_TEST : DecisionNotificationTypes.AB_TEST; + var decisionNotificationType = config.IsFeatureExperiment(experiment.Id) ? DecisionNotificationTypes.FEATURE_TEST : DecisionNotificationTypes.AB_TEST; NotificationCenter.SendNotifications(NotificationCenter.NotificationType.Decision, decisionNotificationType, userId, userAttributes, decisionInfo); return variation; @@ -326,13 +368,19 @@ public Variation GetVariation(string experimentKey, string userId, UserAttribute /// A boolean value that indicates if the set completed successfully. public bool SetForcedVariation(string experimentKey, string userId, string variationKey) { + var config = ProjectConfigManager?.GetConfig(); + if (config == null) + { + return false; + } + var inputValues = new Dictionary { { USER_ID, userId }, { EXPERIMENT_KEY, experimentKey } }; - return ValidateStringInputs(inputValues) && Config.SetForcedVariation(experimentKey, userId, variationKey); + return ValidateStringInputs(inputValues) && config.SetForcedVariation(experimentKey, userId, variationKey); } /// @@ -343,6 +391,11 @@ public bool SetForcedVariation(string experimentKey, string userId, string varia /// null|string The variation key. public Variation GetForcedVariation(string experimentKey, string userId) { + var config = ProjectConfigManager?.GetConfig(); + if (config == null) { + return null; + } + var inputValues = new Dictionary { { USER_ID, userId }, @@ -352,7 +405,7 @@ public Variation GetForcedVariation(string experimentKey, string userId) if (!ValidateStringInputs(inputValues)) return null; - return Config.GetForcedVariation(experimentKey, userId); + return config.GetForcedVariation(experimentKey, userId); } #region FeatureFlag APIs @@ -367,6 +420,14 @@ public Variation GetForcedVariation(string experimentKey, string userId) /// True if feature is enabled, false or null otherwise public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttributes userAttributes = null) { + var config = ProjectConfigManager?.GetConfig(); + if (config == null) { + + Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'IsFeatureEnabled'."); + + return false; + } + var inputValues = new Dictionary { { USER_ID, userId }, @@ -376,16 +437,16 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri if (!ValidateStringInputs(inputValues)) return false; - var featureFlag = Config.GetFeatureFlagFromKey(featureKey); + var featureFlag = config.GetFeatureFlagFromKey(featureKey); if (string.IsNullOrEmpty(featureFlag.Key)) return false; - if (!Validator.IsFeatureFlagValid(Config, featureFlag)) + if (!Validator.IsFeatureFlagValid(config, featureFlag)) return false; bool featureEnabled = false; var sourceInfo = new Dictionary(); - var decision = DecisionService.GetVariationForFeature(featureFlag, userId, userAttributes); + var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); if (decision.Variation != null) { @@ -396,7 +457,7 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri { sourceInfo["experimentKey"] = decision.Experiment.Key; sourceInfo["variationKey"] = variation.Key; - SendImpressionEvent(decision.Experiment, variation, userId, userAttributes); + SendImpressionEvent(decision.Experiment, variation, userId, userAttributes, config); } else { @@ -432,8 +493,16 @@ public virtual bool IsFeatureEnabled(string featureKey, string userId, UserAttri /// Variable type /// string | null Feature variable value public virtual T GetFeatureVariableValueForType(string featureKey, string variableKey, string userId, - UserAttributes userAttributes, FeatureVariable.VariableType variableType) + UserAttributes userAttributes, FeatureVariable.VariableType variableType) { + + var config = ProjectConfigManager?.GetConfig(); + if (config == null) { + + Logger.Log(LogLevel.ERROR, $@"Datafile has invalid format. Failing '{FeatureVariable.GetFeatureVariableTypeName(variableType)}'."); + return default(T); + } + var inputValues = new Dictionary { { USER_ID, userId }, @@ -444,7 +513,7 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var if (!ValidateStringInputs(inputValues)) return default(T); - var featureFlag = Config.GetFeatureFlagFromKey(featureKey); + var featureFlag = config.GetFeatureFlagFromKey(featureKey); if (string.IsNullOrEmpty(featureFlag.Key)) return default(T); @@ -464,7 +533,7 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var var featureEnabled = false; var variableValue = featureVariable.DefaultValue; - var decision = DecisionService.GetVariationForFeature(featureFlag, userId, userAttributes); + var decision = DecisionService.GetVariationForFeature(featureFlag, userId, config, userAttributes); if (decision.Variation != null) { @@ -529,6 +598,8 @@ public virtual T GetFeatureVariableValueForType(string featureKey, string var /// bool | Feature variable value or null public bool? GetFeatureVariableBoolean(string featureKey, string variableKey, string userId, UserAttributes userAttributes = null) { + var config = ProjectConfigManager?.GetConfig(); + return GetFeatureVariableValueForType(featureKey, variableKey, userId, userAttributes, FeatureVariable.VariableType.BOOLEAN); } @@ -575,15 +646,15 @@ public string GetFeatureVariableString(string featureKey, string variableKey, st /// Sends impression event. /// /// The experiment - /// The variation entity + /// The variation entity /// The user ID /// The user's attributes - private void SendImpressionEvent(Experiment experiment, Variation variation, string userId, - UserAttributes userAttributes) + private void SendImpressionEvent(Experiment experiment, Variation variation, string userId, + UserAttributes userAttributes, ProjectConfig config) { if (experiment.IsExperimentRunning) { - var impressionEvent = EventBuilder.CreateImpressionEvent(Config, experiment, variation.Id, userId, userAttributes); + var impressionEvent = EventBuilder.CreateImpressionEvent(config, experiment, variation.Id, userId, userAttributes); Logger.Log(LogLevel.INFO, string.Format("Activating user {0} in experiment {1}.", userId, experiment.Key)); Logger.Log(LogLevel.DEBUG, string.Format("Dispatching impression event to URL {0} with params {1}.", impressionEvent.Url, impressionEvent.GetParamsAsJson())); @@ -616,7 +687,8 @@ public List GetEnabledFeatures(string userId, UserAttributes userAttribu { List enabledFeaturesList = new List(); - if (!IsValid) + var config = ProjectConfigManager?.GetConfig(); + if (config == null) { Logger.Log(LogLevel.ERROR, "Datafile has invalid format. Failing 'GetEnabledFeatures'."); return enabledFeaturesList; @@ -625,7 +697,7 @@ public List GetEnabledFeatures(string userId, UserAttributes userAttribu if (!ValidateStringInputs(new Dictionary { { USER_ID, userId } })) return enabledFeaturesList; - foreach (var feature in Config.FeatureKeyMap.Values) + foreach (var feature in config.FeatureKeyMap.Values) { var featureKey = feature.Key; if (IsFeatureEnabled(featureKey, userId, userAttributes)) diff --git a/OptimizelySDK/OptimizelyFactory.cs b/OptimizelySDK/OptimizelyFactory.cs new file mode 100644 index 00000000..4671f1df --- /dev/null +++ b/OptimizelySDK/OptimizelyFactory.cs @@ -0,0 +1,46 @@ +using System; +using OptimizelySDK.Bucketing; +using OptimizelySDK.Config; +using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Event.Dispatcher; +using OptimizelySDK.Logger; + +namespace OptimizelySDK +{ + /// after component emitting notification. + /// + /// + /// Optimizely factory. + /// + /// TODO: Add documentation of this class + /// TODO: Add unit test of this class. + public static class OptimizelyFactory + { + public static Optimizely NewDefaultInstance(string sdkKey) + { + return NewDefaultInstance(sdkKey, null); + } + + public static Optimizely NewDefaultInstance(string sdkKey, string fallback) + { + var logger = new DefaultLogger(); + var errorHandler = new DefaultErrorHandler(); + var eventDispatcher = new DefaultEventDispatcher(logger); + var builder = new HttpProjectConfigManager.Builder(); + var configManager = builder + .WithSdkKey(sdkKey) + .WithDatafile(fallback) + .WithLogger(logger) + .WithErrorHandler(errorHandler) + .Build(true); + + return NewDefaultInstance(configManager, eventDispatcher, errorHandler, logger); + } + + public static Optimizely NewDefaultInstance(ProjectConfigManager configManager, IEventDispatcher eventDispatcher = null, + IErrorHandler errorHandler = null, ILogger logger = null, UserProfileService userprofileService = null) + { + return new Optimizely(configManager, eventDispatcher, logger, errorHandler, userprofileService); + } + } +} diff --git a/OptimizelySDK/OptimizelySDK.csproj b/OptimizelySDK/OptimizelySDK.csproj index be6eddbc..0fd4a95a 100644 --- a/OptimizelySDK/OptimizelySDK.csproj +++ b/OptimizelySDK/OptimizelySDK.csproj @@ -126,6 +126,7 @@ + diff --git a/OptimizelySDK/ProjectConfig.cs b/OptimizelySDK/ProjectConfig.cs index e011646d..148a74d7 100644 --- a/OptimizelySDK/ProjectConfig.cs +++ b/OptimizelySDK/ProjectConfig.cs @@ -96,12 +96,12 @@ public interface ProjectConfig /// Dictionary AudienceIdMap { get; } - /// - /// Associative array of user IDs to an associative array - /// of experiments to variations.This contains all the forced variations - /// set by the user by calling setForcedVariation (it is not the same as the - /// whitelisting forcedVariations data structure in the Experiments class). - /// + /// + /// Associative array of user IDs to an associative array + /// of experiments to variations.This contains all the forced variations + /// set by the user by calling setForcedVariation (it is not the same as the + /// whitelisting forcedVariations data structure in the Experiments class). + /// Dictionary> ForcedVariationMap { get; } /// @@ -219,21 +219,21 @@ public interface ProjectConfig /// entity if key or ID is invalid Variation GetVariationFromId(string experimentKey, string variationId); - /// - /// Gets the forced variation for the given user and experiment. - /// - /// The experiment key - /// The user ID - /// Variation entity which the given user and experiment should be forced into. + /// + /// Gets the forced variation for the given user and experiment. + /// + /// The experiment key + /// The user ID + /// Variation entity which the given user and experiment should be forced into. Variation GetForcedVariation(string experimentKey, string userId); - /// - /// Sets an associative array of user IDs to an associative array of experiments to forced variations. - /// - /// The experiment key - /// The user ID - /// The variation key - /// A boolean value that indicates if the set completed successfully. + /// + /// Sets an associative array of user IDs to an associative array of experiments to forced variations. + /// + /// The experiment key + /// The user ID + /// The variation key + /// A boolean value that indicates if the set completed successfully. bool SetForcedVariation(string experimentKey, string userId, string variationKey); ///