Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions OptimizelySDK.Tests/DecisionServiceHoldoutTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
using OptimizelySDK.Config;
using OptimizelySDK.Entity;
using OptimizelySDK.ErrorHandler;
using OptimizelySDK.Event;
using OptimizelySDK.Event.Entity;
using OptimizelySDK.Logger;
using OptimizelySDK.OptimizelyDecisions;

Expand All @@ -34,6 +36,7 @@ namespace OptimizelySDK.Tests
public class DecisionServiceHoldoutTest
{
private Mock<ILogger> LoggerMock;
private Mock<EventProcessor> EventProcessorMock;
private DecisionService DecisionService;
private DatafileProjectConfig Config;
private JObject TestData;
Expand All @@ -46,6 +49,7 @@ public class DecisionServiceHoldoutTest
public void Initialize()
{
LoggerMock = new Mock<ILogger>();
EventProcessorMock = new Mock<EventProcessor>();

// Load test data
var testDataPath = Path.Combine(TestContext.CurrentContext.TestDirectory,
Expand Down Expand Up @@ -242,5 +246,90 @@ public void TestGetVariationsForFeatureList_Holdout_DecisionReasons()
Assert.IsTrue(decisionWithReasons.DecisionReasons.ToReport().Count > 0, "Should have decision reasons");
}
}

[Test]
public void TestImpressionEventForHoldout()
{
var featureFlag = Config.FeatureKeyMap["test_flag_1"];
var userAttributes = new UserAttributes();

var eventDispatcher = new Event.Dispatcher.DefaultEventDispatcher(LoggerMock.Object);
var optimizelyWithMockedEvents = new Optimizely(
TestData["datafileWithHoldouts"].ToString(),
eventDispatcher,
LoggerMock.Object,
new ErrorHandler.NoOpErrorHandler(),
null, // userProfileService
false, // skipJsonValidation
EventProcessorMock.Object
);

EventProcessorMock.Setup(ep => ep.Process(It.IsAny<ImpressionEvent>()));

var userContext = optimizelyWithMockedEvents.CreateUserContext(TestUserId, userAttributes);
var decision = userContext.Decide(featureFlag.Key);

Assert.IsNotNull(decision, "Decision should not be null");
Assert.IsNotNull(decision.RuleKey, "RuleKey should not be null");

var actualHoldout = Config.Holdouts?.FirstOrDefault(h => h.Key == decision.RuleKey);

Assert.IsNotNull(actualHoldout,
$"RuleKey '{decision.RuleKey}' should correspond to a holdout experiment");
Assert.AreEqual(featureFlag.Key, decision.FlagKey, "Flag key should match");

var holdoutVariation = actualHoldout.Variations.FirstOrDefault(v => v.Key == decision.VariationKey);

Assert.IsNotNull(holdoutVariation,
$"Variation '{decision.VariationKey}' should be from the chosen holdout '{actualHoldout.Key}'");

Assert.AreEqual(holdoutVariation.FeatureEnabled, decision.Enabled,
"Enabled flag should match holdout variation's featureEnabled value");

EventProcessorMock.Verify(ep => ep.Process(It.IsAny<ImpressionEvent>()), Times.Once,
"Impression event should be processed exactly once for holdout decision");

EventProcessorMock.Verify(ep => ep.Process(It.Is<ImpressionEvent>(ie =>
ie.Experiment.Key == actualHoldout.Key &&
ie.Experiment.Id == actualHoldout.Id &&
ie.Timestamp > 0 &&
ie.UserId == TestUserId
)), Times.Once, "Impression event should contain correct holdout experiment details");
}

[Test]
public void TestImpressionEventForHoldout_DisableDecisionEvent()
{
var featureFlag = Config.FeatureKeyMap["test_flag_1"];
var userAttributes = new UserAttributes();

var eventDispatcher = new Event.Dispatcher.DefaultEventDispatcher(LoggerMock.Object);
var optimizelyWithMockedEvents = new Optimizely(
TestData["datafileWithHoldouts"].ToString(),
eventDispatcher,
LoggerMock.Object,
new ErrorHandler.NoOpErrorHandler(),
null, // userProfileService
false, // skipJsonValidation
EventProcessorMock.Object
);

EventProcessorMock.Setup(ep => ep.Process(It.IsAny<ImpressionEvent>()));

var userContext = optimizelyWithMockedEvents.CreateUserContext(TestUserId, userAttributes);
var decision = userContext.Decide(featureFlag.Key, new[] { OptimizelyDecideOption.DISABLE_DECISION_EVENT });

Assert.IsNotNull(decision, "Decision should not be null");
Assert.IsNotNull(decision.RuleKey, "User should be bucketed into a holdout");

var chosenHoldout = Config.Holdouts?.FirstOrDefault(h => h.Key == decision.RuleKey);

Assert.IsNotNull(chosenHoldout, $"Holdout '{decision.RuleKey}' should exist in config");

Assert.AreEqual(featureFlag.Key, decision.FlagKey, "Flag key should match");

EventProcessorMock.Verify(ep => ep.Process(It.IsAny<ImpressionEvent>()), Times.Never,
"No impression event should be processed when DISABLE_DECISION_EVENT option is used");
}
}
}
88 changes: 88 additions & 0 deletions OptimizelySDK.Tests/OptimizelyUserContextHoldoutTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
using OptimizelySDK.Event;
using OptimizelySDK.Event.Dispatcher;
using OptimizelySDK.Logger;
using OptimizelySDK.Notifications;
using OptimizelySDK.OptimizelyDecisions;
using OptimizelySDK.Utils;

