diff --git a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs index eea84d6b..c39facb6 100644 --- a/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs +++ b/OptimizelySDK.Tests/EventTests/EventFactoryTest.cs @@ -88,7 +88,7 @@ public void TestCreateImpressionEventNoAttributes() { "rule_key", "test_experiment" }, { "flag_key", "test_experiment" }, { "variation_key", "control" }, - {"enabled", false } + { "enabled", false } } } } } diff --git a/OptimizelySDK.Tests/OptimizelyTest.cs b/OptimizelySDK.Tests/OptimizelyTest.cs index e310571a..b961a560 100644 --- a/OptimizelySDK.Tests/OptimizelyTest.cs +++ b/OptimizelySDK.Tests/OptimizelyTest.cs @@ -35,6 +35,7 @@ using System.Globalization; using System.Threading; using OptimizelySDK.Tests.Utils; +using OptimizelySDK.OptimizelyDecisions; namespace OptimizelySDK.Tests { @@ -98,7 +99,7 @@ public void Initialize() SkipJsonValidation = false, }; - OptimizelyMock = new Mock(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, null, false, null) + OptimizelyMock = new Mock(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, null, false, null, null) { CallBase = true }; @@ -166,6 +167,8 @@ private class OptimizelyHelper public bool SkipJsonValidation { get; set; } public EventProcessor EventProcessor { get; set; } + public OptimizelyDecideOption[] DefaultDecideOptions { get; set; } + public PrivateObject CreatePrivateOptimizely() { return new PrivateObject(typeof(Optimizely), ParameterTypes, @@ -177,7 +180,8 @@ public PrivateObject CreatePrivateOptimizely() ErrorHandler, UserProfileService, SkipJsonValidation, - EventProcessor + EventProcessor, + DefaultDecideOptions }); } } @@ -196,7 +200,7 @@ public void TestCreateUserContext() var optlyUserContext = Optimizely.CreateUserContext(TestUserId, attribute); Assert.AreEqual(TestUserId, optlyUserContext.UserId); Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); - Assert.AreEqual(attribute, optlyUserContext.UserAttributes); + Assert.AreEqual(attribute, optlyUserContext.Attributes); } [Test] @@ -205,7 +209,7 @@ public void TestCreateUserContextWithoutAttributes() var optlyUserContext = Optimizely.CreateUserContext(TestUserId); Assert.AreEqual(TestUserId, optlyUserContext.UserId); Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); - Assert.IsTrue(optlyUserContext.UserAttributes.Count == 0); + Assert.IsTrue(optlyUserContext.Attributes.Count == 0); } [Test] @@ -228,11 +232,11 @@ public void TestCreateUserContextMultipleAttribute() Assert.AreEqual("userId1", optlyUserContext1.UserId); Assert.AreEqual(Optimizely, optlyUserContext1.Optimizely); - Assert.AreEqual(attribute1, optlyUserContext1.UserAttributes); + Assert.AreEqual(attribute1, optlyUserContext1.Attributes); Assert.AreEqual("userId2", optlyUserContext2.UserId); Assert.AreEqual(Optimizely, optlyUserContext2.Optimizely); - Assert.AreEqual(attribute2, optlyUserContext2.UserAttributes); + Assert.AreEqual(attribute2, optlyUserContext2.Attributes); } [Test] @@ -247,7 +251,7 @@ public void TestChangeAttributeDoesNotEffectValues() var optlyUserContext = Optimizely.CreateUserContext(userId, attribute); Assert.AreEqual(TestUserId, optlyUserContext.UserId); Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); - Assert.AreEqual(attribute, optlyUserContext.UserAttributes); + Assert.AreEqual(attribute, optlyUserContext.Attributes); attribute = new UserAttributes { @@ -258,7 +262,7 @@ public void TestChangeAttributeDoesNotEffectValues() userId = "InvalidUser"; Assert.AreEqual("testUserId", optlyUserContext.UserId); Assert.AreEqual(Optimizely, optlyUserContext.Optimizely); - Assert.AreNotEqual(attribute, optlyUserContext.UserAttributes); + Assert.AreNotEqual(attribute, optlyUserContext.Attributes); } #endregion diff --git a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs index c8a436a2..75c37ab8 100644 --- a/OptimizelySDK.Tests/OptimizelyUserContextTest.cs +++ b/OptimizelySDK.Tests/OptimizelyUserContextTest.cs @@ -15,14 +15,22 @@ * limitations under the License. */ +using Castle.Core.Internal; using Moq; using NUnit.Framework; +using OptimizelySDK.Bucketing; +using OptimizelySDK.Config; using OptimizelySDK.Entity; using OptimizelySDK.ErrorHandler; +using OptimizelySDK.Event; using OptimizelySDK.Event.Dispatcher; using OptimizelySDK.Logger; +using OptimizelySDK.Notifications; using OptimizelySDK.OptimizelyDecisions; +using OptimizelySDK.Tests.NotificationTests; +using OptimizelySDK.Utils; using System; +using System.Collections.Generic; namespace OptimizelySDK.Tests { @@ -34,6 +42,7 @@ public class OptimizelyUserContextTest private Mock LoggerMock; private Mock ErrorHandlerMock; private Mock EventDispatcherMock; + private Mock NotificationCallbackMock; [SetUp] public void SetUp() @@ -41,6 +50,8 @@ public void SetUp() LoggerMock = new Mock(); LoggerMock.Setup(i => i.Log(It.IsAny(), It.IsAny())); + NotificationCallbackMock = new Mock(); + ErrorHandlerMock = new Mock(); ErrorHandlerMock.Setup(e => e.HandleError(It.IsAny())); EventDispatcherMock = new Mock(); @@ -56,7 +67,7 @@ public void OptimizelyUserContextWithAttributes() Assert.AreEqual(user.Optimizely, Optimizely); Assert.AreEqual(user.UserId, UserID); - Assert.AreEqual(user.UserAttributes, attributes); + Assert.AreEqual(user.Attributes, attributes); } [Test] @@ -66,7 +77,7 @@ public void OptimizelyUserContextNoAttributes() Assert.AreEqual(user.Optimizely, Optimizely); Assert.AreEqual(user.UserId, UserID); - Assert.True(user.UserAttributes.Count == 0); + Assert.True(user.Attributes.Count == 0); } [Test] @@ -82,7 +93,7 @@ public void SetAttribute() Assert.AreEqual(user.Optimizely, Optimizely); Assert.AreEqual(user.UserId, UserID); - var newAttributes = user.UserAttributes; + var newAttributes = user.Attributes; Assert.AreEqual(newAttributes["house"], "GRYFFINDOR"); Assert.AreEqual(newAttributes["k1"], "v1"); Assert.AreEqual(newAttributes["k2"], true); @@ -100,7 +111,7 @@ public void SetAttributeNoAttribute() Assert.AreEqual(user.Optimizely, Optimizely); Assert.AreEqual(user.UserId, UserID); - var newAttributes = user.UserAttributes; + var newAttributes = user.Attributes; Assert.AreEqual(newAttributes["k1"], "v1"); Assert.AreEqual(newAttributes["k2"], true); } @@ -114,7 +125,7 @@ public void SetAttributeOverride() user.SetAttribute("k1", "v1"); user.SetAttribute("house", "v2"); - var newAttributes = user.UserAttributes; + var newAttributes = user.Attributes; Assert.AreEqual(newAttributes["k1"], "v1"); Assert.AreEqual(newAttributes["house"], "v2"); } @@ -125,15 +136,15 @@ public void SetAttributeNullValue() var attributes = new UserAttributes() { { "k1", null } }; OptimizelyUserContext user = new OptimizelyUserContext(Optimizely, UserID, attributes, ErrorHandlerMock.Object, LoggerMock.Object); - var newAttributes = user.UserAttributes; + var newAttributes = user.Attributes; Assert.AreEqual(newAttributes["k1"], null); user.SetAttribute("k1", true); - newAttributes = user.UserAttributes; + newAttributes = user.Attributes; Assert.AreEqual(newAttributes["k1"], true); user.SetAttribute("k1", null); - newAttributes = user.UserAttributes; + newAttributes = user.Attributes; Assert.AreEqual(newAttributes["k1"], null); } @@ -147,10 +158,10 @@ public void SetAttributeToOverrideAttribute() Assert.AreEqual(user.UserId, UserID); user.SetAttribute("k1", "v1"); - Assert.AreEqual(user.UserAttributes["k1"], "v1"); + Assert.AreEqual(user.Attributes["k1"], "v1"); user.SetAttribute("k1", true); - Assert.AreEqual(user.UserAttributes["k1"], true); + Assert.AreEqual(user.Attributes["k1"], true); } #region decide @@ -211,5 +222,554 @@ public void DecideWhenConfigIsNull() Assert.IsTrue(TestData.CompareObjects(decision, decisionExpected)); } #endregion + + #region decideAll + + [Test] + public void DecideForKeysWithOneFlag() + { + var flagKey = "multi_variate_feature"; + var flagKeys = new string[] { flagKey }; + + var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisions = user.DecideForKeys(flagKeys); + + Assert.True(decisions.Count == 1); + var decision = decisions[flagKey]; + + OptimizelyDecision expDecision = new OptimizelyDecision( + "Gred", + false, + variablesExpected, + "test_experiment_multivariate", + flagKey, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decision, expDecision)); + } + + [Test] + public void DecideAllTwoFlag() + { + var flagKey1 = "multi_variate_feature"; + var flagKey2 = "string_single_variable_feature"; + var flagKeys = new string[] { flagKey1, flagKey2 }; + + var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); + var variablesExpected2 = Optimizely.GetAllFeatureVariables(flagKey2, UserID); + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + // Mocking objects. + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>())); + + Optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + + var decisions = user.DecideForKeys(flagKeys); + + var userAttributes = new UserAttributes + { + { "browser_type", "chrome" } + }; + + Assert.True(decisions.Count == 2); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, UserID, userAttributes, It.IsAny>()), + Times.Exactly(2)); + OptimizelyDecision expDecision1 = new OptimizelyDecision( + "Gred", + false, + variablesExpected1, + "test_experiment_multivariate", + flagKey1, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); + + OptimizelyDecision expDecision2 = new OptimizelyDecision( + "control", + true, + variablesExpected2, + "test_experiment_with_feature_rollout", + flagKey2, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey2], expDecision2)); + } + + [Test] + public void DecideAllAllFlags() + { + var flagKey1 = "boolean_feature"; + var flagKey2 = "double_single_variable_feature"; + var flagKey3 = "integer_single_variable_feature"; + var flagKey4 = "boolean_single_variable_feature"; + var flagKey5 = "string_single_variable_feature"; + var flagKey6 = "multi_variate_feature"; + var flagKey7 = "mutex_group_feature"; + var flagKey8 = "empty_feature"; + var flagKey9 = "no_rollout_experiment_feature"; + var flagKey10 = "unsupported_variabletype"; + + var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); + var variablesExpected2 = Optimizely.GetAllFeatureVariables(flagKey2, UserID); + var variablesExpected3 = Optimizely.GetAllFeatureVariables(flagKey3, UserID); + var variablesExpected4 = Optimizely.GetAllFeatureVariables(flagKey4, UserID); + var variablesExpected5 = Optimizely.GetAllFeatureVariables(flagKey5, UserID); + var variablesExpected6 = Optimizely.GetAllFeatureVariables(flagKey6, UserID); + var variablesExpected7 = Optimizely.GetAllFeatureVariables(flagKey7, UserID); + var variablesExpected8 = Optimizely.GetAllFeatureVariables(flagKey8, UserID); + var variablesExpected9 = Optimizely.GetAllFeatureVariables(flagKey9, UserID); + var variablesExpected10 = Optimizely.GetAllFeatureVariables(flagKey10, UserID); + + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + // Mocking objects. + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>())); + + Optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + + var decisions = user.DecideAll(); + + var userAttributes = new UserAttributes + { + { "browser_type", "chrome" } + }; + + Assert.True(decisions.Count == 10); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, UserID, userAttributes, It.IsAny>()), + Times.Exactly(10)); + + OptimizelyDecision expDecision1 = new OptimizelyDecision( + null, + false, + variablesExpected1, + null, + flagKey1, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); + + OptimizelyDecision expDecision2 = new OptimizelyDecision( + "variation", + false, + variablesExpected2, + "test_experiment_double_feature", + flagKey2, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey2], expDecision2)); + + OptimizelyDecision expDecision3 = new OptimizelyDecision( + "control", + false, + variablesExpected3, + "test_experiment_integer_feature", + flagKey3, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey3], expDecision3)); + + OptimizelyDecision expDecision4 = new OptimizelyDecision( + "188881", + false, + variablesExpected4, + "188880", + flagKey4, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey4], expDecision4)); + + OptimizelyDecision expDecision5 = new OptimizelyDecision( + "control", + true, + variablesExpected5, + "test_experiment_with_feature_rollout", + flagKey5, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey5], expDecision5)); + + OptimizelyDecision expDecision6 = new OptimizelyDecision( + "Gred", + false, + variablesExpected6, + "test_experiment_multivariate", + flagKey6, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey6], expDecision6)); + + OptimizelyDecision expDecision7 = new OptimizelyDecision( + null, + false, + variablesExpected7, + null, + flagKey7, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey7], expDecision7)); + + OptimizelyDecision expDecision8 = new OptimizelyDecision( + null, + false, + variablesExpected8, + null, + flagKey8, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey8], expDecision8)); + + OptimizelyDecision expDecision9 = new OptimizelyDecision( + null, + false, + variablesExpected9, + null, + flagKey9, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey9], expDecision9)); + + OptimizelyDecision expDecision10 = new OptimizelyDecision( + null, + false, + variablesExpected10, + null, + flagKey10, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey10], expDecision10)); + } + + [Test] + public void DecideAllEnabledFlagsOnlyDecideOptions() + { + var flagKey1 = "string_single_variable_feature"; + + var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); + + var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.ENABLED_FLAGS_ONLY }; + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisions = user.DecideAll(decideOptions); + + Assert.True(decisions.Count == 1); + + OptimizelyDecision expDecision1 = new OptimizelyDecision( + "control", + true, + variablesExpected1, + "test_experiment_with_feature_rollout", + flagKey1, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); + } + + [Test] + public void DecideAllEnabledFlagsDefaultDecideOptions() + { + var flagKey1 = "string_single_variable_feature"; + var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.ENABLED_FLAGS_ONLY }; + + var optimizely = new Optimizely(TestData.Datafile, + EventDispatcherMock.Object, + LoggerMock.Object, + ErrorHandlerMock.Object, + defaultDecideOptions: decideOptions); + + var variablesExpected1 = Optimizely.GetAllFeatureVariables(flagKey1, UserID); + + var user = optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisions = user.DecideAll(); + + Assert.True(decisions.Count == 1); + + OptimizelyDecision expDecision1 = new OptimizelyDecision( + "control", + true, + variablesExpected1, + "test_experiment_with_feature_rollout", + flagKey1, + user, + new string[0]); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); + } + + [Test] + public void DecideAllEnabledFlagsDefaultDecideOptionsPlusApiOptions() + { + var flagKey1 = "string_single_variable_feature"; + var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.ENABLED_FLAGS_ONLY }; + + var optimizely = new Optimizely(TestData.Datafile, + EventDispatcherMock.Object, + LoggerMock.Object, + ErrorHandlerMock.Object, + defaultDecideOptions: decideOptions); + + var user = optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.EXCLUDE_VARIABLES }; + + var decisions = user.DecideAll(decideOptions); + + Assert.True(decisions.Count == 1); + var expectedOptlyJson = new Dictionary(); + OptimizelyDecision expDecision1 = new OptimizelyDecision( + "control", + true, + new OptimizelyJSON(expectedOptlyJson, ErrorHandlerMock.Object, LoggerMock.Object), + "test_experiment_with_feature_rollout", + flagKey1, + user, + new string[] { }); + Assert.IsTrue(TestData.CompareObjects(decisions[flagKey1], expDecision1)); + } + + [Test] + public void DecideExcludeVariablesDecideOptions() + { + var flagKey = "multi_variate_feature"; + var variablesExpected = new Dictionary(); + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.EXCLUDE_VARIABLES }; + + var decision = user.Decide(flagKey, decideOptions); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + Assert.True(decision.Reasons.IsNullOrEmpty()); + } + + [Test] + public void DecideIncludeReasonsDecideOptions() + { + var flagKey = "invalid_key"; + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decision = user.Decide(flagKey); + Assert.True(decision.Reasons.Length == 1); + Assert.AreEqual(decision.Reasons[0], DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey)); + + var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.INCLUDE_REASONS }; + + decision = user.Decide(flagKey, decideOptions); + Assert.True(decision.Reasons.Length == 1); + Assert.AreEqual(decision.Reasons[0], DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, flagKey)); + + flagKey = "multi_variate_feature"; + decision = user.Decide(flagKey); + Assert.True(decision.Reasons.Length == 0); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + Assert.True(decision.Reasons.IsNullOrEmpty()); + + decision = user.Decide(flagKey, decideOptions); + Assert.True(decision.Reasons.Length > 0); + Assert.AreEqual("User [testUserID] is in variation [Gred] of experiment [test_experiment_multivariate].", decision.Reasons[0]); + Assert.AreEqual("The user \"testUserID\" is bucketed into experiment \"test_experiment_multivariate\" of feature \"multi_variate_feature\".", decision.Reasons[1]); + } + + [Test] + public void TestDoNotSendEventDecide() + { + var flagKey = "multi_variate_feature"; + var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); + + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + var user = optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.DISABLE_DECISION_EVENT }; + var decision = user.Decide(flagKey, decideOptions); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Never); + + decision = user.Decide(flagKey); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary()); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + } + + [Test] + public void TestDefaultDecideOptions() + { + var flagKey = "multi_variate_feature"; + var variablesExpected = Optimizely.GetAllFeatureVariables(flagKey, UserID); + var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.DISABLE_DECISION_EVENT }; + + var optimizely = new Optimizely(TestData.Datafile, + EventDispatcherMock.Object, + LoggerMock.Object, + ErrorHandlerMock.Object, + defaultDecideOptions: decideOptions); + + var user = optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decision = user.Decide(flagKey); + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Never); + + Assert.AreEqual(decision.VariationKey, "Gred"); + Assert.False(decision.Enabled); + Assert.AreEqual(decision.Variables.ToDictionary(), variablesExpected.ToDictionary()); + Assert.AreEqual(decision.RuleKey, "test_experiment_multivariate"); + Assert.AreEqual(decision.FlagKey, flagKey); + Assert.AreEqual(decision.UserContext, user); + } + + [Test] + public void TestDecisionNotification() + { + var flagKey = "string_single_variable_feature"; + var variationKey = "control"; + var enabled = true; + var variables = Optimizely.GetAllFeatureVariables(flagKey, UserID); + var ruleKey = "test_experiment_with_feature_rollout"; + var reasons = new Dictionary(); + var user = Optimizely.CreateUserContext(UserID); + user.SetAttribute("browser_type", "chrome"); + + var decisionInfo = new Dictionary + { + { "flagKey", flagKey }, + { "enabled", enabled }, + { "variables", variables.ToDictionary() }, + { "variationKey", variationKey }, + { "ruleKey", ruleKey }, + { "reasons", reasons }, + { "decisionEventDispatched", true }, + }; + + var userAttributes = new UserAttributes + { + { "browser_type", "chrome" } + }; + + // Mocking objects. + NotificationCallbackMock.Setup(nc => nc.TestDecisionCallback(It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny>())); + + Optimizely.NotificationCenter.AddNotification(NotificationCenter.NotificationType.Decision, NotificationCallbackMock.Object.TestDecisionCallback); + + user.Decide(flagKey); + NotificationCallbackMock.Verify(nc => nc.TestDecisionCallback(DecisionNotificationTypes.FLAG, UserID, userAttributes, It.Is>(info => + TestData.CompareObjects(info, decisionInfo))), + Times.Once); + } + + [Test] + public void TestDecideOptionsByPassUPS() + { + var userProfileServiceMock = new Mock(); + var flagKey = "string_single_variable_feature"; + + var experimentId = "122235"; + var userId = "testUser3"; + var variationKey = "control"; + var fbVariationId = "122237"; + var fbVariationKey = "variation"; + + + var userProfile = new UserProfile(userId, new Dictionary + { + { experimentId, new Decision(fbVariationId)} + }); + + userProfileServiceMock.Setup(_ => _.Lookup(userId)).Returns(userProfile.ToMap()); + + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object); + + var user = optimizely.CreateUserContext(userId); + + var projectConfig = DatafileProjectConfig.Create(TestData.Datafile, LoggerMock.Object, ErrorHandlerMock.Object); + + var variationUserProfile = user.Decide(flagKey); + Assert.AreEqual(fbVariationKey, variationUserProfile.VariationKey); + + var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE }; + variationUserProfile = user.Decide(flagKey, decideOptions); + Assert.AreEqual(variationKey, variationUserProfile.VariationKey); + } + + [Test] + public void TestDecideOptionsByPassUPSNeverCallsSaveVariation() + { + var userProfileServiceMock = new Mock(); + var flagKey = "string_single_variable_feature"; + + var userId = "testUser3"; + var variationKey = "control"; + + var optimizely = new Optimizely(TestData.Datafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object, userProfileServiceMock.Object); + var user = optimizely.CreateUserContext(userId); + + var decideOptions = new OptimizelyDecideOption[] { OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE }; + var variationUserProfile = user.Decide(flagKey, decideOptions); + userProfileServiceMock.Verify(l => l.Save(It.IsAny>()), Times.Never); + + Assert.AreEqual(variationKey, variationUserProfile.VariationKey); + } + #endregion + + #region TrackEvent + [Test] + public void TestTrackEventWithAudienceConditions() + { + var OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + var userAttributes = new UserAttributes + { + { "house", "Gryffindor" }, + { "should_do_it", false } + }; + + var user = OptimizelyWithTypedAudiences.CreateUserContext(UserID, userAttributes); + + // Should be excluded as exact match boolean audience with id '3468206643' does not match so the overall conditions fail. + user.TrackEvent("user_signed_up"); + + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + } + + [Test] + public void TrackEventEmptyAttributesWithEventTags() + { + var OptimizelyWithTypedAudiences = new Optimizely(TestData.TypedAudienceDatafile, EventDispatcherMock.Object, LoggerMock.Object, ErrorHandlerMock.Object); + + var user = OptimizelyWithTypedAudiences.CreateUserContext(UserID); + + // Should be excluded as exact match boolean audience with id '3468206643' does not match so the overall conditions fail. + user.TrackEvent("user_signed_up", new EventTags + { + { "revenue", 42 }, + { "wont_send_null", null} + }); + + EventDispatcherMock.Verify(dispatcher => dispatcher.DispatchEvent(It.IsAny()), Times.Once); + } + #endregion } } diff --git a/OptimizelySDK/Optimizely.cs b/OptimizelySDK/Optimizely.cs index e36a6ec4..b78f90f3 100644 --- a/OptimizelySDK/Optimizely.cs +++ b/OptimizelySDK/Optimizely.cs @@ -123,10 +123,11 @@ public Optimizely(string datafile, IErrorHandler errorHandler = null, UserProfileService userProfileService = null, bool skipJsonValidation = false, - EventProcessor eventProcessor = null) + EventProcessor eventProcessor = null, + OptimizelyDecideOption[] defaultDecideOptions = null) { try { - InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, null, eventProcessor); + InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService, null, eventProcessor, defaultDecideOptions); if (ValidateInputs(datafile, skipJsonValidation)) { var config = DatafileProjectConfig.Create(datafile, Logger, ErrorHandler); @@ -187,7 +188,7 @@ private void InitializeComponents(IEventDispatcher eventDispatcher = null, NotificationCenter = notificationCenter ?? new NotificationCenter(Logger); DecisionService = new DecisionService(Bucketer, ErrorHandler, userProfileService, Logger); EventProcessor = eventProcessor ?? new ForwardingEventProcessor(EventDispatcher, NotificationCenter, Logger); - DefaultDecideOptions = defaultDecideOptions ?? new OptimizelyDecideOption[0]; + DefaultDecideOptions = defaultDecideOptions ?? new OptimizelyDecideOption[] { }; } /// @@ -732,10 +733,18 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, { return OptimizelyDecision.NewErrorDecision(key, user, DecisionMessage.SDK_NOT_READY, ErrorHandler, Logger); } - var userId = user.UserId; + if (key == null) + { + return OptimizelyDecision.NewErrorDecision(key, + user, + DecisionMessage.Reason(DecisionMessage.FLAG_KEY_INVALID, key), + ErrorHandler, Logger); + } + var userId = user?.UserId; + var flag = config.GetFeatureFlagFromKey(key); - if (flag.Key == null) + if (string.IsNullOrEmpty(flag.Key)) { return OptimizelyDecision.NewErrorDecision(key, user, @@ -743,7 +752,8 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, ErrorHandler, Logger); } - var userAttributes = user.UserAttributes; + var userAttributes = user.Attributes; + var decisionEventDispatched = false; var allOptions = GetAllOptions(options); var decisionReasons = DefaultDecisionReasons.NewInstance(allOptions); @@ -836,13 +846,63 @@ internal OptimizelyDecision Decide(OptimizelyUserContext user, reasonsToReport.ToArray()); } + internal Dictionary DecideAll(OptimizelyUserContext user, + OptimizelyDecideOption[] options) + { + var decisionMap = new Dictionary(); + + var projectConfig = ProjectConfigManager?.GetConfig(); + if (projectConfig == null) + { + Logger.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing isFeatureEnabled call."); + return decisionMap; + } + + var allFlags = projectConfig.FeatureFlags; + var allFlagKeys = allFlags.Select(v => v.Key).ToArray(); + + return DecideForKeys(user, allFlagKeys, options); + } + + + internal Dictionary DecideForKeys(OptimizelyUserContext user, + string[] keys, + OptimizelyDecideOption[] options) + { + var decisionDictionary = new Dictionary(); + + var projectConfig = ProjectConfigManager?.GetConfig(); + if (projectConfig == null) + { + Logger.Log(LogLevel.ERROR, "Optimizely instance is not valid, failing isFeatureEnabled call."); + return decisionDictionary; + } + + if (keys.Length == 0) + { + return decisionDictionary; + } + + var allOptions = GetAllOptions(options); + + foreach (string key in keys) + { + var decision = Decide(user, key, options); + if (!allOptions.Contains(OptimizelyDecideOption.ENABLED_FLAGS_ONLY) || decision.Enabled) + { + decisionDictionary.Add(key, decision); + } + } + + return decisionDictionary; + } + private OptimizelyDecideOption[] GetAllOptions(OptimizelyDecideOption[] options) { - OptimizelyDecideOption[] copiedOptions = new OptimizelyDecideOption[DefaultDecideOptions.Length]; - Array.Copy(DefaultDecideOptions, copiedOptions, DefaultDecideOptions.Length); + OptimizelyDecideOption[] copiedOptions = DefaultDecideOptions; if (options != null) { - copiedOptions.Concat(options); + copiedOptions = options.Union(DefaultDecideOptions).ToArray(); } return copiedOptions; } diff --git a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs index a4a934d3..dfc571ac 100644 --- a/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs +++ b/OptimizelySDK/OptimizelyDecisions/OptimizelyDecision.cs @@ -25,19 +25,39 @@ namespace OptimizelySDK.OptimizelyDecisions /// public class OptimizelyDecision { - // variation key for optimizely decision. + /// + /// variation key for optimizely decision. + /// public string VariationKey { get; private set; } - // boolean value indicating if the flag is enabled or not. + + /// + /// boolean value indicating if the flag is enabled or not. + /// public bool Enabled { get; private set; } - // collection of variables associated with the decision. + + /// + /// collection of variables associated with the decision. + /// public OptimizelyJSON Variables { get; private set; } - // rule key of the decision. + + /// + /// rule key of the decision. + /// public string RuleKey { get; private set; } - // flag key for which the decision was made. + + /// + /// flag key for which the decision was made. + /// public string FlagKey { get; private set; } - // user context for which the decision was made. + + /// + /// user context for which the decision was made. + /// public OptimizelyUserContext UserContext { get; private set; } - // an array of error/info/debug messages describing why the decision has been made. + + /// + /// an array of error/info/debug messages describing why the decision has been made. + /// public string[] Reasons { get; private set; } public OptimizelyDecision(string variationKey, diff --git a/OptimizelySDK/OptimizelyUserContext.cs b/OptimizelySDK/OptimizelyUserContext.cs index 88d9c5ee..7aca6751 100644 --- a/OptimizelySDK/OptimizelyUserContext.cs +++ b/OptimizelySDK/OptimizelyUserContext.cs @@ -19,7 +19,6 @@ using OptimizelySDK.ErrorHandler; using OptimizelySDK.Entity; using OptimizelySDK.OptimizelyDecisions; -using System; namespace OptimizelySDK { @@ -34,7 +33,7 @@ public class OptimizelyUserContext // userID for Optimizely user context public string UserId { get; } // user attributes for Optimizely user context. - public UserAttributes UserAttributes { get; } + public UserAttributes Attributes { get; } // Optimizely object to be used. public Optimizely Optimizely { get; } @@ -43,7 +42,7 @@ public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttribute ErrorHandler = errorHandler; Logger = logger; Optimizely = optimizely; - UserAttributes = userAttributes ?? new UserAttributes(); + Attributes = userAttributes ?? new UserAttributes(); UserId = userId; } @@ -60,9 +59,9 @@ public void SetAttribute(string key, object value) } else { - lock(mutex) - { - UserAttributes[key] = value; + lock (mutex) + { + Attributes[key] = value; } } } @@ -94,15 +93,25 @@ public OptimizelyDecision Decide(string key, { return Optimizely.Decide(this, key, options); } - + + /// + /// Returns a key-map of decision results for multiple flag keys and a user context. + /// + /// list of flag keys for which a decision will be made. + /// A dictionary of all decision results, mapped by flag keys. + public Dictionary DecideForKeys(string[] keys, OptimizelyDecideOption[] options) + { + return Optimizely.DecideForKeys(this, keys, options); + } + /// /// Returns a key-map of decision results for multiple flag keys and a user context. /// /// list of flag keys for which a decision will be made. /// A dictionary of all decision results, mapped by flag keys. - public Dictionary DecideForKeys(List keys) + public Dictionary DecideForKeys(string[] keys) { - throw new NotImplementedException(); + return DecideForKeys(keys, new OptimizelyDecideOption[] { }); } /// @@ -114,7 +123,6 @@ public Dictionary DecideAll() return DecideAll(new OptimizelyDecideOption[] { }); } - /// /// Returns a key-map of decision results ({@link OptimizelyDecision}) for all active flag keys. /// @@ -122,7 +130,7 @@ public Dictionary DecideAll() /// All decision results mapped by flag keys. public Dictionary DecideAll(OptimizelyDecideOption[] options) { - throw new NotImplementedException(); + return Optimizely.DecideAll(this, options); } /// @@ -142,7 +150,7 @@ public void TrackEvent(string eventName) public void TrackEvent(string eventName, EventTags eventTags) { - Optimizely.Track(eventName, UserId, UserAttributes, eventTags); + Optimizely.Track(eventName, UserId, Attributes, eventTags); } } } diff --git a/OptimizelySDK/Utils/DecisionInfoTypes.cs b/OptimizelySDK/Utils/DecisionInfoTypes.cs index 10cac1f7..715f72c3 100644 --- a/OptimizelySDK/Utils/DecisionInfoTypes.cs +++ b/OptimizelySDK/Utils/DecisionInfoTypes.cs @@ -1,5 +1,5 @@ /* - * Copyright 2019, Optimizely + * Copyright 2019-2020, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.