namespace OptimizelySDK.Tests
{
Expand Down Expand Up @@ -572,6 +574,92 @@ public void TestDecideReasons_HoldoutDecisionContainsRelevantReasons()
"Should contain holdout-related decision reasoning");
}


#endregion

#region Notification test

[Test]
public void TestDecide_HoldoutNotificationContent()
{
var capturedNotifications = new List<Dictionary<string, object>>();

NotificationCenter.DecisionCallback notificationCallback =
(decisionType, userId, userAttributes, decisionInfo) =>
{
capturedNotifications.Add(new Dictionary<string, object>(decisionInfo));
};

OptimizelyInstance.NotificationCenter.AddNotification(
NotificationCenter.NotificationType.Decision,
notificationCallback);

var userContext = OptimizelyInstance.CreateUserContext(TestUserId,
new UserAttributes { { "country", "us" } });
var decision = userContext.Decide("test_flag_1");

Assert.AreEqual(1, capturedNotifications.Count,
"Should have captured exactly one decision notification");

var notification = capturedNotifications.First();

Assert.IsTrue(notification.ContainsKey("ruleKey"),
"Notification should contain ruleKey");

var ruleKey = notification["ruleKey"]?.ToString();

Assert.IsNotNull(ruleKey, "RuleKey should not be null");

var holdoutExperiment = Config.Holdouts?.FirstOrDefault(h => h.Key == ruleKey);

Assert.IsNotNull(holdoutExperiment,
$"RuleKey '{ruleKey}' should correspond to a holdout experiment");
Assert.IsTrue(notification.ContainsKey("flagKey"),
"Holdout notification should contain flagKey");
Assert.IsTrue(notification.ContainsKey("enabled"),
"Holdout notification should contain enabled flag");
Assert.IsTrue(notification.ContainsKey("variationKey"),
"Holdout notification should contain variationKey");
Assert.IsTrue(notification.ContainsKey("experimentId"),
"Holdout notification should contain experimentId");
Assert.IsTrue(notification.ContainsKey("variationId"),
"Holdout notification should contain variationId");

var flagKey = notification["flagKey"]?.ToString();

Assert.AreEqual("test_flag_1", flagKey, "FlagKey should match the requested flag");

var experimentId = notification["experimentId"]?.ToString();
Assert.AreEqual(holdoutExperiment.Id, experimentId,
"ExperimentId in notification should match holdout experiment ID");

var variationId = notification["variationId"]?.ToString();
var holdoutVariation = holdoutExperiment.Variations?.FirstOrDefault(v => v.Id == variationId);

Assert.IsNotNull(holdoutVariation,
$"VariationId '{variationId}' should correspond to a holdout variation");

var variationKey = notification["variationKey"]?.ToString();

Assert.AreEqual(holdoutVariation.Key, variationKey,
"VariationKey in notification should match holdout variation key");

var enabled = notification["enabled"];

Assert.IsNotNull(enabled, "Enabled flag should be present in notification");
Assert.AreEqual(holdoutVariation.FeatureEnabled, (bool)enabled,
"Enabled flag should match holdout variation's featureEnabled value");

Assert.IsTrue(Config.FeatureKeyMap.ContainsKey(flagKey),
$"FlagKey '{flagKey}' should exist in config");

Assert.IsTrue(notification.ContainsKey("variables"),
"Notification should contain variables");
Assert.IsTrue(notification.ContainsKey("reasons"),
"Notification should contain reasons");
Assert.IsTrue(notification.ContainsKey("decisionEventDispatched"),
"Notification should contain decisionEventDispatched");
}
#endregion
}
}
3 changes: 2 additions & 1 deletion OptimizelySDK/Optimizely.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
try
{
#if USE_ODP
InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService,

Check warning on line 144 in OptimizelySDK/Optimizely.cs

View workflow job for this annotation

GitHub Actions / Build Standard 2.0

'Optimizely.InitializeComponents(IEventDispatcher, ILogger, IErrorHandler, UserProfileService, NotificationCenter, EventProcessor, OptimizelyDecideOption[], IOdpManager)' is obsolete

Check warning on line 144 in OptimizelySDK/Optimizely.cs

View workflow job for this annotation

GitHub Actions / Build Standard 2.0

'Optimizely.InitializeComponents(IEventDispatcher, ILogger, IErrorHandler, UserProfileService, NotificationCenter, EventProcessor, OptimizelyDecideOption[], IOdpManager)' is obsolete
null, eventProcessor, defaultDecideOptions, odpManager);
#else
InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService,
Expand Down Expand Up @@ -209,7 +209,7 @@
ProjectConfigManager = configManager;

#if USE_ODP
InitializeComponents(eventDispatcher, logger, errorHandler, userProfileService,

Check warning on line 212 in OptimizelySDK/Optimizely.cs

View workflow job for this annotation

GitHub Actions / Build Standard 2.0

'Optimizely.InitializeComponents(IEventDispatcher, ILogger, IErrorHandler, UserProfileService, NotificationCenter, EventProcessor, OptimizelyDecideOption[], IOdpManager)' is obsolete

Check warning on line 212 in OptimizelySDK/Optimizely.cs

View workflow job for this annotation

GitHub Actions / Build Standard 2.0

'Optimizely.InitializeComponents(IEventDispatcher, ILogger, IErrorHandler, UserProfileService, NotificationCenter, EventProcessor, OptimizelyDecideOption[], IOdpManager)' is obsolete
notificationCenter, eventProcessor, defaultDecideOptions, odpManager);

var projectConfig = ProjectConfigManager.CachedProjectConfig;
Expand Down Expand Up @@ -564,7 +564,8 @@

// This information is only necessary for feature tests.
// For rollouts experiments and variations are an implementation detail only.
if (decision?.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST)
if (decision?.Source == FeatureDecision.DECISION_SOURCE_FEATURE_TEST ||
decision?.Source == FeatureDecision.DECISION_SOURCE_HOLDOUT)
{
decisionSource = decision.Source;
sourceInfo["experimentKey"] = decision.Experiment.Key;
Expand Down
Loading