From 5cb4eb30ea8153fb141536014698b9847f6536a5 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 6 Aug 2025 20:22:08 +0600 Subject: [PATCH 01/17] fix: update method parameters in Optimizely.java and Bucketer.java to use ExperimentCore instead of Experiment. Fix ActivateNotification, ExperimentUtils, ActivateNotificationListener, NotificationCenterTest to reflect these changes. --- .../java/com/optimizely/ab/Optimizely.java | 3 +- .../com/optimizely/ab/bucketing/Bucketer.java | 25 +++++++++----- .../ab/bucketing/FeatureDecision.java | 12 ++++--- .../ab/event/internal/UserEventFactory.java | 17 ++++++---- .../ab/internal/ExperimentUtils.java | 22 ++++++------ .../ab/notification/ActivateNotification.java | 12 +++---- .../ActivateNotificationListener.java | 10 +++--- ...ActivateNotificationListenerInterface.java | 11 +++--- .../ActivateNotificationListenerTest.java | 20 ++++++----- .../notification/NotificationCenterTest.java | 34 ++++++++++--------- 10 files changed, 94 insertions(+), 72 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index 6eead11c6..f18c61283 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -24,6 +24,7 @@ import com.optimizely.ab.config.DatafileProjectConfig; import com.optimizely.ab.config.EventType; import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ExperimentCore; import com.optimizely.ab.config.FeatureFlag; import com.optimizely.ab.config.FeatureVariable; import com.optimizely.ab.config.FeatureVariableUsageInstance; @@ -319,7 +320,7 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig, * @param ruleType It can either be experiment in case impression event is sent from activate or it's feature-test or rollout */ private boolean sendImpression(@Nonnull ProjectConfig projectConfig, - @Nullable Experiment experiment, + @Nullable ExperimentCore experiment, @Nonnull String userId, @Nonnull Map filteredAttributes, @Nullable Variation variation, diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java index b92d2cf15..db1ea2877 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java @@ -16,18 +16,25 @@ */ package com.optimizely.ab.bucketing; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.concurrent.Immutable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.optimizely.ab.annotations.VisibleForTesting; import com.optimizely.ab.bucketing.internal.MurmurHash3; -import com.optimizely.ab.config.*; +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ExperimentCore; +import com.optimizely.ab.config.Group; +import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.TrafficAllocation; +import com.optimizely.ab.config.Variation; import com.optimizely.ab.optimizelydecision.DecisionReasons; import com.optimizely.ab.optimizelydecision.DecisionResponse; import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nonnull; -import javax.annotation.concurrent.Immutable; -import java.util.List; /** * Default Optimizely bucketing algorithm that evenly distributes users using the Murmur3 hash of some provided @@ -89,7 +96,7 @@ private Experiment bucketToExperiment(@Nonnull Group group, } @Nonnull - private DecisionResponse bucketToVariation(@Nonnull Experiment experiment, + private DecisionResponse bucketToVariation(@Nonnull ExperimentCore experiment, @Nonnull String bucketingId) { DecisionReasons reasons = DefaultDecisionReasons.newInstance(); @@ -130,7 +137,7 @@ private DecisionResponse bucketToVariation(@Nonnull Experiment experi * @return A {@link DecisionResponse} including the {@link Variation} that user is bucketed into (or null) and the decision reasons */ @Nonnull - public DecisionResponse bucket(@Nonnull Experiment experiment, + public DecisionResponse bucket(@Nonnull ExperimentCore experiment, @Nonnull String bucketingId, @Nonnull ProjectConfig projectConfig) { DecisionReasons reasons = DefaultDecisionReasons.newInstance(); diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/FeatureDecision.java b/core-api/src/main/java/com/optimizely/ab/bucketing/FeatureDecision.java index b0f0a11ed..d4acaa7f5 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/FeatureDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/FeatureDecision.java @@ -15,17 +15,18 @@ ***************************************************************************/ package com.optimizely.ab.bucketing; +import javax.annotation.Nullable; + import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ExperimentCore; import com.optimizely.ab.config.Variation; -import javax.annotation.Nullable; - public class FeatureDecision { /** * The {@link Experiment} the Feature is associated with. */ @Nullable - public Experiment experiment; + public ExperimentCore experiment; /** * The {@link Variation} the user was bucketed into. @@ -41,7 +42,8 @@ public class FeatureDecision { public enum DecisionSource { FEATURE_TEST("feature-test"), - ROLLOUT("rollout"); + ROLLOUT("rollout"), + HOLDOUT("holdout"); private final String key; @@ -62,7 +64,7 @@ public String toString() { * @param variation The {@link Variation} the user was bucketed into. * @param decisionSource The source of the variation. */ - public FeatureDecision(@Nullable Experiment experiment, @Nullable Variation variation, + public FeatureDecision(@Nullable ExperimentCore experiment, @Nullable Variation variation, @Nullable DecisionSource decisionSource) { this.experiment = experiment; this.variation = variation; diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/UserEventFactory.java b/core-api/src/main/java/com/optimizely/ab/event/internal/UserEventFactory.java index 9c44f455b..c8687f7a6 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/UserEventFactory.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/UserEventFactory.java @@ -16,23 +16,26 @@ */ package com.optimizely.ab.event.internal; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.optimizely.ab.bucketing.FeatureDecision; -import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ExperimentCore; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.Variation; import com.optimizely.ab.event.internal.payload.DecisionMetadata; import com.optimizely.ab.internal.EventTagUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.Map; public class UserEventFactory { private static final Logger logger = LoggerFactory.getLogger(UserEventFactory.class); public static ImpressionEvent createImpressionEvent(@Nonnull ProjectConfig projectConfig, - @Nullable Experiment activatedExperiment, + @Nullable ExperimentCore activatedExperiment, @Nullable Variation variation, @Nonnull String userId, @Nonnull Map attributes, diff --git a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java index 8da421885..2abb131c6 100644 --- a/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java +++ b/core-api/src/main/java/com/optimizely/ab/internal/ExperimentUtils.java @@ -16,8 +16,17 @@ */ package com.optimizely.ab.internal; +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nonnull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.optimizely.ab.OptimizelyUserContext; import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ExperimentCore; import com.optimizely.ab.config.ProjectConfig; import com.optimizely.ab.config.audience.AudienceIdCondition; import com.optimizely.ab.config.audience.Condition; @@ -25,13 +34,6 @@ import com.optimizely.ab.optimizelydecision.DecisionReasons; import com.optimizely.ab.optimizelydecision.DecisionResponse; import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; public final class ExperimentUtils { @@ -62,7 +64,7 @@ public static boolean isExperimentActive(@Nonnull Experiment experiment) { */ @Nonnull public static DecisionResponse doesUserMeetAudienceConditions(@Nonnull ProjectConfig projectConfig, - @Nonnull Experiment experiment, + @Nonnull ExperimentCore experiment, @Nonnull OptimizelyUserContext user, @Nonnull String loggingEntityType, @Nonnull String loggingKey) { @@ -86,7 +88,7 @@ public static DecisionResponse doesUserMeetAudienceConditions(@Nonnull @Nonnull public static DecisionResponse evaluateAudience(@Nonnull ProjectConfig projectConfig, - @Nonnull Experiment experiment, + @Nonnull ExperimentCore experiment, @Nonnull OptimizelyUserContext user, @Nonnull String loggingEntityType, @Nonnull String loggingKey) { @@ -118,7 +120,7 @@ public static DecisionResponse evaluateAudience(@Nonnull ProjectConfig @Nonnull public static DecisionResponse evaluateAudienceConditions(@Nonnull ProjectConfig projectConfig, - @Nonnull Experiment experiment, + @Nonnull ExperimentCore experiment, @Nonnull OptimizelyUserContext user, @Nonnull String loggingEntityType, @Nonnull String loggingKey) { diff --git a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java index dc70079de..c1c830432 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java @@ -16,13 +16,13 @@ */ package com.optimizely.ab.notification; +import java.util.Map; + import com.optimizely.ab.annotations.VisibleForTesting; -import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ExperimentCore; import com.optimizely.ab.config.Variation; import com.optimizely.ab.event.LogEvent; -import java.util.Map; - /** * ActivateNotification supplies notification for AB activatation. * @@ -32,7 +32,7 @@ @Deprecated public final class ActivateNotification { - private final Experiment experiment; + private final ExperimentCore experiment; private final String userId; private final Map attributes; private final Variation variation; @@ -50,7 +50,7 @@ public final class ActivateNotification { * @param variation - The variation that was returned from activate. * @param event - The impression event that was triggered. */ - public ActivateNotification(Experiment experiment, String userId, Map attributes, Variation variation, LogEvent event) { + public ActivateNotification(ExperimentCore experiment, String userId, Map attributes, Variation variation, LogEvent event) { this.experiment = experiment; this.userId = userId; this.attributes = attributes; @@ -58,7 +58,7 @@ public ActivateNotification(Experiment experiment, String userId, Map this.event = event; } - public Experiment getExperiment() { + public ExperimentCore getExperiment() { return experiment; } diff --git a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListener.java b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListener.java index 4ca602c77..100644f7c 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListener.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListener.java @@ -17,13 +17,15 @@ package com.optimizely.ab.notification; +import java.util.Map; + +import javax.annotation.Nonnull; + import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ExperimentCore; import com.optimizely.ab.config.Variation; import com.optimizely.ab.event.LogEvent; -import javax.annotation.Nonnull; -import java.util.Map; - /** * ActivateNotificationListener handles the activate event notification. * @@ -80,7 +82,7 @@ public final void handle(ActivateNotification message) { * @param variation - The variation that was returned from activate. * @param event - The impression event that was triggered. */ - public abstract void onActivate(@Nonnull Experiment experiment, + public abstract void onActivate(@Nonnull ExperimentCore experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, diff --git a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java index c0a1e3a73..6feda2ef6 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java @@ -16,12 +16,13 @@ */ package com.optimizely.ab.notification; -import com.optimizely.ab.config.Experiment; -import com.optimizely.ab.config.Variation; -import com.optimizely.ab.event.LogEvent; +import java.util.Map; import javax.annotation.Nonnull; -import java.util.Map; + +import com.optimizely.ab.config.ExperimentCore; +import com.optimizely.ab.config.Variation; +import com.optimizely.ab.event.LogEvent; /** * ActivateNotificationListenerInterface provides and interface for activate event notification. @@ -40,7 +41,7 @@ public interface ActivateNotificationListenerInterface { * @param variation - The variation that was returned from activate. * @param event - The impression event that was triggered. */ - public void onActivate(@Nonnull Experiment experiment, + public void onActivate(@Nonnull ExperimentCore experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, diff --git a/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java b/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java index f7fcda09b..4d2a42114 100644 --- a/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java +++ b/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java @@ -16,19 +16,21 @@ */ package com.optimizely.ab.notification; -import com.optimizely.ab.config.Experiment; -import com.optimizely.ab.config.Variation; -import com.optimizely.ab.event.LogEvent; -import org.junit.Before; -import org.junit.Test; - -import javax.annotation.Nonnull; import java.util.Collections; import java.util.Map; -import static org.junit.Assert.*; +import javax.annotation.Nonnull; + +import static org.junit.Assert.assertEquals; +import org.junit.Before; +import org.junit.Test; import static org.mockito.Mockito.mock; +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.ExperimentCore; +import com.optimizely.ab.config.Variation; +import com.optimizely.ab.event.LogEvent; + public class ActivateNotificationListenerTest { private static final Experiment EXPERIMENT = mock(Experiment.class); @@ -64,7 +66,7 @@ public void testNotifyWithActivateNotificationArg() { private static class ActivateNotificationHandler extends ActivateNotificationListener { @Override - public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { + public void onActivate(@Nonnull ExperimentCore experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { assertEquals(EXPERIMENT, experiment); assertEquals(USER_ID, userId); assertEquals(USER_ATTRIBUTES, attributes); diff --git a/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java b/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java index c9e911029..69f46107d 100644 --- a/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java +++ b/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java @@ -16,29 +16,31 @@ */ package com.optimizely.ab.notification; -import ch.qos.logback.classic.Level; -import com.optimizely.ab.OptimizelyRuntimeException; -import com.optimizely.ab.config.Experiment; -import com.optimizely.ab.config.Variation; -import com.optimizely.ab.event.LogEvent; -import com.optimizely.ab.internal.LogbackVerifier; -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; - -import javax.annotation.Nonnull; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import static junit.framework.TestCase.assertNotSame; -import static junit.framework.TestCase.assertTrue; +import javax.annotation.Nonnull; + +import org.junit.After; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; import static org.mockito.Mockito.mock; +import com.optimizely.ab.OptimizelyRuntimeException; +import com.optimizely.ab.config.ExperimentCore; +import com.optimizely.ab.config.Variation; +import com.optimizely.ab.event.LogEvent; +import com.optimizely.ab.internal.LogbackVerifier; + +import ch.qos.logback.classic.Level; +import static junit.framework.TestCase.assertNotSame; +import static junit.framework.TestCase.assertTrue; + public class NotificationCenterTest { private NotificationCenter notificationCenter; private ActivateNotificationListener activateNotification; @@ -92,7 +94,7 @@ public void testAddDecisionNotificationTwice() { public void testAddActivateNotificationTwice() { ActivateNotificationListener listener = new ActivateNotificationListener() { @Override - public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { + public void onActivate(@Nonnull ExperimentCore experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { } }; @@ -107,7 +109,7 @@ public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @ public void testAddActivateNotification() { int notificationId = notificationCenter.addActivateNotificationListener(new ActivateNotificationListener() { @Override - public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { + public void onActivate(@Nonnull ExperimentCore experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { } }); From b7f169b23306b4ea35b050685877044fcea2a25d Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Wed, 6 Aug 2025 23:07:34 +0600 Subject: [PATCH 02/17] feat: add holdout rule support in DecisionService - Add logic to process holdout rules for feature flags - Implement a method to determine variation for a holdout rule - Create decision responses based on a holdout decision and its reasons - Update logger messages for holdout rule evaluation --- .../ab/bucketing/DecisionService.java | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index ff48ffb99..0962cafc1 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -240,10 +240,22 @@ public List> getVariationsForFeatureList(@Non List> decisions = new ArrayList<>(); - for (FeatureFlag featureFlag: featureFlags) { + flagLoop: for (FeatureFlag featureFlag: featureFlags) { DecisionReasons reasons = DefaultDecisionReasons.newInstance(); reasons.merge(upsReasons); + List holdouts = projectConfig.getHoldoutForFlag(featureFlag.getId()); + if (!holdouts.isEmpty()) { + for (Holdout holdout : holdouts) { + DecisionResponse holdoutDecision = getVariationForHoldout(holdout, user, projectConfig); + reasons.merge(holdoutDecision.getReasons()); + if (holdoutDecision.getResult() != null) { + decisions.add(new DecisionResponse<>(new FeatureDecision(holdout, holdoutDecision.getResult(), FeatureDecision.DecisionSource.HOLDOUT), reasons)); + continue flagLoop; + } + } + } + DecisionResponse decisionVariationResponse = getVariationFromExperiment(projectConfig, featureFlag, user, options, userProfileTracker); reasons.merge(decisionVariationResponse.getReasons()); @@ -419,6 +431,50 @@ DecisionResponse getWhitelistedVariation(@Nonnull Experiment experime return new DecisionResponse(null, reasons); } + /** + * Determines the variation for a holdout rule. + * + * @param holdout The holdout rule to evaluate. + * @param user The user context. + * @param projectConfig The current project configuration. + * @return A {@link DecisionResponse} with the variation (if any) and reasons. + */ + @Nonnull + DecisionResponse getVariationForHoldout(@Nonnull Holdout holdout, + @Nonnull OptimizelyUserContext user, + @Nonnull ProjectConfig projectConfig) { + DecisionReasons reasons = DefaultDecisionReasons.newInstance(); + + if (!holdout.isActive()) { + String message = reasons.addInfo("Holdout \"%s\" is not running.", holdout.getKey()); + logger.info(message); + return new DecisionResponse<>(null, reasons); + } + + DecisionResponse decisionMeetAudience = ExperimentUtils.doesUserMeetAudienceConditions(projectConfig, holdout, user, EXPERIMENT, holdout.getKey()); + reasons.merge(decisionMeetAudience.getReasons()); + + if (decisionMeetAudience.getResult()) { + String bucketingId = getBucketingId(user.getUserId(), user.getAttributes()); + DecisionResponse decisionVariation = bucketer.bucket(holdout, bucketingId, projectConfig); + reasons.merge(decisionVariation.getReasons()); + Variation variation = decisionVariation.getResult(); + + if (variation != null) { + String message = reasons.addInfo("User (%s) is in variation (%s) of holdout (%s).", user.getUserId(), variation.getKey(), holdout.getKey()); + logger.info(message); + } else { + String message = reasons.addInfo("User (%s) is in no holdout variation.", user.getUserId()); + logger.info(message); + } + return new DecisionResponse<>(variation, reasons); + } + + String message = reasons.addInfo("User (%s) does not meet conditions for holdout (%s).", user.getUserId(), holdout.getKey()); + logger.info(message); + return new DecisionResponse<>(null, reasons); + } + // TODO: Logically, it makes sense to move this method to UserProfileTracker. But some tests are also calling this // method, requiring us to refactor those tests as well. We'll look to refactor this later. From c68b0968b268b63c6e320f58d43a802d799851eb Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 7 Aug 2025 00:21:46 +0600 Subject: [PATCH 03/17] fix(core-api): fix bucket method parameter in Bucketer.java --- .../src/main/java/com/optimizely/ab/bucketing/Bucketer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java index db1ea2877..35fa21c71 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/Bucketer.java @@ -41,7 +41,7 @@ * identifier. *

* The user identifier must be provided in the first data argument passed to - * {@link #bucket(Experiment, String, ProjectConfig)} and must be non-null and non-empty. + * {@link #bucket(ExperimentCore, String, ProjectConfig)} and must be non-null and non-empty. * * @see MurmurHash */ From d8068e2c55f4a52d16995388a857c182527a9053 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 7 Aug 2025 17:31:26 +0600 Subject: [PATCH 04/17] fix: correct visibility and initialization of constants in ValidProjectConfigV4 - Change visibility of FEATURE_FLAG_BOOLEAN_FEATURE to public - Change visibility of Variation VARIATION_HOLDOUT_VARIATION_OFF to public - Update entityIds in HOLDOUT_BASIC_HOLDOUT and holdouts-project-config.json to use "$opt_dummy_variation_id" instead of hardcoded values --- .../ab/bucketing/DecisionServiceTest.java | 25 +++++++++++++++++++ .../ab/config/ValidProjectConfigV4.java | 16 ++++++------ .../config/holdouts-project-config.json | 10 ++++---- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java index d818826d4..4e1a55c9c 100644 --- a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java @@ -1228,4 +1228,29 @@ public void setForcedVariationMultipleUsers() { assertNull(decisionService.getForcedVariation(experiment2, "testUser2").getResult()); } + @Test + public void getVariationForFeatureReturnHoldoutDecisionForGlobalHoldout() { + ProjectConfig holdoutProjectConfig = generateValidProjectConfigV4_holdout(); + + Bucketer mockBucketer = new Bucketer(); + + DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, null); + + Map attributes = new HashMap<>(); + attributes.put("$opt_bucketing_id", "ppid160000"); + FeatureDecision featureDecision = decisionService.getVariationForFeature( + FEATURE_FLAG_BOOLEAN_FEATURE, + optimizely.createUserContext("user123", attributes), + holdoutProjectConfig + ).getResult(); + + assertEquals(VARIATION_HOLDOUT_VARIATION_OFF, featureDecision.variation); + assertEquals(FeatureDecision.DecisionSource.HOLDOUT, featureDecision.decisionSource); + + logbackVerifier.expectMessage(Level.INFO, "User (user123) is in variation (ho_off_key) of holdout (basic_holdout)."); + } + + + + } diff --git a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java index 0d8f5d3c0..e9fedf9db 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java @@ -235,7 +235,7 @@ public class ValidProjectConfigV4 { // features private static final String FEATURE_BOOLEAN_FEATURE_ID = "4195505407"; private static final String FEATURE_BOOLEAN_FEATURE_KEY = "boolean_feature"; - private static final FeatureFlag FEATURE_FLAG_BOOLEAN_FEATURE = new FeatureFlag( + public static final FeatureFlag FEATURE_FLAG_BOOLEAN_FEATURE = new FeatureFlag( FEATURE_BOOLEAN_FEATURE_ID, FEATURE_BOOLEAN_FEATURE_KEY, "", @@ -490,7 +490,7 @@ public class ValidProjectConfigV4 { VARIATION_BASIC_EXPERIMENT_VARIATION_A_KEY, Collections.emptyList() ); - private static final Variation VARIATION_HOLDOUT_VARIATION_OFF = new Variation( + public static final Variation VARIATION_HOLDOUT_VARIATION_OFF = new Variation( "$opt_dummy_variation_id", "ho_off_key", false @@ -536,7 +536,7 @@ public class ValidProjectConfigV4 { ) ) ); - private static final Holdout HOLDOUT_BASIC_HOLDOUT = new Holdout( + public static final Holdout HOLDOUT_BASIC_HOLDOUT = new Holdout( "10075323428", "basic_holdout", Holdout.HoldoutStatus.RUNNING.toString(), @@ -547,7 +547,7 @@ public class ValidProjectConfigV4 { ), DatafileProjectConfigTestUtils.createListOfObjects( new TrafficAllocation( - "327323", + "$opt_dummy_variation_id", 500 ) ), @@ -566,7 +566,7 @@ public class ValidProjectConfigV4 { ), DatafileProjectConfigTestUtils.createListOfObjects( new TrafficAllocation( - "327323", + "$opt_dummy_variation_id", 0 ) ), @@ -585,7 +585,7 @@ public class ValidProjectConfigV4 { ), DatafileProjectConfigTestUtils.createListOfObjects( new TrafficAllocation( - "327323", + "$opt_dummy_variation_id", 2000 ) ), @@ -608,7 +608,7 @@ public class ValidProjectConfigV4 { ), DatafileProjectConfigTestUtils.createListOfObjects( new TrafficAllocation( - "327323", + "$opt_dummy_variation_id", 1500 ) ), @@ -636,7 +636,7 @@ public class ValidProjectConfigV4 { ), DatafileProjectConfigTestUtils.createListOfObjects( new TrafficAllocation( - "327323", + "$opt_dummy_variation_id", 1000 ) ), diff --git a/core-api/src/test/resources/config/holdouts-project-config.json b/core-api/src/test/resources/config/holdouts-project-config.json index 5a83fad17..585ae8572 100644 --- a/core-api/src/test/resources/config/holdouts-project-config.json +++ b/core-api/src/test/resources/config/holdouts-project-config.json @@ -483,7 +483,7 @@ "trafficAllocation": [ { "endOfRange": 0, - "entityId": "327323" + "entityId": "$opt_dummy_variation_id" } ], "variations": [ @@ -502,7 +502,7 @@ "trafficAllocation": [ { "endOfRange": 2000, - "entityId": "327323" + "entityId": "$opt_dummy_variation_id" } ], "variations": [ @@ -526,7 +526,7 @@ "trafficAllocation": [ { "endOfRange": 500, - "entityId": "327323" + "entityId": "$opt_dummy_variation_id" } ], "variations": [ @@ -544,7 +544,7 @@ "trafficAllocation": [ { "endOfRange": 1000, - "entityId": "327323" + "entityId": "$opt_dummy_variation_id" } ], "variations": [ @@ -565,7 +565,7 @@ "trafficAllocation": [ { "endOfRange": 1500, - "entityId": "327323" + "entityId": "$opt_dummy_variation_id" } ], "variations": [ From 8f831f11586d4a0c81283e7892764987d161f685 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 7 Aug 2025 18:16:22 +0600 Subject: [PATCH 05/17] test: add tests for holdout feature in DecisionServiceTest - Create test for checking if a user is in a variation for holdout feature when included in flags - Implement test to verify exclusion of user from variation for holdout when specified as excluded from flags - Add assertions to validate decision source, experiment, and variation for holdout feature --- .../ab/bucketing/DecisionServiceTest.java | 142 +++++++++++++++--- 1 file changed, 122 insertions(+), 20 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java index 4e1a55c9c..3b1cf6f5d 100644 --- a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java @@ -15,35 +15,86 @@ ***************************************************************************/ package com.optimizely.ab.bucketing; -import ch.qos.logback.classic.Level; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMapOf; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import org.mockito.Mock; +import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import com.fasterxml.jackson.annotation.JsonFormat; import com.optimizely.ab.Optimizely; import com.optimizely.ab.OptimizelyDecisionContext; import com.optimizely.ab.OptimizelyForcedDecision; import com.optimizely.ab.OptimizelyUserContext; -import com.optimizely.ab.config.*; +import com.optimizely.ab.config.DatafileProjectConfigTestUtils; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.noAudienceProjectConfigV3; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV3; +import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.validProjectConfigV4; +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.Rollout; +import com.optimizely.ab.config.TrafficAllocation; +import com.optimizely.ab.config.ValidProjectConfigV4; +import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_NATIONALITY_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_ENGLISH_CITIZENS_VALUE; +import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; +import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_BOOLEAN_FEATURE; +import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_MULTI_VARIATE_FEATURE; +import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_MULTI_VARIATE_FUTURE_FEATURE; +import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_MUTEX_GROUP_FEATURE; +import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_SINGLE_VARIABLE_BOOLEAN; +import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_FLAG_SINGLE_VARIABLE_INTEGER; +import static com.optimizely.ab.config.ValidProjectConfigV4.FEATURE_MULTI_VARIATE_FEATURE_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.HOLDOUT_BASIC_HOLDOUT; +import static com.optimizely.ab.config.ValidProjectConfigV4.HOLDOUT_EXCLUDED_FLAGS_HOLDOUT; +import static com.optimizely.ab.config.ValidProjectConfigV4.HOLDOUT_INCLUDED_FLAGS_HOLDOUT; +import static com.optimizely.ab.config.ValidProjectConfigV4.ROLLOUT_2; +import static com.optimizely.ab.config.ValidProjectConfigV4.ROLLOUT_3_EVERYONE_ELSE_RULE; +import static com.optimizely.ab.config.ValidProjectConfigV4.ROLLOUT_3_EVERYONE_ELSE_RULE_ENABLED_VARIATION; +import static com.optimizely.ab.config.ValidProjectConfigV4.VARIATION_HOLDOUT_VARIATION_OFF; +import static com.optimizely.ab.config.ValidProjectConfigV4.generateValidProjectConfigV4_holdout; +import com.optimizely.ab.config.Variation; import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.internal.ControlAttribute; import com.optimizely.ab.internal.LogbackVerifier; import com.optimizely.ab.optimizelydecision.DecisionReasons; import com.optimizely.ab.optimizelydecision.DecisionResponse; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnit; -import org.mockito.junit.MockitoRule; - -import java.util.*; -import static com.optimizely.ab.config.DatafileProjectConfigTestUtils.*; -import static com.optimizely.ab.config.ValidProjectConfigV4.*; +import ch.qos.logback.classic.Level; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import static junit.framework.TestCase.assertEquals; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; -import static org.mockito.Mockito.*; public class DecisionServiceTest { @@ -1243,14 +1294,65 @@ public void getVariationForFeatureReturnHoldoutDecisionForGlobalHoldout() { optimizely.createUserContext("user123", attributes), holdoutProjectConfig ).getResult(); - + + assertEquals(HOLDOUT_BASIC_HOLDOUT, featureDecision.experiment); assertEquals(VARIATION_HOLDOUT_VARIATION_OFF, featureDecision.variation); assertEquals(FeatureDecision.DecisionSource.HOLDOUT, featureDecision.decisionSource); logbackVerifier.expectMessage(Level.INFO, "User (user123) is in variation (ho_off_key) of holdout (basic_holdout)."); } - + @Test + public void includedFlagsHoldoutOnlyAppliestoSpecificFlags() { + ProjectConfig holdoutProjectConfig = generateValidProjectConfigV4_holdout(); + Bucketer mockBucketer = new Bucketer(); + DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, null); + + Map attributes = new HashMap<>(); + attributes.put("$opt_bucketing_id", "ppid120000"); + FeatureDecision featureDecision = decisionService.getVariationForFeature( + FEATURE_FLAG_BOOLEAN_FEATURE, + optimizely.createUserContext("user123", attributes), + holdoutProjectConfig + ).getResult(); + + assertEquals(HOLDOUT_INCLUDED_FLAGS_HOLDOUT, featureDecision.experiment); + assertEquals(VARIATION_HOLDOUT_VARIATION_OFF, featureDecision.variation); + assertEquals(FeatureDecision.DecisionSource.HOLDOUT, featureDecision.decisionSource); + + logbackVerifier.expectMessage(Level.INFO, "User (user123) is in variation (ho_off_key) of holdout (holdout_included_flags)."); + } + + @Test + public void excludedFlagsHoldoutAppliesToAllExceptSpecified() { + ProjectConfig holdoutProjectConfig = generateValidProjectConfigV4_holdout(); + + Bucketer mockBucketer = new Bucketer(); + + DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, null); + + Map attributes = new HashMap<>(); + attributes.put("$opt_bucketing_id", "ppid300002"); + FeatureDecision excludedDecision = decisionService.getVariationForFeature( + FEATURE_FLAG_SINGLE_VARIABLE_BOOLEAN, // excluded from ho (holdout_excluded_flags) + optimizely.createUserContext("user123", attributes), + holdoutProjectConfig + ).getResult(); + + assertNotEquals(FeatureDecision.DecisionSource.HOLDOUT, excludedDecision.decisionSource); + + FeatureDecision featureDecision = decisionService.getVariationForFeature( + FEATURE_FLAG_SINGLE_VARIABLE_INTEGER, // excluded from ho (holdout_excluded_flags) + optimizely.createUserContext("user123", attributes), + holdoutProjectConfig + ).getResult(); + + assertEquals(HOLDOUT_EXCLUDED_FLAGS_HOLDOUT, featureDecision.experiment); + assertEquals(VARIATION_HOLDOUT_VARIATION_OFF, featureDecision.variation); + assertEquals(FeatureDecision.DecisionSource.HOLDOUT, featureDecision.decisionSource); + + logbackVerifier.expectMessage(Level.INFO, "User (user123) is in variation (ho_off_key) of holdout (holdout_excluded_flags)."); + } } From 524fc9f7dd761e44f07f50c325f7aaa498302e87 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 7 Aug 2025 18:41:08 +0600 Subject: [PATCH 06/17] feat: add HOLDOUT_TYPEDAUDIENCE_HOLDOUT to ValidProjectConfigV4 - Added public constant HOLDOUT_TYPEDAUDIENCE_HOLDOUT to class ValidProjectConfigV4 - Updated feature flag constants to public static in class ValidProjectConfigV4 - Updated holdout constants to public static in class ValidProjectConfigV4 - Created unit test userMeetsHoldoutAudienceConditions in DecisionServiceTest --- .../ab/bucketing/DecisionServiceTest.java | 28 ++++++++++++++++++- .../ab/config/ValidProjectConfigV4.java | 8 +++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java index 3b1cf6f5d..220a62efa 100644 --- a/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/bucketing/DecisionServiceTest.java @@ -51,7 +51,6 @@ import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import com.fasterxml.jackson.annotation.JsonFormat; import com.optimizely.ab.Optimizely; import com.optimizely.ab.OptimizelyDecisionContext; import com.optimizely.ab.OptimizelyForcedDecision; @@ -80,6 +79,7 @@ import static com.optimizely.ab.config.ValidProjectConfigV4.HOLDOUT_BASIC_HOLDOUT; import static com.optimizely.ab.config.ValidProjectConfigV4.HOLDOUT_EXCLUDED_FLAGS_HOLDOUT; import static com.optimizely.ab.config.ValidProjectConfigV4.HOLDOUT_INCLUDED_FLAGS_HOLDOUT; +import static com.optimizely.ab.config.ValidProjectConfigV4.HOLDOUT_TYPEDAUDIENCE_HOLDOUT; import static com.optimizely.ab.config.ValidProjectConfigV4.ROLLOUT_2; import static com.optimizely.ab.config.ValidProjectConfigV4.ROLLOUT_3_EVERYONE_ELSE_RULE; import static com.optimizely.ab.config.ValidProjectConfigV4.ROLLOUT_3_EVERYONE_ELSE_RULE_ENABLED_VARIATION; @@ -1355,4 +1355,30 @@ public void excludedFlagsHoldoutAppliesToAllExceptSpecified() { logbackVerifier.expectMessage(Level.INFO, "User (user123) is in variation (ho_off_key) of holdout (holdout_excluded_flags)."); } + + @Test + public void userMeetsHoldoutAudienceConditions() { + ProjectConfig holdoutProjectConfig = generateValidProjectConfigV4_holdout(); + + Bucketer mockBucketer = new Bucketer(); + + DecisionService decisionService = new DecisionService(mockBucketer, mockErrorHandler, null); + + Map attributes = new HashMap<>(); + attributes.put("$opt_bucketing_id", "ppid543400"); + attributes.put("booleanKey", true); + attributes.put("integerKey", 1); + + FeatureDecision featureDecision = decisionService.getVariationForFeature( + FEATURE_FLAG_BOOLEAN_FEATURE, + optimizely.createUserContext("user123", attributes), + holdoutProjectConfig + ).getResult(); + + assertEquals(HOLDOUT_TYPEDAUDIENCE_HOLDOUT, featureDecision.experiment); + assertEquals(VARIATION_HOLDOUT_VARIATION_OFF, featureDecision.variation); + assertEquals(FeatureDecision.DecisionSource.HOLDOUT, featureDecision.decisionSource); + + logbackVerifier.expectMessage(Level.INFO, "User (user123) is in variation (ho_off_key) of holdout (typed_audience_holdout)."); + } } diff --git a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java index e9fedf9db..0291c0ce1 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java @@ -294,7 +294,7 @@ public class ValidProjectConfigV4 { FeatureVariable.BOOLEAN_TYPE, null ); - private static final FeatureFlag FEATURE_FLAG_SINGLE_VARIABLE_BOOLEAN = new FeatureFlag( + public static final FeatureFlag FEATURE_FLAG_SINGLE_VARIABLE_BOOLEAN = new FeatureFlag( FEATURE_SINGLE_VARIABLE_BOOLEAN_ID, FEATURE_SINGLE_VARIABLE_BOOLEAN_KEY, "", @@ -574,7 +574,7 @@ public class ValidProjectConfigV4 { null ); - private static final Holdout HOLDOUT_INCLUDED_FLAGS_HOLDOUT = new Holdout( + public static final Holdout HOLDOUT_INCLUDED_FLAGS_HOLDOUT = new Holdout( "1007543323427", "holdout_included_flags", Holdout.HoldoutStatus.RUNNING.toString(), @@ -597,7 +597,7 @@ public class ValidProjectConfigV4 { null ); - private static final Holdout HOLDOUT_EXCLUDED_FLAGS_HOLDOUT = new Holdout( + public static final Holdout HOLDOUT_EXCLUDED_FLAGS_HOLDOUT = new Holdout( "100753234214", "holdout_excluded_flags", Holdout.HoldoutStatus.RUNNING.toString(), @@ -620,7 +620,7 @@ public class ValidProjectConfigV4 { ) ); - private static final Holdout HOLDOUT_TYPEDAUDIENCE_HOLDOUT = new Holdout( + public static final Holdout HOLDOUT_TYPEDAUDIENCE_HOLDOUT = new Holdout( "10075323429", "typed_audience_holdout", Holdout.HoldoutStatus.RUNNING.toString(), From c17e2a68ffec6b7880bfb94097cc9000dac596f6 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Thu, 7 Aug 2025 21:45:01 +0600 Subject: [PATCH 07/17] fix(core-api): update Experiment import to ExperimentCore in FeatureDecision class --- .../java/com/optimizely/ab/bucketing/FeatureDecision.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/FeatureDecision.java b/core-api/src/main/java/com/optimizely/ab/bucketing/FeatureDecision.java index d4acaa7f5..e53172e0a 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/FeatureDecision.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/FeatureDecision.java @@ -17,13 +17,12 @@ import javax.annotation.Nullable; -import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.ExperimentCore; import com.optimizely.ab.config.Variation; public class FeatureDecision { /** - * The {@link Experiment} the Feature is associated with. + * The {@link ExperimentCore} the Feature is associated with. */ @Nullable public ExperimentCore experiment; @@ -60,7 +59,7 @@ public String toString() { /** * Initialize a FeatureDecision object. * - * @param experiment The {@link Experiment} the Feature is associated with. + * @param experiment The {@link ExperimentCore} the Feature is associated with. * @param variation The {@link Variation} the user was bucketed into. * @param decisionSource The source of the variation. */ From 2bd5e177454e47ac5f3032902f0f4b9e65e9a46a Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Fri, 8 Aug 2025 17:00:10 +0600 Subject: [PATCH 08/17] chore: clarify holdout logs and replace wildcards --- .../ab/bucketing/DecisionService.java | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java index 0962cafc1..b7536aab5 100644 --- a/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java +++ b/core-api/src/main/java/com/optimizely/ab/bucketing/DecisionService.java @@ -15,27 +15,39 @@ ***************************************************************************/ package com.optimizely.ab.bucketing; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.optimizely.ab.OptimizelyDecisionContext; import com.optimizely.ab.OptimizelyForcedDecision; import com.optimizely.ab.OptimizelyRuntimeException; import com.optimizely.ab.OptimizelyUserContext; -import com.optimizely.ab.config.*; +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.Holdout; +import com.optimizely.ab.config.ProjectConfig; +import com.optimizely.ab.config.Rollout; +import com.optimizely.ab.config.Variation; import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.internal.ControlAttribute; import com.optimizely.ab.internal.ExperimentUtils; +import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.EXPERIMENT; +import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.RULE; import com.optimizely.ab.optimizelydecision.DecisionReasons; import com.optimizely.ab.optimizelydecision.DecisionResponse; import com.optimizely.ab.optimizelydecision.DefaultDecisionReasons; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.EXPERIMENT; -import static com.optimizely.ab.internal.LoggingConstants.LoggingEntityType.RULE; /** * Optimizely's decision service that determines which variation of an experiment the user will be allocated to. @@ -446,7 +458,7 @@ DecisionResponse getVariationForHoldout(@Nonnull Holdout holdout, DecisionReasons reasons = DefaultDecisionReasons.newInstance(); if (!holdout.isActive()) { - String message = reasons.addInfo("Holdout \"%s\" is not running.", holdout.getKey()); + String message = reasons.addInfo("Holdout (%s) is not running.", holdout.getKey()); logger.info(message); return new DecisionResponse<>(null, reasons); } @@ -455,6 +467,10 @@ DecisionResponse getVariationForHoldout(@Nonnull Holdout holdout, reasons.merge(decisionMeetAudience.getReasons()); if (decisionMeetAudience.getResult()) { + // User meets audience conditions for holdout + String audienceMatchMessage = reasons.addInfo("User (%s) meets audience conditions for holdout (%s).", user.getUserId(), holdout.getKey()); + logger.info(audienceMatchMessage); + String bucketingId = getBucketingId(user.getUserId(), user.getAttributes()); DecisionResponse decisionVariation = bucketer.bucket(holdout, bucketingId, projectConfig); reasons.merge(decisionVariation.getReasons()); From 56ab9f1b313426ea39f56bdb26196abab0dcc6e3 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Fri, 8 Aug 2025 17:38:50 +0600 Subject: [PATCH 09/17] test: add holdout decision test to verify behavior --- .../ab/OptimizelyUserContextTest.java | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index bb2d36192..6f3607788 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -57,6 +57,9 @@ import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; public class OptimizelyUserContextTest { @@ -76,6 +79,8 @@ public class OptimizelyUserContextTest { Map featureKeyMapping; Map groupIdMapping; + private String holdoutDatafile; + @Before public void setUp() throws Exception { datafile = Resources.toString(Resources.getResource("config/decide-project-config.json"), Charsets.UTF_8); @@ -85,6 +90,16 @@ public void setUp() throws Exception { .build(); } + private Optimizely createOptimizelyWithHoldouts() throws Exception { + if (holdoutDatafile == null) { + holdoutDatafile = com.google.common.io.Resources.toString( + com.google.common.io.Resources.getResource("config/holdouts-project-config.json"), + com.google.common.base.Charsets.UTF_8 + ); + } + return new Optimizely.Builder().withDatafile(holdoutDatafile).withEventProcessor(new ForwardingEventProcessor(eventHandler, null)).build(); + } + @Test public void optimizelyUserContext_withAttributes() { Map attributes = Collections.singletonMap(ATTRIBUTE_HOUSE_KEY, AUDIENCE_GRYFFINDOR_VALUE); @@ -749,7 +764,7 @@ public void decisionNotification() { public void decideOptions_bypassUPS() throws Exception { String flagKey = "feature_2"; // embedding experiment: "exp_no_audience" String experimentId = "10420810910"; // "exp_no_audience" - String variationId1 = "10418551353"; + String variationId = "10418551353"; String variationId2 = "10418510624"; String variationKey1 = "variation_with_traffic"; String variationKey2 = "variation_no_traffic"; @@ -1786,6 +1801,8 @@ public void fetchQualifiedSegmentsAsync() throws InterruptedException { .withODPManager(mockODPManager) .build(); + + OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); CountDownLatch countDownLatch = new CountDownLatch(1); @@ -2084,4 +2101,42 @@ OptimizelyDecision callDecideWithIncludeReasons(String flagKey) { return callDecideWithIncludeReasons(flagKey, Collections.emptyMap()); } + @Test + public void decide_holdoutApplied_basic() throws Exception { + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + // pick a flag that is eligible for basic_holdout. Using boolean_feature from config. + String flagKey = "boolean_feature"; + String userId = "user123"; + Map attrs = new HashMap<>(); + attrs.put("$opt_bucketing_id", "ppid160000"); + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + + // include reasons to surface holdout logs in decision reasons if implemented + OptimizelyDecision decision = user.decide(flagKey, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertNotNull(decision); + assertEquals(flagKey, decision.getFlagKey()); + // holdout off variation => feature should be disabled + assertFalse(decision.getEnabled()); + // Expect decision source to be holdout (either via metadata map or reasons text) + boolean hasHoldoutReason = false; + String expectedMString = "User (" + userId + ") is in variation (ho_off_key) of holdout (basic_holdout)."; + if (decision.getReasons().contains(expectedMString)) { + hasHoldoutReason = true; + } + assertTrue("Expected holdout to influence decision (reason containing 'holdout')", hasHoldoutReason); + logbackVerifier.expectMessage(Level.INFO, expectedMString); + + // Impression expectation: Holdout decisions still dispatch an impression with holdout context. + String variationId = "$opt_dummy_variation_id"; // from holdouts-project-config.json + String experimentId = "10075323428"; // holdout id for basic_holdout + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("basic_holdout") + .setRuleType("holdout") + .setVariationKey("ho_off_key") + .setEnabled(false) + .build(); + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); + } } From 1f33478aacc33a81c88a85817afc66e988af0866 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Sat, 9 Aug 2025 00:09:35 +0600 Subject: [PATCH 10/17] refactor: rename test from 'decide_holdoutApplied_basic' to 'decide_with_holdout' to accurately reflect the functionality being tested --- .../ab/OptimizelyUserContextTest.java | 112 +++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 6f3607788..4cb4ebc0d 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -2102,7 +2102,7 @@ OptimizelyDecision callDecideWithIncludeReasons(String flagKey) { } @Test - public void decide_holdoutApplied_basic() throws Exception { + public void decide_with_holdout() throws Exception { Optimizely optWithHoldout = createOptimizelyWithHoldouts(); // pick a flag that is eligible for basic_holdout. Using boolean_feature from config. String flagKey = "boolean_feature"; @@ -2139,4 +2139,114 @@ public void decide_holdoutApplied_basic() throws Exception { .build(); eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); } + + @Test + public void decide_for_keys_with_holdout() throws Exception { + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String userId = "user123"; + Map attrs = new HashMap<>(); + attrs.put("$opt_bucketing_id", "ppid300002"); // deterministic bucketing used in prior holdout test + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + + List flagKeys = Arrays.asList( + "boolean_feature", // previously validated basic_holdout membership + "double_single_variable_feature", // also subject to global/basic holdout + "integer_single_variable_feature" // also subject to global/basic holdout + ); + + Map decisions = user.decideForKeys(flagKeys, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); + assertEquals(3, decisions.size()); + + String holdoutExperimentId = "10075323428"; // basic_holdout id + String variationId = "$opt_dummy_variation_id"; + String variationKey = "ho_off_key"; + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (basic_holdout)."; + + for (String flagKey : flagKeys) { + OptimizelyDecision d = decisions.get(flagKey); + assertNotNull(d); + assertEquals(flagKey, d.getFlagKey()); + assertEquals(variationKey, d.getVariationKey()); + assertFalse(d.getEnabled()); + assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("basic_holdout") + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + // attributes map expected empty (reserved $opt_ attribute filtered out) + eventHandler.expectImpression(holdoutExperimentId, variationId, userId, Collections.emptyMap(), metadata); + } + + // At least one log message confirming holdout membership + logbackVerifier.expectMessage(Level.INFO, expectedReason); + } + + @Test + public void decide_all_with_holdout() throws Exception { + + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String userId = "user123"; + Map attrs = new HashMap<>(); + // ppid120000 buckets user into holdout_included_flags + attrs.put("$opt_bucketing_id", "ppid120000"); + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + + // All flag keys present in holdouts-project-config.json + List allFlagKeys = Arrays.asList( + "boolean_feature", + "double_single_variable_feature", + "integer_single_variable_feature", + "boolean_single_variable_feature", + "string_single_variable_feature", + "multi_variate_feature", + "multi_variate_future_feature", + "mutex_group_feature" + ); + + // Flags INCLUDED in holdout_included_flags (only these should be holdout decisions) + List includedInHoldout = Arrays.asList( + "boolean_feature", + "double_single_variable_feature", + "integer_single_variable_feature" + ); + + Map decisions = user.decideAll(Arrays.asList( + OptimizelyDecideOption.INCLUDE_REASONS, + OptimizelyDecideOption.DISABLE_DECISION_EVENT + )); + assertEquals(allFlagKeys.size(), decisions.size()); + + String holdoutExperimentId = "1007543323427"; // holdout_included_flags id + String variationId = "$opt_dummy_variation_id"; + String variationKey = "ho_off_key"; + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags)."; + + int holdoutCount = 0; + for (String flagKey : allFlagKeys) { + OptimizelyDecision d = decisions.get(flagKey); + assertNotNull("Missing decision for flag " + flagKey, d); + if (includedInHoldout.contains(flagKey)) { + // Should be holdout decision + assertEquals(variationKey, d.getVariationKey()); + assertFalse(d.getEnabled()); + assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("holdout_included_flags") + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + holdoutCount++; + } else { + // Should NOT be a holdout decision + assertFalse("Non-included flag should not have holdout reason: " + flagKey, d.getReasons().contains(expectedReason)); + } + } + assertEquals("Expected exactly the included flags to be in holdout", includedInHoldout.size(), holdoutCount); + logbackVerifier.expectMessage(Level.INFO, expectedReason); + } } From 2e98731e7b13c11de41d92678a497168e63c0a41 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Sat, 9 Aug 2025 00:30:19 +0600 Subject: [PATCH 11/17] fix: update eventHandler call to include 'nationality' for impression metadata in OptimizelyUserContextTest --- .../test/java/com/optimizely/ab/OptimizelyUserContextTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 4cb4ebc0d..234429f88 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -2109,6 +2109,7 @@ public void decide_with_holdout() throws Exception { String userId = "user123"; Map attrs = new HashMap<>(); attrs.put("$opt_bucketing_id", "ppid160000"); + attrs.put("nationality", "English"); OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); // include reasons to surface holdout logs in decision reasons if implemented @@ -2137,7 +2138,7 @@ public void decide_with_holdout() throws Exception { .setVariationKey("ho_off_key") .setEnabled(false) .build(); - eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); + eventHandler.expectImpression(experimentId, variationId, userId, Collections.singletonMap("nationality", "English"), metadata); } @Test From 62b24a9bf087d85a9c730710c68e05d11c498b6f Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Mon, 11 Aug 2025 11:35:03 +0600 Subject: [PATCH 12/17] Add decision notification test case for ho --- .../ab/OptimizelyUserContextTest.java | 1671 ++--------------- 1 file changed, 181 insertions(+), 1490 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 234429f88..41d52d56d 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -16,51 +16,57 @@ */ package com.optimizely.ab; -import ch.qos.logback.classic.Level; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + import com.google.common.base.Charsets; import com.google.common.io.Resources; import com.optimizely.ab.bucketing.FeatureDecision; import com.optimizely.ab.bucketing.UserProfile; import com.optimizely.ab.bucketing.UserProfileService; import com.optimizely.ab.bucketing.UserProfileUtils; -import com.optimizely.ab.config.*; -import com.optimizely.ab.config.parser.ConfigParseException; -import com.optimizely.ab.event.EventHandler; +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.FeatureFlag; +import com.optimizely.ab.config.Group; +import com.optimizely.ab.config.ProjectConfig; +import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; import com.optimizely.ab.event.EventProcessor; import com.optimizely.ab.event.ForwardingEventProcessor; import com.optimizely.ab.event.internal.ImpressionEvent; import com.optimizely.ab.event.internal.payload.DecisionMetadata; import com.optimizely.ab.internal.LogbackVerifier; +import static com.optimizely.ab.notification.DecisionNotification.ExperimentDecisionNotificationBuilder.VARIATION_KEY; +import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.DECISION_EVENT_DISPATCHED; +import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.ENABLED; +import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.EXPERIMENT_ID; +import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.FLAG_KEY; +import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.REASONS; +import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.RULE_KEY; +import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.VARIABLES; +import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.VARIATION_ID; import com.optimizely.ab.notification.NotificationCenter; -import com.optimizely.ab.odp.*; -import com.optimizely.ab.optimizelydecision.DecisionMessage; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import com.optimizely.ab.optimizelydecision.OptimizelyDecision; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; -import junit.framework.TestCase; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.*; -import java.util.concurrent.CountDownLatch; - -import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; -import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; -import static com.optimizely.ab.notification.DecisionNotification.ExperimentDecisionNotificationBuilder.VARIATION_KEY; -import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.*; +import ch.qos.logback.classic.Level; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; -import static org.junit.Assert.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.*; public class OptimizelyUserContextTest { @Rule @@ -758,1496 +764,181 @@ public void decisionNotification() { assertTrue(isListenerCalled); } - // options - @Test - public void decideOptions_bypassUPS() throws Exception { - String flagKey = "feature_2"; // embedding experiment: "exp_no_audience" - String experimentId = "10420810910"; // "exp_no_audience" - String variationId = "10418551353"; - String variationId2 = "10418510624"; - String variationKey1 = "variation_with_traffic"; - String variationKey2 = "variation_no_traffic"; - - UserProfileService ups = mock(UserProfileService.class); - when(ups.lookup(userId)).thenReturn(createUserProfileMap(experimentId, variationId2)); + public void decide_for_keys_with_holdout() throws Exception { + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String userId = "user123"; + Map attrs = new HashMap<>(); + attrs.put("$opt_bucketing_id", "ppid160000"); + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); - optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withUserProfileService(ups) - .build(); + List flagKeys = Arrays.asList( + "boolean_feature", // previously validated basic_holdout membership + "double_single_variable_feature", // also subject to global/basic holdout + "integer_single_variable_feature" // also subject to global/basic holdout + ); - OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey); - // should return variationId2 set by UPS - assertEquals(decision.getVariationKey(), variationKey2); - - decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)); - // should ignore variationId2 set by UPS and return variationId1 - assertEquals(decision.getVariationKey(), variationKey1); - // also should not save either - verify(ups, never()).save(anyObject()); - } + Map decisions = user.decideForKeys(flagKeys, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); + assertEquals(3, decisions.size()); - @Test - public void decideOptions_excludeVariables() { - String flagKey = "feature_1"; - OptimizelyUserContext user = optimizely.createUserContext(userId); + String holdoutExperimentId = "10075323428"; // basic_holdout id + String variationId = "$opt_dummy_variation_id"; + String variationKey = "ho_off_key"; + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (basic_holdout)."; - OptimizelyDecision decision = user.decide(flagKey); - assertTrue(decision.getVariables().toMap().size() > 0); + for (String flagKey : flagKeys) { + OptimizelyDecision d = decisions.get(flagKey); + assertNotNull(d); + assertEquals(flagKey, d.getFlagKey()); + assertEquals(variationKey, d.getVariationKey()); + assertFalse(d.getEnabled()); + assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("basic_holdout") + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + // attributes map expected empty (reserved $opt_ attribute filtered out) + eventHandler.expectImpression(holdoutExperimentId, variationId, userId, Collections.emptyMap(), metadata); + } - decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.EXCLUDE_VARIABLES)); - assertTrue(decision.getVariables().toMap().size() == 0); + // At least one log message confirming holdout membership + logbackVerifier.expectMessage(Level.INFO, expectedReason); } @Test - public void decideOptions_includeReasons() { - OptimizelyUserContext user = optimizely.createUserContext(userId); - - String flagKey = "invalid_key"; - OptimizelyDecision decision = user.decide(flagKey); - assertEquals(decision.getReasons().size(), 1); - TestCase.assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); - - decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); - - flagKey = "feature_1"; - decision = user.decide(flagKey); - assertEquals(decision.getReasons().size(), 0); - - decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - assertTrue(decision.getReasons().size() > 0); - } - - public void decideOptions_disableDispatchEvent() { - // tested already with decide_doNotSendEvent() above - } + public void decide_all_with_holdout() throws Exception { - public void decideOptions_enabledFlagsOnly() { - // tested already with decideAll_allFlags_enabledFlagsOnly() above - } + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String userId = "user123"; + Map attrs = new HashMap<>(); + // ppid120000 buckets user into holdout_included_flags + attrs.put("$opt_bucketing_id", "ppid120000"); + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); - @Test - public void decideOptions_defaultDecideOptions() { - List options = Arrays.asList( - OptimizelyDecideOption.EXCLUDE_VARIABLES + // All flag keys present in holdouts-project-config.json + List allFlagKeys = Arrays.asList( + "boolean_feature", + "double_single_variable_feature", + "integer_single_variable_feature", + "boolean_single_variable_feature", + "string_single_variable_feature", + "multi_variate_feature", + "multi_variate_future_feature", + "mutex_group_feature" ); - optimizely = Optimizely.builder() - .withDatafile(datafile) - .withDefaultDecideOptions(options) - .build(); + // Flags INCLUDED in holdout_included_flags (only these should be holdout decisions) + List includedInHoldout = Arrays.asList( + "boolean_feature", + "double_single_variable_feature", + "integer_single_variable_feature" + ); - String flagKey = "feature_1"; - OptimizelyUserContext user = optimizely.createUserContext(userId); + Map decisions = user.decideAll(Arrays.asList( + OptimizelyDecideOption.INCLUDE_REASONS, + OptimizelyDecideOption.DISABLE_DECISION_EVENT + )); + assertEquals(allFlagKeys.size(), decisions.size()); - // should be excluded by DefaultDecideOption - OptimizelyDecision decision = user.decide(flagKey); - assertTrue(decision.getVariables().toMap().size() == 0); + String holdoutExperimentId = "1007543323427"; // holdout_included_flags id + String variationId = "$opt_dummy_variation_id"; + String variationKey = "ho_off_key"; + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags)."; - decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS, OptimizelyDecideOption.EXCLUDE_VARIABLES)); - // other options should work as well - assertTrue(decision.getReasons().size() > 0); - // redundant setting ignored - assertTrue(decision.getVariables().toMap().size() == 0); + int holdoutCount = 0; + for (String flagKey : allFlagKeys) { + OptimizelyDecision d = decisions.get(flagKey); + assertNotNull("Missing decision for flag " + flagKey, d); + if (includedInHoldout.contains(flagKey)) { + // Should be holdout decision + assertEquals(variationKey, d.getVariationKey()); + assertFalse(d.getEnabled()); + assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("holdout_included_flags") + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + holdoutCount++; + } else { + // Should NOT be a holdout decision + assertFalse("Non-included flag should not have holdout reason: " + flagKey, d.getReasons().contains(expectedReason)); + } + } + assertEquals("Expected exactly the included flags to be in holdout", includedInHoldout.size(), holdoutCount); + logbackVerifier.expectMessage(Level.INFO, expectedReason); } - // errors - @Test - public void decide_sdkNotReady() { - String flagKey = "feature_1"; - - Optimizely optimizely = new Optimizely.Builder().build(); - OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey); + public void decisionNotification_with_holdout() throws Exception { + // Use holdouts datafile + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String flagKey = "boolean_feature"; + String userId = "user123"; + String ruleKey = "basic_holdout"; // holdout rule key + String variationKey = "ho_off_key"; // holdout (off) variation key + String experimentId = "10075323428"; // holdout experiment id in holdouts-project-config.json + String variationId = "$opt_dummy_variation_id";// dummy variation id used for holdout impressions + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (" + ruleKey + ")."; - assertNull(decision.getVariationKey()); - assertFalse(decision.getEnabled()); - assertTrue(decision.getVariables().isEmpty()); - assertEquals(decision.getFlagKey(), flagKey); - assertEquals(decision.getUserContext(), user); + Map attrs = new HashMap<>(); + attrs.put("$opt_bucketing_id", "ppid160000"); // deterministic bucketing into basic_holdout + attrs.put("nationality", "English"); // non-reserved attribute should appear in impression & notification - assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), DecisionMessage.SDK_NOT_READY.reason()); - } + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); - @Test - public void decide_invalidFeatureKey() { - String flagKey = "invalid_key"; + // Register notification handler similar to decisionNotification test + isListenerCalled = false; + optWithHoldout.addDecisionNotificationHandler(decisionNotification -> { + Assert.assertEquals(NotificationCenter.DecisionNotificationType.FLAG.toString(), decisionNotification.getType()); + Assert.assertEquals(userId, decisionNotification.getUserId()); + + Assert.assertEquals(attrs, decisionNotification.getAttributes()); + + Map info = decisionNotification.getDecisionInfo(); + Assert.assertEquals(flagKey, info.get(FLAG_KEY)); + Assert.assertEquals(variationKey, info.get(VARIATION_KEY)); + Assert.assertEquals(false, info.get(ENABLED)); + Assert.assertEquals(ruleKey, info.get(RULE_KEY)); + Assert.assertEquals(experimentId, info.get(EXPERIMENT_ID)); + Assert.assertEquals(variationId, info.get(VARIATION_ID)); + // Variables should be empty because feature is disabled by holdout + Assert.assertTrue(((Map) info.get(VARIABLES)).isEmpty()); + // Event should be dispatched (no DISABLE_DECISION_EVENT option) + Assert.assertEquals(true, info.get(DECISION_EVENT_DISPATCHED)); + + @SuppressWarnings("unchecked") + List reasons = (List) info.get(REASONS); + Assert.assertTrue("Expected holdout reason present", reasons.contains(expectedReason)); + isListenerCalled = true; + }); - OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey); + // Execute decision with INCLUDE_REASONS so holdout reason is present + OptimizelyDecision decision = user.decide(flagKey, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); + assertTrue(isListenerCalled); - assertNull(decision.getVariationKey()); + // Sanity checks on returned decision + assertEquals(variationKey, decision.getVariationKey()); assertFalse(decision.getEnabled()); - assertTrue(decision.getVariables().isEmpty()); - assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); - } - - @Test - public void decideAll_sdkNotReady() { - List flagKeys = Arrays.asList("feature_1"); - - Optimizely optimizely = new Optimizely.Builder().build(); - OptimizelyUserContext user = optimizely.createUserContext(userId); - Map decisions = user.decideForKeys(flagKeys); - - assertEquals(decisions.size(), 0); - } - - @Test - public void decideAll_errorDecisionIncluded() { - String flagKey1 = "feature_2"; - String flagKey2 = "invalid_key"; - - List flagKeys = Arrays.asList(flagKey1, flagKey2); - OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); - - OptimizelyUserContext user = optimizely.createUserContext(userId); - Map decisions = user.decideForKeys(flagKeys); - - assertEquals(decisions.size(), 2); - - assertEquals( - decisions.get(flagKey1), - new OptimizelyDecision( - "variation_with_traffic", - true, - variablesExpected1, - "exp_no_audience", - flagKey1, - user, - Collections.emptyList())); - assertEquals( - decisions.get(flagKey2), - OptimizelyDecision.newErrorDecision( - flagKey2, - user, - DecisionMessage.FLAG_KEY_INVALID.reason(flagKey2))); - } - - // reasons (errors) - - @Test - public void decideReasons_sdkNotReady() { - String flagKey = "feature_1"; - - Optimizely optimizely = new Optimizely.Builder().build(); - OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey); - - assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), DecisionMessage.SDK_NOT_READY.reason()); - } - - @Test - public void decideReasons_featureKeyInvalid() { - String flagKey = "invalid_key"; - - OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey); - - assertEquals(decision.getReasons().size(), 1); - assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); - } - - @Test - public void decideReasons_variableValueInvalid() { - String flagKey = "feature_1"; - - FeatureFlag flag = getSpyFeatureFlag(flagKey); - List variables = Arrays.asList(new FeatureVariable("any-id", "any-key", "invalid", null, "integer", null)); - when(flag.getVariables()).thenReturn(variables); - addSpyFeatureFlag(flag); - - OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey); - - assertEquals(decision.getReasons().get(0), DecisionMessage.VARIABLE_VALUE_INVALID.reason("any-key")); - } - - // reasons (infos with includeReasons) - - @Test - public void decideReasons_experimentNotRunning() { - String flagKey = "feature_1"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.isActive()).thenReturn(false); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); - - assertTrue(decision.getReasons().contains( - String.format("Experiment \"exp_with_audience\" is not running.") - )); - } - - @Test - public void decideReasons_gotVariationFromUserProfile() throws Exception { - String flagKey = "feature_2"; // embedding experiment: "exp_no_audience" - String experimentId = "10420810910"; // "exp_no_audience" - String experimentKey = "exp_no_audience"; - String variationId2 = "10418510624"; - String variationKey2 = "variation_no_traffic"; - - UserProfileService ups = mock(UserProfileService.class); - when(ups.lookup(userId)).thenReturn(createUserProfileMap(experimentId, variationId2)); - - optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withUserProfileService(ups) - .build(); + assertTrue(decision.getReasons().contains(expectedReason)); - OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + // Impression expectation (nationality only) + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey(ruleKey) + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + eventHandler.expectImpression(experimentId, variationId, userId, Collections.singletonMap("nationality", "English"), metadata); - assertTrue(decision.getReasons().contains( - String.format("Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", variationKey2, experimentKey, userId) - )); + // Log expectation (reuse existing pattern) + logbackVerifier.expectMessage(Level.INFO, expectedReason); } - @Test - public void decideReasons_forcedVariationFound() { - String flagKey = "feature_1"; - String variationKey = "b"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getUserIdToVariationKeyMap()).thenReturn(Collections.singletonMap(userId, variationKey)); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); - - assertTrue(decision.getReasons().contains( - String.format("User \"%s\" is forced in variation \"%s\".", userId, variationKey) - )); - } - - @Test - public void decideReasons_forcedVariationFoundButInvalid() { - String flagKey = "feature_1"; - String variationKey = "invalid-key"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getUserIdToVariationKeyMap()).thenReturn(Collections.singletonMap(userId, variationKey)); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); - - assertTrue(decision.getReasons().contains( - String.format("Variation \"%s\" is not in the datafile. Not activating user \"%s\".", variationKey, userId) - )); - } - - @Test - public void decideReasons_userMeetsConditionsForTargetingRule() { - String flagKey = "feature_1"; - - OptimizelyUserContext user = optimizely.createUserContext(userId); - user.setAttribute("country", "US"); - OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(decision.getReasons().contains( - String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) - )); - } - - @Test - public void decideReasons_userDoesntMeetConditionsForTargetingRule() { - String flagKey = "feature_1"; - - OptimizelyUserContext user = optimizely.createUserContext(userId); - user.setAttribute("country", "CA"); - OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(decision.getReasons().contains( - String.format("User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, 1) - )); - } - - @Test - public void decideReasons_userBucketedIntoTargetingRule() { - String flagKey = "feature_1"; - - OptimizelyUserContext user = optimizely.createUserContext(userId); - user.setAttribute("country", "US"); - OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(decision.getReasons().contains( - String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) - )); - } - - @Test - public void decideReasons_userBucketedIntoEveryoneTargetingRule() { - String flagKey = "feature_1"; - - OptimizelyUserContext user = optimizely.createUserContext(userId); - user.setAttribute("country", "KO"); - OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(decision.getReasons().contains( - String.format("User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId) - )); - } - - @Test - public void decideReasons_userNotBucketedIntoTargetingRule() { - String flagKey = "feature_1"; - String experimentKey = "3332020494"; // experimentKey of rollout[2] - - OptimizelyUserContext user = optimizely.createUserContext(userId); - user.setAttribute("browser", "safari"); - OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", userId, experimentKey) - )); - } - - @Test - public void decideReasons_userBucketedIntoVariationInExperiment() { - String flagKey = "feature_2"; - String experimentKey = "exp_no_audience"; - String variationKey = "variation_with_traffic"; - - OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", userId, variationKey, experimentKey) - )); - } - - @Test - public void decideReasons_userNotBucketedIntoVariation() { - String flagKey = "feature_2"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getTrafficAllocation()).thenReturn(Arrays.asList(new TrafficAllocation("any-id", 0))); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); - - assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"%s\" is not in any variation of experiment \"exp_no_audience\".", userId) - )); - } - - @Test - public void decideReasons_userBucketedIntoExperimentInGroup() { - String flagKey = "feature_3"; - String experimentId = "10390965532"; // "group_exp_1" - - FeatureFlag flag = getSpyFeatureFlag(flagKey); - when(flag.getExperimentIds()).thenReturn(Arrays.asList(experimentId)); - addSpyFeatureFlag(flag); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); - - assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"tester\" is in experiment \"group_exp_1\" of group 13142870430.") - )); - } - - @Test - public void decideReasons_userNotBucketedIntoExperimentInGroup() { - String flagKey = "feature_3"; - String experimentId = "10420843432"; // "group_exp_2" - - FeatureFlag flag = getSpyFeatureFlag(flagKey); - when(flag.getExperimentIds()).thenReturn(Arrays.asList(experimentId)); - addSpyFeatureFlag(flag); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); - - assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"tester\" is not in experiment \"group_exp_2\" of group 13142870430.") - )); - } - - @Test - public void decideReasons_userNotBucketedIntoAnyExperimentInGroup() { - String flagKey = "feature_3"; - String experimentId = "10390965532"; // "group_exp_1" - String groupId = "13142870430"; - - FeatureFlag flag = getSpyFeatureFlag(flagKey); - when(flag.getExperimentIds()).thenReturn(Arrays.asList(experimentId)); - addSpyFeatureFlag(flag); - - Group group = getSpyGroup(groupId); - when(group.getTrafficAllocation()).thenReturn(Collections.emptyList()); - addSpyGroup(group); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); - - assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"tester\" is not in any experiment of group 13142870430.") - )); - } - - @Test - public void decideReasons_userNotInExperiment() { - String flagKey = "feature_1"; - String experimentKey = "exp_with_audience"; - - OptimizelyUserContext user = optimizely.createUserContext(userId); - OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(decision.getReasons().contains( - String.format("User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experimentKey) - )); - } - - @Test - public void decideReasons_conditionNoMatchingAudience() throws ConfigParseException { - String flagKey = "feature_1"; - String audienceId = "invalid_id"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); - - assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) - )); - } - - @Test - public void decideReasons_evaluateAttributeInvalidType() { - String flagKey = "feature_1"; - String audienceId = "13389130056"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("country", 25)); - - assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) - )); - } - - @Test - public void decideReasons_evaluateAttributeValueOutOfRange() { - String flagKey = "feature_1"; - String audienceId = "age_18"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", (float)Math.pow(2, 54))); - - assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) - )); - } - - @Test - public void decideReasons_userAttributeInvalidType() { - String flagKey = "feature_1"; - String audienceId = "invalid_type"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); - - assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) - )); - } - - @Test - public void decideReasons_userAttributeInvalidMatch() { - String flagKey = "feature_1"; - String audienceId = "invalid_match"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); - - assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) - )); - } - - @Test - public void decideReasons_userAttributeNilValue() { - String flagKey = "feature_1"; - String audienceId = "nil_value"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); - - assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) - )); - } - - @Test - public void decideReasons_missingAttributeValue() { - String flagKey = "feature_1"; - String audienceId = "age_18"; - - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); - addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); - - assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) - )); - } - - @Test - public void setForcedDecisionWithRuleKeyTest() { - String flagKey = "55555"; - String ruleKey = "77777"; - String variationKey = "33333"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - String foundVariationKey = optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey(); - assertEquals(variationKey, foundVariationKey); - } - - @Test - public void setForcedDecisionsWithRuleKeyTest() { - String flagKey = "feature_2"; - String ruleKey = "exp_no_audience"; - String ruleKey2 = "88888"; - String variationKey = "33333"; - String variationKey2 = "variation_with_traffic"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - OptimizelyDecisionContext optimizelyDecisionContext2 = new OptimizelyDecisionContext(flagKey, ruleKey2); - OptimizelyForcedDecision optimizelyForcedDecision2 = new OptimizelyForcedDecision(variationKey2); - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext2, optimizelyForcedDecision2); - assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); - assertEquals(variationKey2, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext2).getVariationKey()); - - // Update first forcedDecision - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision2); - assertEquals(variationKey2, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); - - // Test to confirm decide uses proper FD - OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(decision.getReasons().contains( - String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey2, flagKey, ruleKey, userId) - )); - } - - @Test - public void setForcedDecisionWithoutRuleKeyTest() { - String flagKey = "55555"; - String variationKey = "33333"; - String updatedVariationKey = "55555"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - OptimizelyForcedDecision updatedOptimizelyForcedDecision = new OptimizelyForcedDecision(updatedVariationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); - - // Update forcedDecision - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, updatedOptimizelyForcedDecision); - assertEquals(updatedVariationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); - } - - - @Test - public void getForcedVariationWithRuleKey() { - String flagKey = "55555"; - String ruleKey = "77777"; - String variationKey = "33333"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); - } - - @Test - public void failedGetForcedDecisionWithRuleKey() { - String flagKey = "55555"; - String invalidFlagKey = "11"; - String ruleKey = "77777"; - String variationKey = "33333"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyDecisionContext invalidOptimizelyDecisionContext = new OptimizelyDecisionContext(invalidFlagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertNull(optimizelyUserContext.getForcedDecision(invalidOptimizelyDecisionContext)); - } - - @Test - public void getForcedVariationWithoutRuleKey() { - String flagKey = "55555"; - String variationKey = "33333"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); - } - - - @Test - public void failedGetForcedDecisionWithoutRuleKey() { - String flagKey = "55555"; - String invalidFlagKey = "11"; - String variationKey = "33333"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); - OptimizelyDecisionContext invalidOptimizelyDecisionContext = new OptimizelyDecisionContext(invalidFlagKey, null); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertNull(optimizelyUserContext.getForcedDecision(invalidOptimizelyDecisionContext)); - } - - @Test - public void removeForcedDecisionWithRuleKey() { - String flagKey = "55555"; - String ruleKey = "77777"; - String variationKey = "33333"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertTrue(optimizelyUserContext.removeForcedDecision(optimizelyDecisionContext)); - } - - @Test - public void removeForcedDecisionWithoutRuleKey() { - String flagKey = "55555"; - String variationKey = "33333"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertTrue(optimizelyUserContext.removeForcedDecision(optimizelyDecisionContext)); - } - - @Test - public void removeForcedDecisionWithNullRuleKeyAfterAddingWithRuleKey() { - String flagKey = "flag2"; - String ruleKey = "default-rollout-3045-20390585493"; - String variationKey = "variation2"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); - OptimizelyDecisionContext optimizelyDecisionContextNonNull = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertFalse(optimizelyUserContext.removeForcedDecision(optimizelyDecisionContextNonNull)); - } - - @Test - public void removeForcedDecisionWithIncorrectFlagKey() { - String flagKey = "55555"; - String variationKey = "variation2"; - String incorrectFlagKey = "flag1"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); - OptimizelyDecisionContext incorrectOptimizelyDecisionContext = new OptimizelyDecisionContext(incorrectFlagKey, null); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertFalse(optimizelyUserContext.removeForcedDecision(incorrectOptimizelyDecisionContext)); - } - - - @Test - public void removeForcedDecisionWithIncorrectFlagKeyButSimilarRuleKey() { - String flagKey = "flag2"; - String incorrectFlagKey = "flag3"; - String ruleKey = "default-rollout-3045-20390585493"; - String variationKey = "variation2"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyDecisionContext similarOptimizelyDecisionContext = new OptimizelyDecisionContext(incorrectFlagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertFalse(optimizelyUserContext.removeForcedDecision(similarOptimizelyDecisionContext)); - } - - @Test - public void removeAllForcedDecisions() { - String flagKey = "55555"; - String ruleKey = "77777"; - String variationKey = "33333"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertTrue(optimizelyUserContext.removeAllForcedDecisions()); - } - - @Test - public void setForcedDecisionsAndCallDecide() { - String flagKey = "feature_2"; - String ruleKey = "exp_no_audience"; - String variationKey = "variation_with_traffic"; - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); - - // Test to confirm decide uses proper FD - OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertNotNull(decision); - assertTrue(decision.getReasons().contains( - String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) - )); - } - /******************************************[START DECIDE TESTS WITH FDs]******************************************/ - @Test - public void setForcedDecisionsAndCallDecideFlagToDecision() { - String flagKey = "feature_1"; - String variationKey = "a"; - - optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); - - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); - - optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); - - isListenerCalled = false; - - // Test to confirm decide uses proper FD - OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(isListenerCalled); - - String variationId = "10389729780"; - String experimentId = ""; - - - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("") - .setRuleType("feature-test") - .setVariationKey(variationKey) - .setEnabled(true) - .build(); - - eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); - - assertNotNull(decision); - assertTrue(decision.getReasons().contains( - String.format("Variation (%s) is mapped to flag (%s) and user (%s) in the forced decision map.", variationKey, flagKey, userId) - )); - } - @Test - public void setForcedDecisionsAndCallDecideExperimentRuleToDecision() { - String flagKey = "feature_1"; - String ruleKey = "exp_with_audience"; - String variationKey = "a"; - - optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); - - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); - - optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); - - isListenerCalled = false; - - // Test to confirm decide uses proper FD - OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(isListenerCalled); - - String variationId = "10389729780"; - String experimentId = "10390977673"; - - - eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap()); - - assertNotNull(decision); - assertTrue(decision.getReasons().contains( - String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) - )); - } - - @Test - public void setForcedDecisionsAndCallDecideDeliveryRuleToDecision() { - String flagKey = "feature_1"; - String ruleKey = "3332020515"; - String variationKey = "3324490633"; - - optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); - - OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); - - OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); - OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); - optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); - assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); - - optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); - - isListenerCalled = false; - - // Test to confirm decide uses proper FD - OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertTrue(isListenerCalled); - - String variationId = "3324490633"; - String experimentId = "3332020515"; - - - eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap()); - - assertNotNull(decision); - assertTrue(decision.getReasons().contains( - String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) - )); - } - /********************************************[END DECIDE TESTS WITH FDs]******************************************/ - - @Test - public void fetchQualifiedSegments() { - ODPEventManager mockODPEventManager = mock(ODPEventManager.class); - ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); - ODPManager mockODPManager = mock(ODPManager.class); - - Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); - Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); - - Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .withODPManager(mockODPManager) - .build(); - - OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); - - assertTrue(userContext.fetchQualifiedSegments()); - verify(mockODPSegmentManager).getQualifiedSegments("test-user", Collections.emptyList()); - - assertTrue(userContext.fetchQualifiedSegments(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); - verify(mockODPSegmentManager).getQualifiedSegments("test-user", Collections.singletonList(ODPSegmentOption.RESET_CACHE)); - } - - @Test - public void fetchQualifiedSegmentsErrorWhenConfigIsInvalid() { - ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class); - Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(null); - ODPEventManager mockODPEventManager = mock(ODPEventManager.class); - ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); - ODPManager mockODPManager = mock(ODPManager.class); - - Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); - Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); - - Optimizely optimizely = Optimizely.builder() - .withConfigManager(mockProjectConfigManager) - .withODPManager(mockODPManager) - .build(); - - OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); - - assertFalse(userContext.fetchQualifiedSegments()); - logbackVerifier.expectMessage(Level.ERROR, "Optimizely instance is not valid, failing fetchQualifiedSegments call."); - } - - @Test - public void fetchQualifiedSegmentsError() { - Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); - OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); - - assertFalse(userContext.fetchQualifiedSegments()); - logbackVerifier.expectMessage(Level.ERROR, "Audience segments fetch failed (ODP is not enabled)."); - } - - @Test - public void fetchQualifiedSegmentsAsync() throws InterruptedException { - ODPEventManager mockODPEventManager = mock(ODPEventManager.class); - ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); - ODPManager mockODPManager = mock(ODPManager.class); - - doAnswer( - invocation -> { - ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(1, ODPSegmentManager.ODPSegmentFetchCallback.class); - callback.onCompleted(Arrays.asList("segment1", "segment2")); - return null; - } - ).when(mockODPSegmentManager).getQualifiedSegments(any(), (ODPSegmentManager.ODPSegmentFetchCallback) any(), any()); - Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); - Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); - - Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .withODPManager(mockODPManager) - .build(); - - - - OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); - - CountDownLatch countDownLatch = new CountDownLatch(1); - userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { - assertTrue(isFetchSuccessful); - countDownLatch.countDown(); - }); - - countDownLatch.await(); - verify(mockODPSegmentManager).getQualifiedSegments(eq("test-user"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.emptyList())); - assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); - - // reset qualified segments - userContext.setQualifiedSegments(Collections.emptyList()); - CountDownLatch countDownLatch2 = new CountDownLatch(1); - userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { - assertTrue(isFetchSuccessful); - countDownLatch2.countDown(); - }, Collections.singletonList(ODPSegmentOption.RESET_CACHE)); - - countDownLatch2.await(); - verify(mockODPSegmentManager).getQualifiedSegments(eq("test-user"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); - assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); - } - - @Test - public void fetchQualifiedSegmentsAsyncWithVUID() throws InterruptedException { - ODPEventManager mockODPEventManager = mock(ODPEventManager.class); - ODPApiManager mockAPIManager = mock(ODPApiManager.class); - ODPSegmentManager mockODPSegmentManager = spy(new ODPSegmentManager(mockAPIManager)); - ODPManager mockODPManager = mock(ODPManager.class); - - doAnswer( - invocation -> { - ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(2, ODPSegmentManager.ODPSegmentFetchCallback.class); - callback.onCompleted(Arrays.asList("segment1", "segment2")); - return null; - } - ).when(mockODPSegmentManager).getQualifiedSegments(any(), eq("vuid_f6db3d60ba3a493d8e41bc995bb"), (ODPSegmentManager.ODPSegmentFetchCallback) any(), any()); - Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); - Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); - - Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .withODPManager(mockODPManager) - .build(); - - OptimizelyUserContext userContext = optimizely.createUserContext("vuid_f6db3d60ba3a493d8e41bc995bb"); - - CountDownLatch countDownLatch = new CountDownLatch(1); - userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { - assertTrue(isFetchSuccessful); - countDownLatch.countDown(); - }); - - countDownLatch.await(); - verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.VUID), eq("vuid_f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.emptyList())); - assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); - - // reset qualified segments - userContext.setQualifiedSegments(Collections.emptyList()); - CountDownLatch countDownLatch2 = new CountDownLatch(1); - userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { - assertTrue(isFetchSuccessful); - countDownLatch2.countDown(); - }, Collections.singletonList(ODPSegmentOption.RESET_CACHE)); - - countDownLatch2.await(); - verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.VUID) ,eq("vuid_f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); - assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); - } - - - @Test - public void fetchQualifiedSegmentsAsyncWithUserID() throws InterruptedException { - ODPEventManager mockODPEventManager = mock(ODPEventManager.class); - ODPApiManager mockAPIManager = mock(ODPApiManager.class); - ODPSegmentManager mockODPSegmentManager = spy(new ODPSegmentManager(mockAPIManager)); - ODPManager mockODPManager = mock(ODPManager.class); - - doAnswer( - invocation -> { - ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(2, ODPSegmentManager.ODPSegmentFetchCallback.class); - callback.onCompleted(Arrays.asList("segment1", "segment2")); - return null; - } - ).when(mockODPSegmentManager).getQualifiedSegments(any(), eq("f6db3d60ba3a493d8e41bc995bb"), (ODPSegmentManager.ODPSegmentFetchCallback) any(), any()); - Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); - Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); - - Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .withODPManager(mockODPManager) - .build(); - - OptimizelyUserContext userContext = optimizely.createUserContext("f6db3d60ba3a493d8e41bc995bb"); - - CountDownLatch countDownLatch = new CountDownLatch(1); - userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { - assertTrue(isFetchSuccessful); - countDownLatch.countDown(); - }); - - countDownLatch.await(); - verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.FS_USER_ID), eq("f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.emptyList())); - assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); - - // reset qualified segments - userContext.setQualifiedSegments(Collections.emptyList()); - CountDownLatch countDownLatch2 = new CountDownLatch(1); - userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { - assertTrue(isFetchSuccessful); - countDownLatch2.countDown(); - }, Collections.singletonList(ODPSegmentOption.RESET_CACHE)); - - countDownLatch2.await(); - verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.FS_USER_ID) ,eq("f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); - assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); - } - - @Test - public void fetchQualifiedSegmentsAsyncError() throws InterruptedException { - Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); - - OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); - - CountDownLatch countDownLatch = new CountDownLatch(1); - userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { - assertFalse(isFetchSuccessful); - countDownLatch.countDown(); - }); - - countDownLatch.await(); - assertEquals(null, userContext.getQualifiedSegments()); - logbackVerifier.expectMessage(Level.ERROR, "Audience segments fetch failed (ODP is not enabled)."); - } - - @Test - public void fetchQualifiedSegmentsAsyncErrorWhenConfigIsInvalid() throws InterruptedException { - ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class); - Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(null); - ODPEventManager mockODPEventManager = mock(ODPEventManager.class); - ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); - ODPManager mockODPManager = mock(ODPManager.class); - - Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); - Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); - - Optimizely optimizely = Optimizely.builder() - .withConfigManager(mockProjectConfigManager) - .withODPManager(mockODPManager) - .build(); - - OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); - - CountDownLatch countDownLatch = new CountDownLatch(1); - userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { - assertFalse(isFetchSuccessful); - countDownLatch.countDown(); - }); - - countDownLatch.await(); - assertEquals(null, userContext.getQualifiedSegments()); - logbackVerifier.expectMessage(Level.ERROR, "Optimizely instance is not valid, failing fetchQualifiedSegments call."); - } - - @Test - public void identifyUserErrorWhenConfigIsInvalid() { - ODPEventManager mockODPEventManager = mock(ODPEventManager.class); - ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); - ODPManager mockODPManager = mock(ODPManager.class); - ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class); - Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(null); - Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); - Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); - - Optimizely optimizely = Optimizely.builder() - .withConfigManager(mockProjectConfigManager) - .withODPManager(mockODPManager) - .build(); - - optimizely.createUserContext("test-user"); - verify(mockODPEventManager, never()).identifyUser("test-user"); - Mockito.reset(mockODPEventManager); - - logbackVerifier.expectMessage(Level.ERROR, "Optimizely instance is not valid, failing identifyUser call."); - } - - @Test - public void identifyUser() { - ODPEventManager mockODPEventManager = mock(ODPEventManager.class); - ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); - ODPManager mockODPManager = mock(ODPManager.class); - - Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); - Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); - - Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .withODPManager(mockODPManager) - .build(); - - OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); - verify(mockODPEventManager).identifyUser("test-user"); - - Mockito.reset(mockODPEventManager); - OptimizelyUserContext userContextClone = userContext.copy(); - - // identifyUser should not be called the new userContext is created through copy - verify(mockODPEventManager, never()).identifyUser("test-user"); - - assertNotSame(userContextClone, userContext); - } - - // utils - - Map createUserProfileMap(String experimentId, String variationId) { - Map userProfileMap = new HashMap(); - userProfileMap.put(UserProfileService.userIdKey, userId); - - Map decisionMap = new HashMap(1); - decisionMap.put(UserProfileService.variationIdKey, variationId); - - Map> decisionsMap = new HashMap>(); - decisionsMap.put(experimentId, decisionMap); - userProfileMap.put(UserProfileService.experimentBucketMapKey, decisionsMap); - - return userProfileMap; - } - - void setAudienceForFeatureTest(String flagKey, String audienceId) throws ConfigParseException { - Experiment experiment = getSpyExperiment(flagKey); - when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); - addSpyExperiment(experiment); - } - - Experiment getSpyExperiment(String flagKey) { - setMockConfig(); - String experimentId = config.getFeatureKeyMapping().get(flagKey).getExperimentIds().get(0); - return spy(experimentIdMapping.get(experimentId)); - } - - FeatureFlag getSpyFeatureFlag(String flagKey) { - setMockConfig(); - return spy(config.getFeatureKeyMapping().get(flagKey)); - } - - Group getSpyGroup(String groupId) { - setMockConfig(); - return spy(groupIdMapping.get(groupId)); - } - - void addSpyExperiment(Experiment experiment) { - experimentIdMapping.put(experiment.getId(), experiment); - when(config.getExperimentIdMapping()).thenReturn(experimentIdMapping); - } - - void addSpyFeatureFlag(FeatureFlag flag) { - featureKeyMapping.put(flag.getKey(), flag); - when(config.getFeatureKeyMapping()).thenReturn(featureKeyMapping); - } - - void addSpyGroup(Group group) { - groupIdMapping.put(group.getId(), group); - when(config.getGroupIdMapping()).thenReturn(groupIdMapping); - } - - void setMockConfig() { - if (config != null) return; - - ProjectConfig configReal = null; - try { - configReal = new DatafileProjectConfig.Builder().withDatafile(datafile).build(); - config = spy(configReal); - optimizely = Optimizely.builder().withConfig(config).build(); - experimentIdMapping = new HashMap<>(config.getExperimentIdMapping()); - groupIdMapping = new HashMap<>(config.getGroupIdMapping()); - featureKeyMapping = new HashMap<>(config.getFeatureKeyMapping()); - } catch (ConfigParseException e) { - fail("ProjectConfig build failed"); - } - } - - OptimizelyDecision callDecideWithIncludeReasons(String flagKey, Map attributes) { - OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); - return user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); - } - - OptimizelyDecision callDecideWithIncludeReasons(String flagKey) { - return callDecideWithIncludeReasons(flagKey, Collections.emptyMap()); - } - - @Test - public void decide_with_holdout() throws Exception { - Optimizely optWithHoldout = createOptimizelyWithHoldouts(); - // pick a flag that is eligible for basic_holdout. Using boolean_feature from config. - String flagKey = "boolean_feature"; - String userId = "user123"; - Map attrs = new HashMap<>(); - attrs.put("$opt_bucketing_id", "ppid160000"); - attrs.put("nationality", "English"); - OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); - - // include reasons to surface holdout logs in decision reasons if implemented - OptimizelyDecision decision = user.decide(flagKey, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); - - assertNotNull(decision); - assertEquals(flagKey, decision.getFlagKey()); - // holdout off variation => feature should be disabled - assertFalse(decision.getEnabled()); - // Expect decision source to be holdout (either via metadata map or reasons text) - boolean hasHoldoutReason = false; - String expectedMString = "User (" + userId + ") is in variation (ho_off_key) of holdout (basic_holdout)."; - if (decision.getReasons().contains(expectedMString)) { - hasHoldoutReason = true; - } - assertTrue("Expected holdout to influence decision (reason containing 'holdout')", hasHoldoutReason); - logbackVerifier.expectMessage(Level.INFO, expectedMString); - - // Impression expectation: Holdout decisions still dispatch an impression with holdout context. - String variationId = "$opt_dummy_variation_id"; // from holdouts-project-config.json - String experimentId = "10075323428"; // holdout id for basic_holdout - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("basic_holdout") - .setRuleType("holdout") - .setVariationKey("ho_off_key") - .setEnabled(false) - .build(); - eventHandler.expectImpression(experimentId, variationId, userId, Collections.singletonMap("nationality", "English"), metadata); - } - - @Test - public void decide_for_keys_with_holdout() throws Exception { - Optimizely optWithHoldout = createOptimizelyWithHoldouts(); - String userId = "user123"; - Map attrs = new HashMap<>(); - attrs.put("$opt_bucketing_id", "ppid300002"); // deterministic bucketing used in prior holdout test - OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); - - List flagKeys = Arrays.asList( - "boolean_feature", // previously validated basic_holdout membership - "double_single_variable_feature", // also subject to global/basic holdout - "integer_single_variable_feature" // also subject to global/basic holdout - ); - - Map decisions = user.decideForKeys(flagKeys, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); - assertEquals(3, decisions.size()); - - String holdoutExperimentId = "10075323428"; // basic_holdout id - String variationId = "$opt_dummy_variation_id"; - String variationKey = "ho_off_key"; - String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (basic_holdout)."; - - for (String flagKey : flagKeys) { - OptimizelyDecision d = decisions.get(flagKey); - assertNotNull(d); - assertEquals(flagKey, d.getFlagKey()); - assertEquals(variationKey, d.getVariationKey()); - assertFalse(d.getEnabled()); - assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("basic_holdout") - .setRuleType("holdout") - .setVariationKey(variationKey) - .setEnabled(false) - .build(); - // attributes map expected empty (reserved $opt_ attribute filtered out) - eventHandler.expectImpression(holdoutExperimentId, variationId, userId, Collections.emptyMap(), metadata); - } - - // At least one log message confirming holdout membership - logbackVerifier.expectMessage(Level.INFO, expectedReason); - } - - @Test - public void decide_all_with_holdout() throws Exception { - - Optimizely optWithHoldout = createOptimizelyWithHoldouts(); - String userId = "user123"; - Map attrs = new HashMap<>(); - // ppid120000 buckets user into holdout_included_flags - attrs.put("$opt_bucketing_id", "ppid120000"); - OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); - - // All flag keys present in holdouts-project-config.json - List allFlagKeys = Arrays.asList( - "boolean_feature", - "double_single_variable_feature", - "integer_single_variable_feature", - "boolean_single_variable_feature", - "string_single_variable_feature", - "multi_variate_feature", - "multi_variate_future_feature", - "mutex_group_feature" - ); - - // Flags INCLUDED in holdout_included_flags (only these should be holdout decisions) - List includedInHoldout = Arrays.asList( - "boolean_feature", - "double_single_variable_feature", - "integer_single_variable_feature" - ); - - Map decisions = user.decideAll(Arrays.asList( - OptimizelyDecideOption.INCLUDE_REASONS, - OptimizelyDecideOption.DISABLE_DECISION_EVENT - )); - assertEquals(allFlagKeys.size(), decisions.size()); - - String holdoutExperimentId = "1007543323427"; // holdout_included_flags id - String variationId = "$opt_dummy_variation_id"; - String variationKey = "ho_off_key"; - String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags)."; - - int holdoutCount = 0; - for (String flagKey : allFlagKeys) { - OptimizelyDecision d = decisions.get(flagKey); - assertNotNull("Missing decision for flag " + flagKey, d); - if (includedInHoldout.contains(flagKey)) { - // Should be holdout decision - assertEquals(variationKey, d.getVariationKey()); - assertFalse(d.getEnabled()); - assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("holdout_included_flags") - .setRuleType("holdout") - .setVariationKey(variationKey) - .setEnabled(false) - .build(); - holdoutCount++; - } else { - // Should NOT be a holdout decision - assertFalse("Non-included flag should not have holdout reason: " + flagKey, d.getReasons().contains(expectedReason)); - } - } - assertEquals("Expected exactly the included flags to be in holdout", includedInHoldout.size(), holdoutCount); - logbackVerifier.expectMessage(Level.INFO, expectedReason); - } } From 075fd4e421b7b949096f3121475850a3663eae61 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Mon, 11 Aug 2025 19:46:11 +0600 Subject: [PATCH 13/17] Revert back original test cases --- .../ab/OptimizelyUserContextTest.java | 1853 +++++++++++++---- 1 file changed, 1493 insertions(+), 360 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 41d52d56d..c2ec1c580 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -16,59 +16,51 @@ */ package com.optimizely.ab; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.junit.Assert; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - +import ch.qos.logback.classic.Level; import com.google.common.base.Charsets; import com.google.common.io.Resources; import com.optimizely.ab.bucketing.FeatureDecision; import com.optimizely.ab.bucketing.UserProfile; import com.optimizely.ab.bucketing.UserProfileService; import com.optimizely.ab.bucketing.UserProfileUtils; -import com.optimizely.ab.config.Experiment; -import com.optimizely.ab.config.FeatureFlag; -import com.optimizely.ab.config.Group; -import com.optimizely.ab.config.ProjectConfig; -import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; -import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; +import com.optimizely.ab.config.*; +import com.optimizely.ab.config.parser.ConfigParseException; +import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.EventProcessor; import com.optimizely.ab.event.ForwardingEventProcessor; import com.optimizely.ab.event.internal.ImpressionEvent; import com.optimizely.ab.event.internal.payload.DecisionMetadata; import com.optimizely.ab.internal.LogbackVerifier; -import static com.optimizely.ab.notification.DecisionNotification.ExperimentDecisionNotificationBuilder.VARIATION_KEY; -import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.DECISION_EVENT_DISPATCHED; -import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.ENABLED; -import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.EXPERIMENT_ID; -import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.FLAG_KEY; -import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.REASONS; -import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.RULE_KEY; -import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.VARIABLES; -import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.VARIATION_ID; import com.optimizely.ab.notification.NotificationCenter; +import com.optimizely.ab.odp.*; +import com.optimizely.ab.optimizelydecision.DecisionMessage; import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption; import com.optimizely.ab.optimizelydecision.OptimizelyDecision; import com.optimizely.ab.optimizelyjson.OptimizelyJSON; +import junit.framework.TestCase; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import ch.qos.logback.classic.Level; +import java.util.*; +import java.util.concurrent.CountDownLatch; + +import static com.optimizely.ab.config.ValidProjectConfigV4.ATTRIBUTE_HOUSE_KEY; +import static com.optimizely.ab.config.ValidProjectConfigV4.AUDIENCE_GRYFFINDOR_VALUE; +import static com.optimizely.ab.notification.DecisionNotification.ExperimentDecisionNotificationBuilder.VARIATION_KEY; +import static com.optimizely.ab.notification.DecisionNotification.FlagDecisionNotificationBuilder.*; import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; public class OptimizelyUserContextTest { + @Rule public EventHandlerRule eventHandler = new EventHandlerRule(); @@ -85,25 +77,13 @@ public class OptimizelyUserContextTest { Map featureKeyMapping; Map groupIdMapping; - private String holdoutDatafile; - @Before public void setUp() throws Exception { datafile = Resources.toString(Resources.getResource("config/decide-project-config.json"), Charsets.UTF_8); optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .build(); - } - - private Optimizely createOptimizelyWithHoldouts() throws Exception { - if (holdoutDatafile == null) { - holdoutDatafile = com.google.common.io.Resources.toString( - com.google.common.io.Resources.getResource("config/holdouts-project-config.json"), - com.google.common.base.Charsets.UTF_8 - ); - } - return new Optimizely.Builder().withDatafile(holdoutDatafile).withEventProcessor(new ForwardingEventProcessor(eventHandler, null)).build(); + .withDatafile(datafile) + .build(); } @Test @@ -192,13 +172,12 @@ public void setAttribute_nullValue() { } // decide - @Test public void decide_featureTest() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_2"; String experimentKey = "exp_no_audience"; @@ -219,21 +198,21 @@ public void decide_featureTest() { assertTrue(decision.getReasons().isEmpty()); DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey(experimentKey) - .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) - .setVariationKey(variationKey) - .setEnabled(true) - .build(); + .setFlagKey(flagKey) + .setRuleKey(experimentKey) + .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) + .setVariationKey(variationKey) + .setEnabled(true) + .build(); eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); } @Test public void decide_rollout() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_1"; String experimentKey = "18322080788"; @@ -254,21 +233,21 @@ public void decide_rollout() { assertTrue(decision.getReasons().isEmpty()); DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey(experimentKey) - .setRuleType(FeatureDecision.DecisionSource.ROLLOUT.toString()) - .setVariationKey(variationKey) - .setEnabled(true) - .build(); + .setFlagKey(flagKey) + .setRuleKey(experimentKey) + .setRuleType(FeatureDecision.DecisionSource.ROLLOUT.toString()) + .setVariationKey(variationKey) + .setEnabled(true) + .build(); eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); } @Test public void decide_nullVariation() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_3"; OptimizelyJSON variablesExpected = new OptimizelyJSON(Collections.emptyMap()); @@ -285,23 +264,22 @@ public void decide_nullVariation() { assertTrue(decision.getReasons().isEmpty()); DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("") - .setRuleType(FeatureDecision.DecisionSource.ROLLOUT.toString()) - .setVariationKey("") - .setEnabled(false) - .build(); + .setFlagKey(flagKey) + .setRuleKey("") + .setRuleType(FeatureDecision.DecisionSource.ROLLOUT.toString()) + .setVariationKey("") + .setEnabled(false) + .build(); eventHandler.expectImpression(null, "", userId, Collections.emptyMap(), metadata); } // decideAll - @Test public void decideAll_oneFlag() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_2"; String experimentKey = "exp_no_audience"; @@ -319,22 +297,22 @@ public void decideAll_oneFlag() { OptimizelyDecision decision = decisions.get(flagKey); OptimizelyDecision expDecision = new OptimizelyDecision( - variationKey, - true, - variablesExpected, - experimentKey, - flagKey, - user, - Collections.emptyList()); + variationKey, + true, + variablesExpected, + experimentKey, + flagKey, + user, + Collections.emptyList()); assertEquals(decision, expDecision); DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey(experimentKey) - .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) - .setVariationKey(variationKey) - .setEnabled(true) - .build(); + .setFlagKey(flagKey) + .setRuleKey(experimentKey) + .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) + .setVariationKey(variationKey) + .setEnabled(true) + .build(); eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); } @@ -353,23 +331,23 @@ public void decideAll_twoFlags() { assertTrue(decisions.size() == 2); assertEquals( - decisions.get(flagKey1), - new OptimizelyDecision("a", - true, - variablesExpected1, - "exp_with_audience", - flagKey1, - user, - Collections.emptyList())); + decisions.get(flagKey1), + new OptimizelyDecision("a", + true, + variablesExpected1, + "exp_with_audience", + flagKey1, + user, + Collections.emptyList())); assertEquals( - decisions.get(flagKey2), - new OptimizelyDecision("variation_with_traffic", - true, - variablesExpected2, - "exp_no_audience", - flagKey2, - user, - Collections.emptyList())); + decisions.get(flagKey2), + new OptimizelyDecision("variation_with_traffic", + true, + variablesExpected2, + "exp_no_audience", + flagKey2, + user, + Collections.emptyList())); } @Test @@ -377,9 +355,9 @@ public void decideAll_allFlags() { EventProcessor mockEventProcessor = mock(EventProcessor.class); optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(mockEventProcessor) - .build(); + .withDatafile(datafile) + .withEventProcessor(mockEventProcessor) + .build(); String flagKey1 = "feature_1"; String flagKey2 = "feature_2"; @@ -395,35 +373,35 @@ public void decideAll_allFlags() { assertEquals(decisions.size(), 3); assertEquals( - decisions.get(flagKey1), - new OptimizelyDecision( - "a", - true, - variablesExpected1, - "exp_with_audience", - flagKey1, - user, - Collections.emptyList())); + decisions.get(flagKey1), + new OptimizelyDecision( + "a", + true, + variablesExpected1, + "exp_with_audience", + flagKey1, + user, + Collections.emptyList())); assertEquals( - decisions.get(flagKey2), - new OptimizelyDecision( - "variation_with_traffic", - true, - variablesExpected2, - "exp_no_audience", - flagKey2, - user, - Collections.emptyList())); + decisions.get(flagKey2), + new OptimizelyDecision( + "variation_with_traffic", + true, + variablesExpected2, + "exp_no_audience", + flagKey2, + user, + Collections.emptyList())); assertEquals( - decisions.get(flagKey3), - new OptimizelyDecision( - null, - false, - variablesExpected3, - null, - flagKey3, - user, - Collections.emptyList())); + decisions.get(flagKey3), + new OptimizelyDecision( + null, + false, + variablesExpected3, + null, + flagKey3, + user, + Collections.emptyList())); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ImpressionEvent.class); verify(mockEventProcessor, times(3)).process(argumentCaptor.capture()); @@ -435,7 +413,6 @@ public void decideAll_allFlags() { assertEquals(sentEvents.get(0).getVariationKey(), "a"); assertEquals(sentEvents.get(0).getUserContext().getUserId(), userId); - assertEquals(sentEvents.get(1).getExperimentKey(), "exp_no_audience"); assertEquals(sentEvents.get(1).getVariationKey(), "variation_with_traffic"); assertEquals(sentEvents.get(1).getUserContext().getUserId(), userId); @@ -449,9 +426,9 @@ public void decideForKeys_ups_batching() throws Exception { UserProfileService ups = mock(UserProfileService.class); optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withUserProfileService(ups) - .build(); + .withDatafile(datafile) + .withUserProfileService(ups) + .build(); String flagKey1 = "feature_1"; String flagKey2 = "feature_2"; @@ -460,14 +437,13 @@ public void decideForKeys_ups_batching() throws Exception { OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); Map decisions = user.decideForKeys(Arrays.asList( - flagKey1, flagKey2, flagKey3 + flagKey1, flagKey2, flagKey3 )); assertEquals(decisions.size(), 3); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Map.class); - verify(ups, times(1)).lookup(userId); verify(ups, times(1)).save(argumentCaptor.capture()); @@ -482,9 +458,9 @@ public void decideAll_ups_batching() throws Exception { UserProfileService ups = mock(UserProfileService.class); optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withUserProfileService(ups) - .build(); + .withDatafile(datafile) + .withUserProfileService(ups) + .build(); Map attributes = Collections.singletonMap("gender", "f"); @@ -495,7 +471,6 @@ public void decideAll_ups_batching() throws Exception { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Map.class); - verify(ups, times(1)).lookup(userId); verify(ups, times(1)).save(argumentCaptor.capture()); @@ -516,25 +491,24 @@ public void decideAll_allFlags_enabledFlagsOnly() { assertTrue(decisions.size() == 2); assertEquals( - decisions.get(flagKey1), - new OptimizelyDecision( - "a", - true, - variablesExpected1, - "exp_with_audience", - flagKey1, - user, - Collections.emptyList())); + decisions.get(flagKey1), + new OptimizelyDecision( + "a", + true, + variablesExpected1, + "exp_with_audience", + flagKey1, + user, + Collections.emptyList())); } // trackEvent - @Test public void trackEvent() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); String eventKey = "event1"; @@ -548,9 +522,9 @@ public void trackEvent() { @Test public void trackEvent_noEventTags() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); String eventKey = "event1"; @@ -563,9 +537,9 @@ public void trackEvent_noEventTags() { @Test public void trackEvent_emptyAttributes() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String eventKey = "event1"; Map eventTags = Collections.singletonMap("name", "carrot"); @@ -576,13 +550,12 @@ public void trackEvent_emptyAttributes() { } // send events - @Test public void decide_sendEvent() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_2"; String variationKey = "variation_with_traffic"; @@ -600,9 +573,9 @@ public void decide_sendEvent() { @Test public void decide_doNotSendEvent_withOption() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_2"; @@ -617,18 +590,18 @@ public void decide_doNotSendEvent_withOption() { @Test public void decide_sendEvent_featureTest_withSendFlagDecisionsOn() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); String flagKey = "feature_2"; String experimentId = "10420810910"; @@ -643,18 +616,18 @@ public void decide_sendEvent_featureTest_withSendFlagDecisionsOn() { @Test public void decide_sendEvent_rollout_withSendFlagDecisionsOn() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); String flagKey = "feature_3"; String experimentId = null; @@ -670,18 +643,18 @@ public void decide_sendEvent_rollout_withSendFlagDecisionsOn() { public void decide_sendEvent_featureTest_withSendFlagDecisionsOff() { String datafileWithSendFlagDecisionsOff = datafile.replace("\"sendFlagDecisions\": true", "\"sendFlagDecisions\": false"); optimizely = new Optimizely.Builder() - .withDatafile(datafileWithSendFlagDecisionsOff) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafileWithSendFlagDecisionsOff) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); String flagKey = "feature_2"; String experimentId = "10420810910"; @@ -697,18 +670,18 @@ public void decide_sendEvent_featureTest_withSendFlagDecisionsOff() { public void decide_sendEvent_rollout_withSendFlagDecisionsOff() { String datafileWithSendFlagDecisionsOff = datafile.replace("\"sendFlagDecisions\": true", "\"sendFlagDecisions\": false"); optimizely = new Optimizely.Builder() - .withDatafile(datafileWithSendFlagDecisionsOff) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafileWithSendFlagDecisionsOff) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), false); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), false); + isListenerCalled = true; + }); String flagKey = "feature_3"; isListenerCalled = false; @@ -719,7 +692,6 @@ public void decide_sendEvent_rollout_withSendFlagDecisionsOff() { } // notifications - @Test public void decisionNotification() { String flagKey = "feature_2"; @@ -745,13 +717,13 @@ public void decisionNotification() { OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getType(), NotificationCenter.DecisionNotificationType.FLAG.toString()); - Assert.assertEquals(decisionNotification.getUserId(), userId); - Assert.assertEquals(decisionNotification.getAttributes(), attributes); - Assert.assertEquals(decisionNotification.getDecisionInfo(), testDecisionInfoMap); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getType(), NotificationCenter.DecisionNotificationType.FLAG.toString()); + Assert.assertEquals(decisionNotification.getUserId(), userId); + Assert.assertEquals(decisionNotification.getAttributes(), attributes); + Assert.assertEquals(decisionNotification.getDecisionInfo(), testDecisionInfoMap); + isListenerCalled = true; + }); isListenerCalled = false; testDecisionInfoMap.put(DECISION_EVENT_DISPATCHED, true); @@ -764,181 +736,1342 @@ public void decisionNotification() { assertTrue(isListenerCalled); } + // options @Test - public void decide_for_keys_with_holdout() throws Exception { - Optimizely optWithHoldout = createOptimizelyWithHoldouts(); - String userId = "user123"; - Map attrs = new HashMap<>(); - attrs.put("$opt_bucketing_id", "ppid160000"); - OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + public void decideOptions_bypassUPS() throws Exception { + String flagKey = "feature_2"; // embedding experiment: "exp_no_audience" + String experimentId = "10420810910"; // "exp_no_audience" + String variationId1 = "10418551353"; + String variationId2 = "10418510624"; + String variationKey1 = "variation_with_traffic"; + String variationKey2 = "variation_no_traffic"; - List flagKeys = Arrays.asList( - "boolean_feature", // previously validated basic_holdout membership - "double_single_variable_feature", // also subject to global/basic holdout - "integer_single_variable_feature" // also subject to global/basic holdout - ); + UserProfileService ups = mock(UserProfileService.class); + when(ups.lookup(userId)).thenReturn(createUserProfileMap(experimentId, variationId2)); - Map decisions = user.decideForKeys(flagKeys, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); - assertEquals(3, decisions.size()); - - String holdoutExperimentId = "10075323428"; // basic_holdout id - String variationId = "$opt_dummy_variation_id"; - String variationKey = "ho_off_key"; - String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (basic_holdout)."; - - for (String flagKey : flagKeys) { - OptimizelyDecision d = decisions.get(flagKey); - assertNotNull(d); - assertEquals(flagKey, d.getFlagKey()); - assertEquals(variationKey, d.getVariationKey()); - assertFalse(d.getEnabled()); - assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("basic_holdout") - .setRuleType("holdout") - .setVariationKey(variationKey) - .setEnabled(false) + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withUserProfileService(ups) .build(); - // attributes map expected empty (reserved $opt_ attribute filtered out) - eventHandler.expectImpression(holdoutExperimentId, variationId, userId, Collections.emptyMap(), metadata); - } - // At least one log message confirming holdout membership - logbackVerifier.expectMessage(Level.INFO, expectedReason); + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + // should return variationId2 set by UPS + assertEquals(decision.getVariationKey(), variationKey2); + + decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.IGNORE_USER_PROFILE_SERVICE)); + // should ignore variationId2 set by UPS and return variationId1 + assertEquals(decision.getVariationKey(), variationKey1); + // also should not save either + verify(ups, never()).save(anyObject()); + } + + @Test + public void decideOptions_excludeVariables() { + String flagKey = "feature_1"; + OptimizelyUserContext user = optimizely.createUserContext(userId); + + OptimizelyDecision decision = user.decide(flagKey); + assertTrue(decision.getVariables().toMap().size() > 0); + + decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.EXCLUDE_VARIABLES)); + assertTrue(decision.getVariables().toMap().size() == 0); } @Test - public void decide_all_with_holdout() throws Exception { + public void decideOptions_includeReasons() { + OptimizelyUserContext user = optimizely.createUserContext(userId); - Optimizely optWithHoldout = createOptimizelyWithHoldouts(); - String userId = "user123"; - Map attrs = new HashMap<>(); - // ppid120000 buckets user into holdout_included_flags - attrs.put("$opt_bucketing_id", "ppid120000"); - OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + String flagKey = "invalid_key"; + OptimizelyDecision decision = user.decide(flagKey); + assertEquals(decision.getReasons().size(), 1); + TestCase.assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); - // All flag keys present in holdouts-project-config.json - List allFlagKeys = Arrays.asList( - "boolean_feature", - "double_single_variable_feature", - "integer_single_variable_feature", - "boolean_single_variable_feature", - "string_single_variable_feature", - "multi_variate_feature", - "multi_variate_future_feature", - "mutex_group_feature" - ); + decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); - // Flags INCLUDED in holdout_included_flags (only these should be holdout decisions) - List includedInHoldout = Arrays.asList( - "boolean_feature", - "double_single_variable_feature", - "integer_single_variable_feature" + flagKey = "feature_1"; + decision = user.decide(flagKey); + assertEquals(decision.getReasons().size(), 0); + + decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + assertTrue(decision.getReasons().size() > 0); + } + + public void decideOptions_disableDispatchEvent() { + // tested already with decide_doNotSendEvent() above + } + + public void decideOptions_enabledFlagsOnly() { + // tested already with decideAll_allFlags_enabledFlagsOnly() above + } + + @Test + public void decideOptions_defaultDecideOptions() { + List options = Arrays.asList( + OptimizelyDecideOption.EXCLUDE_VARIABLES ); - Map decisions = user.decideAll(Arrays.asList( - OptimizelyDecideOption.INCLUDE_REASONS, - OptimizelyDecideOption.DISABLE_DECISION_EVENT - )); - assertEquals(allFlagKeys.size(), decisions.size()); - - String holdoutExperimentId = "1007543323427"; // holdout_included_flags id - String variationId = "$opt_dummy_variation_id"; - String variationKey = "ho_off_key"; - String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags)."; - - int holdoutCount = 0; - for (String flagKey : allFlagKeys) { - OptimizelyDecision d = decisions.get(flagKey); - assertNotNull("Missing decision for flag " + flagKey, d); - if (includedInHoldout.contains(flagKey)) { - // Should be holdout decision - assertEquals(variationKey, d.getVariationKey()); - assertFalse(d.getEnabled()); - assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("holdout_included_flags") - .setRuleType("holdout") - .setVariationKey(variationKey) - .setEnabled(false) - .build(); - holdoutCount++; - } else { - // Should NOT be a holdout decision - assertFalse("Non-included flag should not have holdout reason: " + flagKey, d.getReasons().contains(expectedReason)); - } - } - assertEquals("Expected exactly the included flags to be in holdout", includedInHoldout.size(), holdoutCount); - logbackVerifier.expectMessage(Level.INFO, expectedReason); + optimizely = Optimizely.builder() + .withDatafile(datafile) + .withDefaultDecideOptions(options) + .build(); + + String flagKey = "feature_1"; + OptimizelyUserContext user = optimizely.createUserContext(userId); + + // should be excluded by DefaultDecideOption + OptimizelyDecision decision = user.decide(flagKey); + assertTrue(decision.getVariables().toMap().size() == 0); + + decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS, OptimizelyDecideOption.EXCLUDE_VARIABLES)); + // other options should work as well + assertTrue(decision.getReasons().size() > 0); + // redundant setting ignored + assertTrue(decision.getVariables().toMap().size() == 0); } + // errors @Test - public void decisionNotification_with_holdout() throws Exception { - // Use holdouts datafile - Optimizely optWithHoldout = createOptimizelyWithHoldouts(); - String flagKey = "boolean_feature"; - String userId = "user123"; - String ruleKey = "basic_holdout"; // holdout rule key - String variationKey = "ho_off_key"; // holdout (off) variation key - String experimentId = "10075323428"; // holdout experiment id in holdouts-project-config.json - String variationId = "$opt_dummy_variation_id";// dummy variation id used for holdout impressions - String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (" + ruleKey + ")."; + public void decide_sdkNotReady() { + String flagKey = "feature_1"; - Map attrs = new HashMap<>(); - attrs.put("$opt_bucketing_id", "ppid160000"); // deterministic bucketing into basic_holdout - attrs.put("nationality", "English"); // non-reserved attribute should appear in impression & notification + Optimizely optimizely = new Optimizely.Builder().build(); + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); - OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + assertNull(decision.getVariationKey()); + assertFalse(decision.getEnabled()); + assertTrue(decision.getVariables().isEmpty()); + assertEquals(decision.getFlagKey(), flagKey); + assertEquals(decision.getUserContext(), user); - // Register notification handler similar to decisionNotification test - isListenerCalled = false; - optWithHoldout.addDecisionNotificationHandler(decisionNotification -> { - Assert.assertEquals(NotificationCenter.DecisionNotificationType.FLAG.toString(), decisionNotification.getType()); - Assert.assertEquals(userId, decisionNotification.getUserId()); - - Assert.assertEquals(attrs, decisionNotification.getAttributes()); - - Map info = decisionNotification.getDecisionInfo(); - Assert.assertEquals(flagKey, info.get(FLAG_KEY)); - Assert.assertEquals(variationKey, info.get(VARIATION_KEY)); - Assert.assertEquals(false, info.get(ENABLED)); - Assert.assertEquals(ruleKey, info.get(RULE_KEY)); - Assert.assertEquals(experimentId, info.get(EXPERIMENT_ID)); - Assert.assertEquals(variationId, info.get(VARIATION_ID)); - // Variables should be empty because feature is disabled by holdout - Assert.assertTrue(((Map) info.get(VARIABLES)).isEmpty()); - // Event should be dispatched (no DISABLE_DECISION_EVENT option) - Assert.assertEquals(true, info.get(DECISION_EVENT_DISPATCHED)); - - @SuppressWarnings("unchecked") - List reasons = (List) info.get(REASONS); - Assert.assertTrue("Expected holdout reason present", reasons.contains(expectedReason)); - isListenerCalled = true; - }); + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), DecisionMessage.SDK_NOT_READY.reason()); + } - // Execute decision with INCLUDE_REASONS so holdout reason is present - OptimizelyDecision decision = user.decide(flagKey, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); - assertTrue(isListenerCalled); + @Test + public void decide_invalidFeatureKey() { + String flagKey = "invalid_key"; - // Sanity checks on returned decision - assertEquals(variationKey, decision.getVariationKey()); + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertNull(decision.getVariationKey()); assertFalse(decision.getEnabled()); - assertTrue(decision.getReasons().contains(expectedReason)); + assertTrue(decision.getVariables().isEmpty()); + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); + } - // Impression expectation (nationality only) - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey(ruleKey) - .setRuleType("holdout") - .setVariationKey(variationKey) - .setEnabled(false) + @Test + public void decideAll_sdkNotReady() { + List flagKeys = Arrays.asList("feature_1"); + + Optimizely optimizely = new Optimizely.Builder().build(); + OptimizelyUserContext user = optimizely.createUserContext(userId); + Map decisions = user.decideForKeys(flagKeys); + + assertEquals(decisions.size(), 0); + } + + @Test + public void decideAll_errorDecisionIncluded() { + String flagKey1 = "feature_2"; + String flagKey2 = "invalid_key"; + + List flagKeys = Arrays.asList(flagKey1, flagKey2); + OptimizelyJSON variablesExpected1 = optimizely.getAllFeatureVariables(flagKey1, userId); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + Map decisions = user.decideForKeys(flagKeys); + + assertEquals(decisions.size(), 2); + + assertEquals( + decisions.get(flagKey1), + new OptimizelyDecision( + "variation_with_traffic", + true, + variablesExpected1, + "exp_no_audience", + flagKey1, + user, + Collections.emptyList())); + assertEquals( + decisions.get(flagKey2), + OptimizelyDecision.newErrorDecision( + flagKey2, + user, + DecisionMessage.FLAG_KEY_INVALID.reason(flagKey2))); + } + + // reasons (errors) + @Test + public void decideReasons_sdkNotReady() { + String flagKey = "feature_1"; + + Optimizely optimizely = new Optimizely.Builder().build(); + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), DecisionMessage.SDK_NOT_READY.reason()); + } + + @Test + public void decideReasons_featureKeyInvalid() { + String flagKey = "invalid_key"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertEquals(decision.getReasons().size(), 1); + assertEquals(decision.getReasons().get(0), DecisionMessage.FLAG_KEY_INVALID.reason(flagKey)); + } + + @Test + public void decideReasons_variableValueInvalid() { + String flagKey = "feature_1"; + + FeatureFlag flag = getSpyFeatureFlag(flagKey); + List variables = Arrays.asList(new FeatureVariable("any-id", "any-key", "invalid", null, "integer", null)); + when(flag.getVariables()).thenReturn(variables); + addSpyFeatureFlag(flag); + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey); + + assertEquals(decision.getReasons().get(0), DecisionMessage.VARIABLE_VALUE_INVALID.reason("any-key")); + } + + // reasons (infos with includeReasons) + @Test + public void decideReasons_experimentNotRunning() { + String flagKey = "feature_1"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.isActive()).thenReturn(false); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("Experiment \"exp_with_audience\" is not running.") + )); + } + + @Test + public void decideReasons_gotVariationFromUserProfile() throws Exception { + String flagKey = "feature_2"; // embedding experiment: "exp_no_audience" + String experimentId = "10420810910"; // "exp_no_audience" + String experimentKey = "exp_no_audience"; + String variationId2 = "10418510624"; + String variationKey2 = "variation_no_traffic"; + + UserProfileService ups = mock(UserProfileService.class); + when(ups.lookup(userId)).thenReturn(createUserProfileMap(experimentId, variationId2)); + + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withUserProfileService(ups) .build(); - eventHandler.expectImpression(experimentId, variationId, userId, Collections.singletonMap("nationality", "English"), metadata); - // Log expectation (reuse existing pattern) - logbackVerifier.expectMessage(Level.INFO, expectedReason); + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(decision.getReasons().contains( + String.format("Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", variationKey2, experimentKey, userId) + )); + } + + @Test + public void decideReasons_forcedVariationFound() { + String flagKey = "feature_1"; + String variationKey = "b"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getUserIdToVariationKeyMap()).thenReturn(Collections.singletonMap(userId, variationKey)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("User \"%s\" is forced in variation \"%s\".", userId, variationKey) + )); + } + + @Test + public void decideReasons_forcedVariationFoundButInvalid() { + String flagKey = "feature_1"; + String variationKey = "invalid-key"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getUserIdToVariationKeyMap()).thenReturn(Collections.singletonMap(userId, variationKey)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("Variation \"%s\" is not in the datafile. Not activating user \"%s\".", variationKey, userId) + )); + } + + @Test + public void decideReasons_userMeetsConditionsForTargetingRule() { + String flagKey = "feature_1"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.setAttribute("country", "US"); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(decision.getReasons().contains( + String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) + )); + } + + @Test + public void decideReasons_userDoesntMeetConditionsForTargetingRule() { + String flagKey = "feature_1"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.setAttribute("country", "CA"); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(decision.getReasons().contains( + String.format("User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, 1) + )); + } + + @Test + public void decideReasons_userBucketedIntoTargetingRule() { + String flagKey = "feature_1"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.setAttribute("country", "US"); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(decision.getReasons().contains( + String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) + )); + } + + @Test + public void decideReasons_userBucketedIntoEveryoneTargetingRule() { + String flagKey = "feature_1"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.setAttribute("country", "KO"); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(decision.getReasons().contains( + String.format("User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId) + )); + } + + @Test + public void decideReasons_userNotBucketedIntoTargetingRule() { + String flagKey = "feature_1"; + String experimentKey = "3332020494"; // experimentKey of rollout[2] + + OptimizelyUserContext user = optimizely.createUserContext(userId); + user.setAttribute("browser", "safari"); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", userId, experimentKey) + )); + } + + @Test + public void decideReasons_userBucketedIntoVariationInExperiment() { + String flagKey = "feature_2"; + String experimentKey = "exp_no_audience"; + String variationKey = "variation_with_traffic"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", userId, variationKey, experimentKey) + )); + } + + @Test + public void decideReasons_userNotBucketedIntoVariation() { + String flagKey = "feature_2"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getTrafficAllocation()).thenReturn(Arrays.asList(new TrafficAllocation("any-id", 0))); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); + + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"%s\" is not in any variation of experiment \"exp_no_audience\".", userId) + )); + } + + @Test + public void decideReasons_userBucketedIntoExperimentInGroup() { + String flagKey = "feature_3"; + String experimentId = "10390965532"; // "group_exp_1" + + FeatureFlag flag = getSpyFeatureFlag(flagKey); + when(flag.getExperimentIds()).thenReturn(Arrays.asList(experimentId)); + addSpyFeatureFlag(flag); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"tester\" is in experiment \"group_exp_1\" of group 13142870430.") + )); + } + + @Test + public void decideReasons_userNotBucketedIntoExperimentInGroup() { + String flagKey = "feature_3"; + String experimentId = "10420843432"; // "group_exp_2" + + FeatureFlag flag = getSpyFeatureFlag(flagKey); + when(flag.getExperimentIds()).thenReturn(Arrays.asList(experimentId)); + addSpyFeatureFlag(flag); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"tester\" is not in experiment \"group_exp_2\" of group 13142870430.") + )); + } + + @Test + public void decideReasons_userNotBucketedIntoAnyExperimentInGroup() { + String flagKey = "feature_3"; + String experimentId = "10390965532"; // "group_exp_1" + String groupId = "13142870430"; + + FeatureFlag flag = getSpyFeatureFlag(flagKey); + when(flag.getExperimentIds()).thenReturn(Arrays.asList(experimentId)); + addSpyFeatureFlag(flag); + + Group group = getSpyGroup(groupId); + when(group.getTrafficAllocation()).thenReturn(Collections.emptyList()); + addSpyGroup(group); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("User with bucketingId \"tester\" is not in any experiment of group 13142870430.") + )); + } + + @Test + public void decideReasons_userNotInExperiment() { + String flagKey = "feature_1"; + String experimentKey = "exp_with_audience"; + + OptimizelyUserContext user = optimizely.createUserContext(userId); + OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(decision.getReasons().contains( + String.format("User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experimentKey) + )); + } + + @Test + public void decideReasons_conditionNoMatchingAudience() throws ConfigParseException { + String flagKey = "feature_1"; + String audienceId = "invalid_id"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + )); + } + + @Test + public void decideReasons_evaluateAttributeInvalidType() { + String flagKey = "feature_1"; + String audienceId = "13389130056"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("country", 25)); + + assertTrue(decision.getReasons().contains( + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + )); + } + + @Test + public void decideReasons_evaluateAttributeValueOutOfRange() { + String flagKey = "feature_1"; + String audienceId = "age_18"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", (float) Math.pow(2, 54))); + + assertTrue(decision.getReasons().contains( + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + )); + } + + @Test + public void decideReasons_userAttributeInvalidType() { + String flagKey = "feature_1"; + String audienceId = "invalid_type"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); + + assertTrue(decision.getReasons().contains( + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + )); + } + + @Test + public void decideReasons_userAttributeInvalidMatch() { + String flagKey = "feature_1"; + String audienceId = "invalid_match"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); + + assertTrue(decision.getReasons().contains( + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + )); + } + + @Test + public void decideReasons_userAttributeNilValue() { + String flagKey = "feature_1"; + String audienceId = "nil_value"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); + + assertTrue(decision.getReasons().contains( + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + )); + } + + @Test + public void decideReasons_missingAttributeValue() { + String flagKey = "feature_1"; + String audienceId = "age_18"; + + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); + + assertTrue(decision.getReasons().contains( + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + )); + } + + @Test + public void setForcedDecisionWithRuleKeyTest() { + String flagKey = "55555"; + String ruleKey = "77777"; + String variationKey = "33333"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + String foundVariationKey = optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey(); + assertEquals(variationKey, foundVariationKey); + } + + @Test + public void setForcedDecisionsWithRuleKeyTest() { + String flagKey = "feature_2"; + String ruleKey = "exp_no_audience"; + String ruleKey2 = "88888"; + String variationKey = "33333"; + String variationKey2 = "variation_with_traffic"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + OptimizelyDecisionContext optimizelyDecisionContext2 = new OptimizelyDecisionContext(flagKey, ruleKey2); + OptimizelyForcedDecision optimizelyForcedDecision2 = new OptimizelyForcedDecision(variationKey2); + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext2, optimizelyForcedDecision2); + assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); + assertEquals(variationKey2, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext2).getVariationKey()); + + // Update first forcedDecision + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision2); + assertEquals(variationKey2, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); + + // Test to confirm decide uses proper FD + OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(decision.getReasons().contains( + String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey2, flagKey, ruleKey, userId) + )); + } + + @Test + public void setForcedDecisionWithoutRuleKeyTest() { + String flagKey = "55555"; + String variationKey = "33333"; + String updatedVariationKey = "55555"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + OptimizelyForcedDecision updatedOptimizelyForcedDecision = new OptimizelyForcedDecision(updatedVariationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); + + // Update forcedDecision + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, updatedOptimizelyForcedDecision); + assertEquals(updatedVariationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); + } + + @Test + public void getForcedVariationWithRuleKey() { + String flagKey = "55555"; + String ruleKey = "77777"; + String variationKey = "33333"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); + } + + @Test + public void failedGetForcedDecisionWithRuleKey() { + String flagKey = "55555"; + String invalidFlagKey = "11"; + String ruleKey = "77777"; + String variationKey = "33333"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyDecisionContext invalidOptimizelyDecisionContext = new OptimizelyDecisionContext(invalidFlagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertNull(optimizelyUserContext.getForcedDecision(invalidOptimizelyDecisionContext)); + } + + @Test + public void getForcedVariationWithoutRuleKey() { + String flagKey = "55555"; + String variationKey = "33333"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); + } + + @Test + public void failedGetForcedDecisionWithoutRuleKey() { + String flagKey = "55555"; + String invalidFlagKey = "11"; + String variationKey = "33333"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); + OptimizelyDecisionContext invalidOptimizelyDecisionContext = new OptimizelyDecisionContext(invalidFlagKey, null); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertNull(optimizelyUserContext.getForcedDecision(invalidOptimizelyDecisionContext)); + } + + @Test + public void removeForcedDecisionWithRuleKey() { + String flagKey = "55555"; + String ruleKey = "77777"; + String variationKey = "33333"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertTrue(optimizelyUserContext.removeForcedDecision(optimizelyDecisionContext)); + } + + @Test + public void removeForcedDecisionWithoutRuleKey() { + String flagKey = "55555"; + String variationKey = "33333"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertTrue(optimizelyUserContext.removeForcedDecision(optimizelyDecisionContext)); + } + + @Test + public void removeForcedDecisionWithNullRuleKeyAfterAddingWithRuleKey() { + String flagKey = "flag2"; + String ruleKey = "default-rollout-3045-20390585493"; + String variationKey = "variation2"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); + OptimizelyDecisionContext optimizelyDecisionContextNonNull = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertFalse(optimizelyUserContext.removeForcedDecision(optimizelyDecisionContextNonNull)); + } + + @Test + public void removeForcedDecisionWithIncorrectFlagKey() { + String flagKey = "55555"; + String variationKey = "variation2"; + String incorrectFlagKey = "flag1"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); + OptimizelyDecisionContext incorrectOptimizelyDecisionContext = new OptimizelyDecisionContext(incorrectFlagKey, null); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertFalse(optimizelyUserContext.removeForcedDecision(incorrectOptimizelyDecisionContext)); + } + + @Test + public void removeForcedDecisionWithIncorrectFlagKeyButSimilarRuleKey() { + String flagKey = "flag2"; + String incorrectFlagKey = "flag3"; + String ruleKey = "default-rollout-3045-20390585493"; + String variationKey = "variation2"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyDecisionContext similarOptimizelyDecisionContext = new OptimizelyDecisionContext(incorrectFlagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertFalse(optimizelyUserContext.removeForcedDecision(similarOptimizelyDecisionContext)); + } + + @Test + public void removeAllForcedDecisions() { + String flagKey = "55555"; + String ruleKey = "77777"; + String variationKey = "33333"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertTrue(optimizelyUserContext.removeAllForcedDecisions()); + } + + @Test + public void setForcedDecisionsAndCallDecide() { + String flagKey = "feature_2"; + String ruleKey = "exp_no_audience"; + String variationKey = "variation_with_traffic"; + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); + + // Test to confirm decide uses proper FD + OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertNotNull(decision); + assertTrue(decision.getReasons().contains( + String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) + )); + } + + /** + * ****************************************[START DECIDE TESTS WITH FDs]***************************************** + */ + @Test + public void setForcedDecisionsAndCallDecideFlagToDecision() { + String flagKey = "feature_1"; + String variationKey = "a"; + + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); + + optimizely.addDecisionNotificationHandler( + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); + + isListenerCalled = false; + + // Test to confirm decide uses proper FD + OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(isListenerCalled); + + String variationId = "10389729780"; + String experimentId = ""; + + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("") + .setRuleType("feature-test") + .setVariationKey(variationKey) + .setEnabled(true) + .build(); + + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); + + assertNotNull(decision); + assertTrue(decision.getReasons().contains( + String.format("Variation (%s) is mapped to flag (%s) and user (%s) in the forced decision map.", variationKey, flagKey, userId) + )); + } + + @Test + public void setForcedDecisionsAndCallDecideExperimentRuleToDecision() { + String flagKey = "feature_1"; + String ruleKey = "exp_with_audience"; + String variationKey = "a"; + + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); + + optimizely.addDecisionNotificationHandler( + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); + + isListenerCalled = false; + + // Test to confirm decide uses proper FD + OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(isListenerCalled); + + String variationId = "10389729780"; + String experimentId = "10390977673"; + + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap()); + + assertNotNull(decision); + assertTrue(decision.getReasons().contains( + String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) + )); + } + + @Test + public void setForcedDecisionsAndCallDecideDeliveryRuleToDecision() { + String flagKey = "feature_1"; + String ruleKey = "3332020515"; + String variationKey = "3324490633"; + + optimizely = new Optimizely.Builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + + OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( + optimizely, + userId, + Collections.emptyMap()); + + OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); + OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); + optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); + assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); + + optimizely.addDecisionNotificationHandler( + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); + + isListenerCalled = false; + + // Test to confirm decide uses proper FD + OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + + assertTrue(isListenerCalled); + + String variationId = "3324490633"; + String experimentId = "3332020515"; + + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap()); + + assertNotNull(decision); + assertTrue(decision.getReasons().contains( + String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) + )); + } + + /** + * ******************************************[END DECIDE TESTS WITH FDs]***************************************** + */ + + @Test + public void fetchQualifiedSegments() { + ODPEventManager mockODPEventManager = mock(ODPEventManager.class); + ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); + ODPManager mockODPManager = mock(ODPManager.class); + + Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); + Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); + + Optimizely optimizely = Optimizely.builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withODPManager(mockODPManager) + .build(); + + OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); + + assertTrue(userContext.fetchQualifiedSegments()); + verify(mockODPSegmentManager).getQualifiedSegments("test-user", Collections.emptyList()); + + assertTrue(userContext.fetchQualifiedSegments(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); + verify(mockODPSegmentManager).getQualifiedSegments("test-user", Collections.singletonList(ODPSegmentOption.RESET_CACHE)); + } + + @Test + public void fetchQualifiedSegmentsErrorWhenConfigIsInvalid() { + ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class); + Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(null); + ODPEventManager mockODPEventManager = mock(ODPEventManager.class); + ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); + ODPManager mockODPManager = mock(ODPManager.class); + + Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); + Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); + + Optimizely optimizely = Optimizely.builder() + .withConfigManager(mockProjectConfigManager) + .withODPManager(mockODPManager) + .build(); + + OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); + + assertFalse(userContext.fetchQualifiedSegments()); + logbackVerifier.expectMessage(Level.ERROR, "Optimizely instance is not valid, failing fetchQualifiedSegments call."); + } + + @Test + public void fetchQualifiedSegmentsError() { + Optimizely optimizely = Optimizely.builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); + + assertFalse(userContext.fetchQualifiedSegments()); + logbackVerifier.expectMessage(Level.ERROR, "Audience segments fetch failed (ODP is not enabled)."); + } + + @Test + public void fetchQualifiedSegmentsAsync() throws InterruptedException { + ODPEventManager mockODPEventManager = mock(ODPEventManager.class); + ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); + ODPManager mockODPManager = mock(ODPManager.class); + + doAnswer( + invocation -> { + ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(1, ODPSegmentManager.ODPSegmentFetchCallback.class); + callback.onCompleted(Arrays.asList("segment1", "segment2")); + return null; + } + ).when(mockODPSegmentManager).getQualifiedSegments(any(), (ODPSegmentManager.ODPSegmentFetchCallback) any(), any()); + Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); + Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); + + Optimizely optimizely = Optimizely.builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withODPManager(mockODPManager) + .build(); + + OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); + + CountDownLatch countDownLatch = new CountDownLatch(1); + userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { + assertTrue(isFetchSuccessful); + countDownLatch.countDown(); + }); + + countDownLatch.await(); + verify(mockODPSegmentManager).getQualifiedSegments(eq("test-user"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.emptyList())); + assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); + + // reset qualified segments + userContext.setQualifiedSegments(Collections.emptyList()); + CountDownLatch countDownLatch2 = new CountDownLatch(1); + userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { + assertTrue(isFetchSuccessful); + countDownLatch2.countDown(); + }, Collections.singletonList(ODPSegmentOption.RESET_CACHE)); + + countDownLatch2.await(); + verify(mockODPSegmentManager).getQualifiedSegments(eq("test-user"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); + assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); + } + + @Test + public void fetchQualifiedSegmentsAsyncWithVUID() throws InterruptedException { + ODPEventManager mockODPEventManager = mock(ODPEventManager.class); + ODPApiManager mockAPIManager = mock(ODPApiManager.class); + ODPSegmentManager mockODPSegmentManager = spy(new ODPSegmentManager(mockAPIManager)); + ODPManager mockODPManager = mock(ODPManager.class); + + doAnswer( + invocation -> { + ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(2, ODPSegmentManager.ODPSegmentFetchCallback.class); + callback.onCompleted(Arrays.asList("segment1", "segment2")); + return null; + } + ).when(mockODPSegmentManager).getQualifiedSegments(any(), eq("vuid_f6db3d60ba3a493d8e41bc995bb"), (ODPSegmentManager.ODPSegmentFetchCallback) any(), any()); + Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); + Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); + + Optimizely optimizely = Optimizely.builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withODPManager(mockODPManager) + .build(); + + OptimizelyUserContext userContext = optimizely.createUserContext("vuid_f6db3d60ba3a493d8e41bc995bb"); + + CountDownLatch countDownLatch = new CountDownLatch(1); + userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { + assertTrue(isFetchSuccessful); + countDownLatch.countDown(); + }); + + countDownLatch.await(); + verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.VUID), eq("vuid_f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.emptyList())); + assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); + + // reset qualified segments + userContext.setQualifiedSegments(Collections.emptyList()); + CountDownLatch countDownLatch2 = new CountDownLatch(1); + userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { + assertTrue(isFetchSuccessful); + countDownLatch2.countDown(); + }, Collections.singletonList(ODPSegmentOption.RESET_CACHE)); + + countDownLatch2.await(); + verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.VUID), eq("vuid_f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); + assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); + } + + @Test + public void fetchQualifiedSegmentsAsyncWithUserID() throws InterruptedException { + ODPEventManager mockODPEventManager = mock(ODPEventManager.class); + ODPApiManager mockAPIManager = mock(ODPApiManager.class); + ODPSegmentManager mockODPSegmentManager = spy(new ODPSegmentManager(mockAPIManager)); + ODPManager mockODPManager = mock(ODPManager.class); + + doAnswer( + invocation -> { + ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(2, ODPSegmentManager.ODPSegmentFetchCallback.class); + callback.onCompleted(Arrays.asList("segment1", "segment2")); + return null; + } + ).when(mockODPSegmentManager).getQualifiedSegments(any(), eq("f6db3d60ba3a493d8e41bc995bb"), (ODPSegmentManager.ODPSegmentFetchCallback) any(), any()); + Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); + Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); + + Optimizely optimizely = Optimizely.builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withODPManager(mockODPManager) + .build(); + + OptimizelyUserContext userContext = optimizely.createUserContext("f6db3d60ba3a493d8e41bc995bb"); + + CountDownLatch countDownLatch = new CountDownLatch(1); + userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { + assertTrue(isFetchSuccessful); + countDownLatch.countDown(); + }); + + countDownLatch.await(); + verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.FS_USER_ID), eq("f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.emptyList())); + assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); + + // reset qualified segments + userContext.setQualifiedSegments(Collections.emptyList()); + CountDownLatch countDownLatch2 = new CountDownLatch(1); + userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { + assertTrue(isFetchSuccessful); + countDownLatch2.countDown(); + }, Collections.singletonList(ODPSegmentOption.RESET_CACHE)); + + countDownLatch2.await(); + verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.FS_USER_ID), eq("f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); + assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); + } + + @Test + public void fetchQualifiedSegmentsAsyncError() throws InterruptedException { + Optimizely optimizely = Optimizely.builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); + + OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); + + CountDownLatch countDownLatch = new CountDownLatch(1); + userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { + assertFalse(isFetchSuccessful); + countDownLatch.countDown(); + }); + + countDownLatch.await(); + assertEquals(null, userContext.getQualifiedSegments()); + logbackVerifier.expectMessage(Level.ERROR, "Audience segments fetch failed (ODP is not enabled)."); + } + + @Test + public void fetchQualifiedSegmentsAsyncErrorWhenConfigIsInvalid() throws InterruptedException { + ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class); + Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(null); + ODPEventManager mockODPEventManager = mock(ODPEventManager.class); + ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); + ODPManager mockODPManager = mock(ODPManager.class); + + Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); + Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); + + Optimizely optimizely = Optimizely.builder() + .withConfigManager(mockProjectConfigManager) + .withODPManager(mockODPManager) + .build(); + + OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); + + CountDownLatch countDownLatch = new CountDownLatch(1); + userContext.fetchQualifiedSegments((Boolean isFetchSuccessful) -> { + assertFalse(isFetchSuccessful); + countDownLatch.countDown(); + }); + + countDownLatch.await(); + assertEquals(null, userContext.getQualifiedSegments()); + logbackVerifier.expectMessage(Level.ERROR, "Optimizely instance is not valid, failing fetchQualifiedSegments call."); + } + + @Test + public void identifyUserErrorWhenConfigIsInvalid() { + ODPEventManager mockODPEventManager = mock(ODPEventManager.class); + ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); + ODPManager mockODPManager = mock(ODPManager.class); + ProjectConfigManager mockProjectConfigManager = mock(ProjectConfigManager.class); + Mockito.when(mockProjectConfigManager.getConfig()).thenReturn(null); + Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); + Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); + + Optimizely optimizely = Optimizely.builder() + .withConfigManager(mockProjectConfigManager) + .withODPManager(mockODPManager) + .build(); + + optimizely.createUserContext("test-user"); + verify(mockODPEventManager, never()).identifyUser("test-user"); + Mockito.reset(mockODPEventManager); + + logbackVerifier.expectMessage(Level.ERROR, "Optimizely instance is not valid, failing identifyUser call."); + } + + @Test + public void identifyUser() { + ODPEventManager mockODPEventManager = mock(ODPEventManager.class); + ODPSegmentManager mockODPSegmentManager = mock(ODPSegmentManager.class); + ODPManager mockODPManager = mock(ODPManager.class); + + Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); + Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); + + Optimizely optimizely = Optimizely.builder() + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withODPManager(mockODPManager) + .build(); + + OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); + verify(mockODPEventManager).identifyUser("test-user"); + + Mockito.reset(mockODPEventManager); + OptimizelyUserContext userContextClone = userContext.copy(); + + // identifyUser should not be called the new userContext is created through copy + verify(mockODPEventManager, never()).identifyUser("test-user"); + + assertNotSame(userContextClone, userContext); + } + + // utils + Map createUserProfileMap(String experimentId, String variationId) { + Map userProfileMap = new HashMap(); + userProfileMap.put(UserProfileService.userIdKey, userId); + + Map decisionMap = new HashMap(1); + decisionMap.put(UserProfileService.variationIdKey, variationId); + + Map> decisionsMap = new HashMap>(); + decisionsMap.put(experimentId, decisionMap); + userProfileMap.put(UserProfileService.experimentBucketMapKey, decisionsMap); + + return userProfileMap; + } + + void setAudienceForFeatureTest(String flagKey, String audienceId) throws ConfigParseException { + Experiment experiment = getSpyExperiment(flagKey); + when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); + addSpyExperiment(experiment); + } + + Experiment getSpyExperiment(String flagKey) { + setMockConfig(); + String experimentId = config.getFeatureKeyMapping().get(flagKey).getExperimentIds().get(0); + return spy(experimentIdMapping.get(experimentId)); + } + + FeatureFlag getSpyFeatureFlag(String flagKey) { + setMockConfig(); + return spy(config.getFeatureKeyMapping().get(flagKey)); + } + + Group getSpyGroup(String groupId) { + setMockConfig(); + return spy(groupIdMapping.get(groupId)); + } + + void addSpyExperiment(Experiment experiment) { + experimentIdMapping.put(experiment.getId(), experiment); + when(config.getExperimentIdMapping()).thenReturn(experimentIdMapping); + } + + void addSpyFeatureFlag(FeatureFlag flag) { + featureKeyMapping.put(flag.getKey(), flag); + when(config.getFeatureKeyMapping()).thenReturn(featureKeyMapping); + } + + void addSpyGroup(Group group) { + groupIdMapping.put(group.getId(), group); + when(config.getGroupIdMapping()).thenReturn(groupIdMapping); + } + + void setMockConfig() { + if (config != null) { + return; + } + + ProjectConfig configReal = null; + try { + configReal = new DatafileProjectConfig.Builder().withDatafile(datafile).build(); + config = spy(configReal); + optimizely = Optimizely.builder().withConfig(config).build(); + experimentIdMapping = new HashMap<>(config.getExperimentIdMapping()); + groupIdMapping = new HashMap<>(config.getGroupIdMapping()); + featureKeyMapping = new HashMap<>(config.getFeatureKeyMapping()); + } catch (ConfigParseException e) { + fail("ProjectConfig build failed"); + } + } + + OptimizelyDecision callDecideWithIncludeReasons(String flagKey, Map attributes) { + OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); + return user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); + } + + OptimizelyDecision callDecideWithIncludeReasons(String flagKey) { + return callDecideWithIncludeReasons(flagKey, Collections.emptyMap()); } } From 5a1cca2249794b61d5c9895767854497d854421f Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Mon, 11 Aug 2025 19:53:31 +0600 Subject: [PATCH 14/17] feat: add method for handling decision notifications with holdouts - Implement createOptimizelyWithHoldouts method to create Optimizely instance with holdouts - Add test for decisionNotification with holdout scenario - Update decide_for_keys_with_holdout test for holdout variations - Implement decide_all_with_holdout test for multiple flag keys with holdouts --- .../ab/OptimizelyUserContextTest.java | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index c2ec1c580..130f52e57 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -57,6 +57,9 @@ import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.*; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; public class OptimizelyUserContextTest { @@ -2074,4 +2077,188 @@ OptimizelyDecision callDecideWithIncludeReasons(String flagKey) { return callDecideWithIncludeReasons(flagKey, Collections.emptyMap()); } + private Optimizely createOptimizelyWithHoldouts() throws Exception { + String holdoutDatafile = com.google.common.io.Resources.toString( + com.google.common.io.Resources.getResource("config/holdouts-project-config.json"), + com.google.common.base.Charsets.UTF_8 + ); + return new Optimizely.Builder().withDatafile(holdoutDatafile).withEventProcessor(new ForwardingEventProcessor(eventHandler, null)).build(); + } + + @Test + public void decisionNotification_with_holdout() throws Exception { + // Use holdouts datafile + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String flagKey = "boolean_feature"; + String userId = "user123"; + String ruleKey = "basic_holdout"; // holdout rule key + String variationKey = "ho_off_key"; // holdout (off) variation key + String experimentId = "10075323428"; // holdout experiment id in holdouts-project-config.json + String variationId = "$opt_dummy_variation_id";// dummy variation id used for holdout impressions + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (" + ruleKey + ")."; + + Map attrs = new HashMap<>(); + attrs.put("$opt_bucketing_id", "ppid160000"); // deterministic bucketing into basic_holdout + attrs.put("nationality", "English"); // non-reserved attribute should appear in impression & notification + + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + + // Register notification handler similar to decisionNotification test + isListenerCalled = false; + optWithHoldout.addDecisionNotificationHandler(decisionNotification -> { + Assert.assertEquals(NotificationCenter.DecisionNotificationType.FLAG.toString(), decisionNotification.getType()); + Assert.assertEquals(userId, decisionNotification.getUserId()); + + Assert.assertEquals(attrs, decisionNotification.getAttributes()); + + Map info = decisionNotification.getDecisionInfo(); + Assert.assertEquals(flagKey, info.get(FLAG_KEY)); + Assert.assertEquals(variationKey, info.get(VARIATION_KEY)); + Assert.assertEquals(false, info.get(ENABLED)); + Assert.assertEquals(ruleKey, info.get(RULE_KEY)); + Assert.assertEquals(experimentId, info.get(EXPERIMENT_ID)); + Assert.assertEquals(variationId, info.get(VARIATION_ID)); + // Variables should be empty because feature is disabled by holdout + Assert.assertTrue(((Map) info.get(VARIABLES)).isEmpty()); + // Event should be dispatched (no DISABLE_DECISION_EVENT option) + Assert.assertEquals(true, info.get(DECISION_EVENT_DISPATCHED)); + + @SuppressWarnings("unchecked") + List reasons = (List) info.get(REASONS); + Assert.assertTrue("Expected holdout reason present", reasons.contains(expectedReason)); + isListenerCalled = true; + }); + + // Execute decision with INCLUDE_REASONS so holdout reason is present + OptimizelyDecision decision = user.decide(flagKey, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); + assertTrue(isListenerCalled); + + // Sanity checks on returned decision + assertEquals(variationKey, decision.getVariationKey()); + assertFalse(decision.getEnabled()); + assertTrue(decision.getReasons().contains(expectedReason)); + + // Impression expectation (nationality only) + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey(ruleKey) + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + eventHandler.expectImpression(experimentId, variationId, userId, Collections.singletonMap("nationality", "English"), metadata); + + // Log expectation (reuse existing pattern) + logbackVerifier.expectMessage(Level.INFO, expectedReason); + } + + @Test + public void decide_for_keys_with_holdout() throws Exception { + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String userId = "user123"; + Map attrs = new HashMap<>(); + attrs.put("$opt_bucketing_id", "ppid160000"); + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + + List flagKeys = Arrays.asList( + "boolean_feature", // previously validated basic_holdout membership + "double_single_variable_feature", // also subject to global/basic holdout + "integer_single_variable_feature" // also subject to global/basic holdout + ); + + Map decisions = user.decideForKeys(flagKeys, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); + assertEquals(3, decisions.size()); + + String holdoutExperimentId = "10075323428"; // basic_holdout id + String variationId = "$opt_dummy_variation_id"; + String variationKey = "ho_off_key"; + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (basic_holdout)."; + + for (String flagKey : flagKeys) { + OptimizelyDecision d = decisions.get(flagKey); + assertNotNull(d); + assertEquals(flagKey, d.getFlagKey()); + assertEquals(variationKey, d.getVariationKey()); + assertFalse(d.getEnabled()); + assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("basic_holdout") + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + // attributes map expected empty (reserved $opt_ attribute filtered out) + eventHandler.expectImpression(holdoutExperimentId, variationId, userId, Collections.emptyMap(), metadata); + } + + // At least one log message confirming holdout membership + logbackVerifier.expectMessage(Level.INFO, expectedReason); + } + + @Test + public void decide_all_with_holdout() throws Exception { + + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String userId = "user123"; + Map attrs = new HashMap<>(); + // ppid120000 buckets user into holdout_included_flags + attrs.put("$opt_bucketing_id", "ppid120000"); + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + + // All flag keys present in holdouts-project-config.json + List allFlagKeys = Arrays.asList( + "boolean_feature", + "double_single_variable_feature", + "integer_single_variable_feature", + "boolean_single_variable_feature", + "string_single_variable_feature", + "multi_variate_feature", + "multi_variate_future_feature", + "mutex_group_feature" + ); + + // Flags INCLUDED in holdout_included_flags (only these should be holdout decisions) + List includedInHoldout = Arrays.asList( + "boolean_feature", + "double_single_variable_feature", + "integer_single_variable_feature" + ); + + Map decisions = user.decideAll(Arrays.asList( + OptimizelyDecideOption.INCLUDE_REASONS, + OptimizelyDecideOption.DISABLE_DECISION_EVENT + )); + assertEquals(allFlagKeys.size(), decisions.size()); + + String holdoutExperimentId = "1007543323427"; // holdout_included_flags id + String variationId = "$opt_dummy_variation_id"; + String variationKey = "ho_off_key"; + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags)."; + + int holdoutCount = 0; + for (String flagKey : allFlagKeys) { + OptimizelyDecision d = decisions.get(flagKey); + assertNotNull("Missing decision for flag " + flagKey, d); + if (includedInHoldout.contains(flagKey)) { + // Should be holdout decision + assertEquals(variationKey, d.getVariationKey()); + assertFalse(d.getEnabled()); + assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("holdout_included_flags") + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + holdoutCount++; + } else { + // Should NOT be a holdout decision + assertFalse("Non-included flag should not have holdout reason: " + flagKey, d.getReasons().contains(expectedReason)); + } + } + assertEquals("Expected exactly the included flags to be in holdout", includedInHoldout.size(), holdoutCount); + logbackVerifier.expectMessage(Level.INFO, expectedReason); + } } From 3ea6d9e55c7cbc967e512d7698cdf7e2ff6774ac Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Mon, 11 Aug 2025 20:02:46 +0600 Subject: [PATCH 15/17] Revert original test cases --- .../ab/OptimizelyUserContextTest.java | 901 +++++++----------- 1 file changed, 362 insertions(+), 539 deletions(-) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index 130f52e57..bb2d36192 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -57,13 +57,9 @@ import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.*; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; public class OptimizelyUserContextTest { - @Rule public EventHandlerRule eventHandler = new EventHandlerRule(); @@ -85,8 +81,8 @@ public void setUp() throws Exception { datafile = Resources.toString(Resources.getResource("config/decide-project-config.json"), Charsets.UTF_8); optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .build(); + .withDatafile(datafile) + .build(); } @Test @@ -175,12 +171,13 @@ public void setAttribute_nullValue() { } // decide + @Test public void decide_featureTest() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_2"; String experimentKey = "exp_no_audience"; @@ -201,21 +198,21 @@ public void decide_featureTest() { assertTrue(decision.getReasons().isEmpty()); DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey(experimentKey) - .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) - .setVariationKey(variationKey) - .setEnabled(true) - .build(); + .setFlagKey(flagKey) + .setRuleKey(experimentKey) + .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) + .setVariationKey(variationKey) + .setEnabled(true) + .build(); eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); } @Test public void decide_rollout() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_1"; String experimentKey = "18322080788"; @@ -236,21 +233,21 @@ public void decide_rollout() { assertTrue(decision.getReasons().isEmpty()); DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey(experimentKey) - .setRuleType(FeatureDecision.DecisionSource.ROLLOUT.toString()) - .setVariationKey(variationKey) - .setEnabled(true) - .build(); + .setFlagKey(flagKey) + .setRuleKey(experimentKey) + .setRuleType(FeatureDecision.DecisionSource.ROLLOUT.toString()) + .setVariationKey(variationKey) + .setEnabled(true) + .build(); eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); } @Test public void decide_nullVariation() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_3"; OptimizelyJSON variablesExpected = new OptimizelyJSON(Collections.emptyMap()); @@ -267,22 +264,23 @@ public void decide_nullVariation() { assertTrue(decision.getReasons().isEmpty()); DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("") - .setRuleType(FeatureDecision.DecisionSource.ROLLOUT.toString()) - .setVariationKey("") - .setEnabled(false) - .build(); + .setFlagKey(flagKey) + .setRuleKey("") + .setRuleType(FeatureDecision.DecisionSource.ROLLOUT.toString()) + .setVariationKey("") + .setEnabled(false) + .build(); eventHandler.expectImpression(null, "", userId, Collections.emptyMap(), metadata); } // decideAll + @Test public void decideAll_oneFlag() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_2"; String experimentKey = "exp_no_audience"; @@ -300,22 +298,22 @@ public void decideAll_oneFlag() { OptimizelyDecision decision = decisions.get(flagKey); OptimizelyDecision expDecision = new OptimizelyDecision( - variationKey, - true, - variablesExpected, - experimentKey, - flagKey, - user, - Collections.emptyList()); + variationKey, + true, + variablesExpected, + experimentKey, + flagKey, + user, + Collections.emptyList()); assertEquals(decision, expDecision); DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey(experimentKey) - .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) - .setVariationKey(variationKey) - .setEnabled(true) - .build(); + .setFlagKey(flagKey) + .setRuleKey(experimentKey) + .setRuleType(FeatureDecision.DecisionSource.FEATURE_TEST.toString()) + .setVariationKey(variationKey) + .setEnabled(true) + .build(); eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); } @@ -334,23 +332,23 @@ public void decideAll_twoFlags() { assertTrue(decisions.size() == 2); assertEquals( - decisions.get(flagKey1), - new OptimizelyDecision("a", - true, - variablesExpected1, - "exp_with_audience", - flagKey1, - user, - Collections.emptyList())); + decisions.get(flagKey1), + new OptimizelyDecision("a", + true, + variablesExpected1, + "exp_with_audience", + flagKey1, + user, + Collections.emptyList())); assertEquals( - decisions.get(flagKey2), - new OptimizelyDecision("variation_with_traffic", - true, - variablesExpected2, - "exp_no_audience", - flagKey2, - user, - Collections.emptyList())); + decisions.get(flagKey2), + new OptimizelyDecision("variation_with_traffic", + true, + variablesExpected2, + "exp_no_audience", + flagKey2, + user, + Collections.emptyList())); } @Test @@ -358,9 +356,9 @@ public void decideAll_allFlags() { EventProcessor mockEventProcessor = mock(EventProcessor.class); optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(mockEventProcessor) - .build(); + .withDatafile(datafile) + .withEventProcessor(mockEventProcessor) + .build(); String flagKey1 = "feature_1"; String flagKey2 = "feature_2"; @@ -376,35 +374,35 @@ public void decideAll_allFlags() { assertEquals(decisions.size(), 3); assertEquals( - decisions.get(flagKey1), - new OptimizelyDecision( - "a", - true, - variablesExpected1, - "exp_with_audience", - flagKey1, - user, - Collections.emptyList())); + decisions.get(flagKey1), + new OptimizelyDecision( + "a", + true, + variablesExpected1, + "exp_with_audience", + flagKey1, + user, + Collections.emptyList())); assertEquals( - decisions.get(flagKey2), - new OptimizelyDecision( - "variation_with_traffic", - true, - variablesExpected2, - "exp_no_audience", - flagKey2, - user, - Collections.emptyList())); + decisions.get(flagKey2), + new OptimizelyDecision( + "variation_with_traffic", + true, + variablesExpected2, + "exp_no_audience", + flagKey2, + user, + Collections.emptyList())); assertEquals( - decisions.get(flagKey3), - new OptimizelyDecision( - null, - false, - variablesExpected3, - null, - flagKey3, - user, - Collections.emptyList())); + decisions.get(flagKey3), + new OptimizelyDecision( + null, + false, + variablesExpected3, + null, + flagKey3, + user, + Collections.emptyList())); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ImpressionEvent.class); verify(mockEventProcessor, times(3)).process(argumentCaptor.capture()); @@ -416,6 +414,7 @@ public void decideAll_allFlags() { assertEquals(sentEvents.get(0).getVariationKey(), "a"); assertEquals(sentEvents.get(0).getUserContext().getUserId(), userId); + assertEquals(sentEvents.get(1).getExperimentKey(), "exp_no_audience"); assertEquals(sentEvents.get(1).getVariationKey(), "variation_with_traffic"); assertEquals(sentEvents.get(1).getUserContext().getUserId(), userId); @@ -429,9 +428,9 @@ public void decideForKeys_ups_batching() throws Exception { UserProfileService ups = mock(UserProfileService.class); optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withUserProfileService(ups) - .build(); + .withDatafile(datafile) + .withUserProfileService(ups) + .build(); String flagKey1 = "feature_1"; String flagKey2 = "feature_2"; @@ -440,13 +439,14 @@ public void decideForKeys_ups_batching() throws Exception { OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); Map decisions = user.decideForKeys(Arrays.asList( - flagKey1, flagKey2, flagKey3 + flagKey1, flagKey2, flagKey3 )); assertEquals(decisions.size(), 3); ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Map.class); + verify(ups, times(1)).lookup(userId); verify(ups, times(1)).save(argumentCaptor.capture()); @@ -461,9 +461,9 @@ public void decideAll_ups_batching() throws Exception { UserProfileService ups = mock(UserProfileService.class); optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withUserProfileService(ups) - .build(); + .withDatafile(datafile) + .withUserProfileService(ups) + .build(); Map attributes = Collections.singletonMap("gender", "f"); @@ -474,6 +474,7 @@ public void decideAll_ups_batching() throws Exception { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Map.class); + verify(ups, times(1)).lookup(userId); verify(ups, times(1)).save(argumentCaptor.capture()); @@ -494,24 +495,25 @@ public void decideAll_allFlags_enabledFlagsOnly() { assertTrue(decisions.size() == 2); assertEquals( - decisions.get(flagKey1), - new OptimizelyDecision( - "a", - true, - variablesExpected1, - "exp_with_audience", - flagKey1, - user, - Collections.emptyList())); + decisions.get(flagKey1), + new OptimizelyDecision( + "a", + true, + variablesExpected1, + "exp_with_audience", + flagKey1, + user, + Collections.emptyList())); } // trackEvent + @Test public void trackEvent() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); String eventKey = "event1"; @@ -525,9 +527,9 @@ public void trackEvent() { @Test public void trackEvent_noEventTags() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); String eventKey = "event1"; @@ -540,9 +542,9 @@ public void trackEvent_noEventTags() { @Test public void trackEvent_emptyAttributes() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String eventKey = "event1"; Map eventTags = Collections.singletonMap("name", "carrot"); @@ -553,12 +555,13 @@ public void trackEvent_emptyAttributes() { } // send events + @Test public void decide_sendEvent() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_2"; String variationKey = "variation_with_traffic"; @@ -576,9 +579,9 @@ public void decide_sendEvent() { @Test public void decide_doNotSendEvent_withOption() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); String flagKey = "feature_2"; @@ -593,18 +596,18 @@ public void decide_doNotSendEvent_withOption() { @Test public void decide_sendEvent_featureTest_withSendFlagDecisionsOn() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); String flagKey = "feature_2"; String experimentId = "10420810910"; @@ -619,18 +622,18 @@ public void decide_sendEvent_featureTest_withSendFlagDecisionsOn() { @Test public void decide_sendEvent_rollout_withSendFlagDecisionsOn() { optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); String flagKey = "feature_3"; String experimentId = null; @@ -646,18 +649,18 @@ public void decide_sendEvent_rollout_withSendFlagDecisionsOn() { public void decide_sendEvent_featureTest_withSendFlagDecisionsOff() { String datafileWithSendFlagDecisionsOff = datafile.replace("\"sendFlagDecisions\": true", "\"sendFlagDecisions\": false"); optimizely = new Optimizely.Builder() - .withDatafile(datafileWithSendFlagDecisionsOff) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafileWithSendFlagDecisionsOff) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); String flagKey = "feature_2"; String experimentId = "10420810910"; @@ -673,18 +676,18 @@ public void decide_sendEvent_featureTest_withSendFlagDecisionsOff() { public void decide_sendEvent_rollout_withSendFlagDecisionsOff() { String datafileWithSendFlagDecisionsOff = datafile.replace("\"sendFlagDecisions\": true", "\"sendFlagDecisions\": false"); optimizely = new Optimizely.Builder() - .withDatafile(datafileWithSendFlagDecisionsOff) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafileWithSendFlagDecisionsOff) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); Map attributes = Collections.singletonMap("gender", "f"); OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), false); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), false); + isListenerCalled = true; + }); String flagKey = "feature_3"; isListenerCalled = false; @@ -695,6 +698,7 @@ public void decide_sendEvent_rollout_withSendFlagDecisionsOff() { } // notifications + @Test public void decisionNotification() { String flagKey = "feature_2"; @@ -720,13 +724,13 @@ public void decisionNotification() { OptimizelyUserContext user = optimizely.createUserContext(userId, attributes); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getType(), NotificationCenter.DecisionNotificationType.FLAG.toString()); - Assert.assertEquals(decisionNotification.getUserId(), userId); - Assert.assertEquals(decisionNotification.getAttributes(), attributes); - Assert.assertEquals(decisionNotification.getDecisionInfo(), testDecisionInfoMap); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getType(), NotificationCenter.DecisionNotificationType.FLAG.toString()); + Assert.assertEquals(decisionNotification.getUserId(), userId); + Assert.assertEquals(decisionNotification.getAttributes(), attributes); + Assert.assertEquals(decisionNotification.getDecisionInfo(), testDecisionInfoMap); + isListenerCalled = true; + }); isListenerCalled = false; testDecisionInfoMap.put(DECISION_EVENT_DISPATCHED, true); @@ -740,6 +744,7 @@ public void decisionNotification() { } // options + @Test public void decideOptions_bypassUPS() throws Exception { String flagKey = "feature_2"; // embedding experiment: "exp_no_audience" @@ -753,9 +758,9 @@ public void decideOptions_bypassUPS() throws Exception { when(ups.lookup(userId)).thenReturn(createUserProfileMap(experimentId, variationId2)); optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withUserProfileService(ups) - .build(); + .withDatafile(datafile) + .withUserProfileService(ups) + .build(); OptimizelyUserContext user = optimizely.createUserContext(userId); OptimizelyDecision decision = user.decide(flagKey); @@ -813,13 +818,13 @@ public void decideOptions_enabledFlagsOnly() { @Test public void decideOptions_defaultDecideOptions() { List options = Arrays.asList( - OptimizelyDecideOption.EXCLUDE_VARIABLES + OptimizelyDecideOption.EXCLUDE_VARIABLES ); optimizely = Optimizely.builder() - .withDatafile(datafile) - .withDefaultDecideOptions(options) - .build(); + .withDatafile(datafile) + .withDefaultDecideOptions(options) + .build(); String flagKey = "feature_1"; OptimizelyUserContext user = optimizely.createUserContext(userId); @@ -836,6 +841,7 @@ public void decideOptions_defaultDecideOptions() { } // errors + @Test public void decide_sdkNotReady() { String flagKey = "feature_1"; @@ -893,24 +899,25 @@ public void decideAll_errorDecisionIncluded() { assertEquals(decisions.size(), 2); assertEquals( - decisions.get(flagKey1), - new OptimizelyDecision( - "variation_with_traffic", - true, - variablesExpected1, - "exp_no_audience", - flagKey1, - user, - Collections.emptyList())); + decisions.get(flagKey1), + new OptimizelyDecision( + "variation_with_traffic", + true, + variablesExpected1, + "exp_no_audience", + flagKey1, + user, + Collections.emptyList())); assertEquals( - decisions.get(flagKey2), - OptimizelyDecision.newErrorDecision( - flagKey2, - user, - DecisionMessage.FLAG_KEY_INVALID.reason(flagKey2))); + decisions.get(flagKey2), + OptimizelyDecision.newErrorDecision( + flagKey2, + user, + DecisionMessage.FLAG_KEY_INVALID.reason(flagKey2))); } // reasons (errors) + @Test public void decideReasons_sdkNotReady() { String flagKey = "feature_1"; @@ -950,6 +957,7 @@ public void decideReasons_variableValueInvalid() { } // reasons (infos with includeReasons) + @Test public void decideReasons_experimentNotRunning() { String flagKey = "feature_1"; @@ -960,7 +968,7 @@ public void decideReasons_experimentNotRunning() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); assertTrue(decision.getReasons().contains( - String.format("Experiment \"exp_with_audience\" is not running.") + String.format("Experiment \"exp_with_audience\" is not running.") )); } @@ -976,15 +984,15 @@ public void decideReasons_gotVariationFromUserProfile() throws Exception { when(ups.lookup(userId)).thenReturn(createUserProfileMap(experimentId, variationId2)); optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withUserProfileService(ups) - .build(); + .withDatafile(datafile) + .withUserProfileService(ups) + .build(); OptimizelyUserContext user = optimizely.createUserContext(userId); OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format("Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", variationKey2, experimentKey, userId) + String.format("Returning previously activated variation \"%s\" of experiment \"%s\" for user \"%s\" from user profile.", variationKey2, experimentKey, userId) )); } @@ -999,7 +1007,7 @@ public void decideReasons_forcedVariationFound() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); assertTrue(decision.getReasons().contains( - String.format("User \"%s\" is forced in variation \"%s\".", userId, variationKey) + String.format("User \"%s\" is forced in variation \"%s\".", userId, variationKey) )); } @@ -1014,7 +1022,7 @@ public void decideReasons_forcedVariationFoundButInvalid() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); assertTrue(decision.getReasons().contains( - String.format("Variation \"%s\" is not in the datafile. Not activating user \"%s\".", variationKey, userId) + String.format("Variation \"%s\" is not in the datafile. Not activating user \"%s\".", variationKey, userId) )); } @@ -1027,7 +1035,7 @@ public void decideReasons_userMeetsConditionsForTargetingRule() { OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) + String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) )); } @@ -1040,7 +1048,7 @@ public void decideReasons_userDoesntMeetConditionsForTargetingRule() { OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format("User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, 1) + String.format("User \"%s\" does not meet conditions for targeting rule \"%d\".", userId, 1) )); } @@ -1053,7 +1061,7 @@ public void decideReasons_userBucketedIntoTargetingRule() { OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) + String.format("The user \"%s\" was bucketed into a rollout for feature flag \"%s\".", userId, flagKey) )); } @@ -1066,7 +1074,7 @@ public void decideReasons_userBucketedIntoEveryoneTargetingRule() { OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format("User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId) + String.format("User \"%s\" meets conditions for targeting rule \"Everyone Else\".", userId) )); } @@ -1080,7 +1088,7 @@ public void decideReasons_userNotBucketedIntoTargetingRule() { OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", userId, experimentKey) + String.format("User with bucketingId \"%s\" is not in any variation of experiment \"%s\".", userId, experimentKey) )); } @@ -1094,7 +1102,7 @@ public void decideReasons_userBucketedIntoVariationInExperiment() { OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", userId, variationKey, experimentKey) + String.format("User with bucketingId \"%s\" is in variation \"%s\" of experiment \"%s\".", userId, variationKey, experimentKey) )); } @@ -1108,7 +1116,7 @@ public void decideReasons_userNotBucketedIntoVariation() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"%s\" is not in any variation of experiment \"exp_no_audience\".", userId) + String.format("User with bucketingId \"%s\" is not in any variation of experiment \"exp_no_audience\".", userId) )); } @@ -1123,7 +1131,7 @@ public void decideReasons_userBucketedIntoExperimentInGroup() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"tester\" is in experiment \"group_exp_1\" of group 13142870430.") + String.format("User with bucketingId \"tester\" is in experiment \"group_exp_1\" of group 13142870430.") )); } @@ -1138,7 +1146,7 @@ public void decideReasons_userNotBucketedIntoExperimentInGroup() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"tester\" is not in experiment \"group_exp_2\" of group 13142870430.") + String.format("User with bucketingId \"tester\" is not in experiment \"group_exp_2\" of group 13142870430.") )); } @@ -1158,7 +1166,7 @@ public void decideReasons_userNotBucketedIntoAnyExperimentInGroup() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); assertTrue(decision.getReasons().contains( - String.format("User with bucketingId \"tester\" is not in any experiment of group 13142870430.") + String.format("User with bucketingId \"tester\" is not in any experiment of group 13142870430.") )); } @@ -1171,7 +1179,7 @@ public void decideReasons_userNotInExperiment() { OptimizelyDecision decision = user.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format("User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experimentKey) + String.format("User \"%s\" does not meet conditions to be in experiment \"%s\".", userId, experimentKey) )); } @@ -1186,7 +1194,7 @@ public void decideReasons_conditionNoMatchingAudience() throws ConfigParseExcept OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) )); } @@ -1201,7 +1209,7 @@ public void decideReasons_evaluateAttributeInvalidType() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("country", 25)); assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) )); } @@ -1213,10 +1221,10 @@ public void decideReasons_evaluateAttributeValueOutOfRange() { Experiment experiment = getSpyExperiment(flagKey); when(experiment.getAudienceIds()).thenReturn(Arrays.asList(audienceId)); addSpyExperiment(experiment); - OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", (float) Math.pow(2, 54))); + OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", (float)Math.pow(2, 54))); assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) )); } @@ -1231,7 +1239,7 @@ public void decideReasons_userAttributeInvalidType() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) )); } @@ -1246,7 +1254,7 @@ public void decideReasons_userAttributeInvalidMatch() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) )); } @@ -1261,7 +1269,7 @@ public void decideReasons_userAttributeNilValue() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey, Collections.singletonMap("age", 25)); assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) )); } @@ -1276,7 +1284,7 @@ public void decideReasons_missingAttributeValue() { OptimizelyDecision decision = callDecideWithIncludeReasons(flagKey); assertTrue(decision.getReasons().contains( - String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) + String.format("Audiences for experiment \"%s\" collectively evaluated to null.", experiment.getKey()) )); } @@ -1286,9 +1294,9 @@ public void setForcedDecisionWithRuleKeyTest() { String ruleKey = "77777"; String variationKey = "33333"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); optimizelyUserContext.setForcedDecision(optimizelyDecisionContext, optimizelyForcedDecision); @@ -1304,9 +1312,9 @@ public void setForcedDecisionsWithRuleKeyTest() { String variationKey = "33333"; String variationKey2 = "variation_with_traffic"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1325,7 +1333,7 @@ public void setForcedDecisionsWithRuleKeyTest() { OptimizelyDecision decision = optimizelyUserContext.decide(flagKey, Arrays.asList(OptimizelyDecideOption.INCLUDE_REASONS)); assertTrue(decision.getReasons().contains( - String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey2, flagKey, ruleKey, userId) + String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey2, flagKey, ruleKey, userId) )); } @@ -1335,9 +1343,9 @@ public void setForcedDecisionWithoutRuleKeyTest() { String variationKey = "33333"; String updatedVariationKey = "55555"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1351,15 +1359,16 @@ public void setForcedDecisionWithoutRuleKeyTest() { assertEquals(updatedVariationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); } + @Test public void getForcedVariationWithRuleKey() { String flagKey = "55555"; String ruleKey = "77777"; String variationKey = "33333"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1375,9 +1384,9 @@ public void failedGetForcedDecisionWithRuleKey() { String ruleKey = "77777"; String variationKey = "33333"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); OptimizelyDecisionContext invalidOptimizelyDecisionContext = new OptimizelyDecisionContext(invalidFlagKey, ruleKey); @@ -1392,9 +1401,9 @@ public void getForcedVariationWithoutRuleKey() { String flagKey = "55555"; String variationKey = "33333"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1403,15 +1412,16 @@ public void getForcedVariationWithoutRuleKey() { assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); } + @Test public void failedGetForcedDecisionWithoutRuleKey() { String flagKey = "55555"; String invalidFlagKey = "11"; String variationKey = "33333"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); OptimizelyDecisionContext invalidOptimizelyDecisionContext = new OptimizelyDecisionContext(invalidFlagKey, null); @@ -1427,9 +1437,9 @@ public void removeForcedDecisionWithRuleKey() { String ruleKey = "77777"; String variationKey = "33333"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1443,9 +1453,9 @@ public void removeForcedDecisionWithoutRuleKey() { String flagKey = "55555"; String variationKey = "33333"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1460,9 +1470,9 @@ public void removeForcedDecisionWithNullRuleKeyAfterAddingWithRuleKey() { String ruleKey = "default-rollout-3045-20390585493"; String variationKey = "variation2"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); OptimizelyDecisionContext optimizelyDecisionContextNonNull = new OptimizelyDecisionContext(flagKey, ruleKey); @@ -1478,9 +1488,9 @@ public void removeForcedDecisionWithIncorrectFlagKey() { String variationKey = "variation2"; String incorrectFlagKey = "flag1"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); OptimizelyDecisionContext incorrectOptimizelyDecisionContext = new OptimizelyDecisionContext(incorrectFlagKey, null); @@ -1490,6 +1500,7 @@ public void removeForcedDecisionWithIncorrectFlagKey() { assertFalse(optimizelyUserContext.removeForcedDecision(incorrectOptimizelyDecisionContext)); } + @Test public void removeForcedDecisionWithIncorrectFlagKeyButSimilarRuleKey() { String flagKey = "flag2"; @@ -1497,9 +1508,9 @@ public void removeForcedDecisionWithIncorrectFlagKeyButSimilarRuleKey() { String ruleKey = "default-rollout-3045-20390585493"; String variationKey = "variation2"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); OptimizelyDecisionContext similarOptimizelyDecisionContext = new OptimizelyDecisionContext(incorrectFlagKey, ruleKey); @@ -1515,9 +1526,9 @@ public void removeAllForcedDecisions() { String ruleKey = "77777"; String variationKey = "33333"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1532,9 +1543,9 @@ public void setForcedDecisionsAndCallDecide() { String ruleKey = "exp_no_audience"; String variationKey = "variation_with_traffic"; OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1546,27 +1557,24 @@ public void setForcedDecisionsAndCallDecide() { assertNotNull(decision); assertTrue(decision.getReasons().contains( - String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) + String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) )); } - - /** - * ****************************************[START DECIDE TESTS WITH FDs]***************************************** - */ + /******************************************[START DECIDE TESTS WITH FDs]******************************************/ @Test public void setForcedDecisionsAndCallDecideFlagToDecision() { String flagKey = "feature_1"; String variationKey = "a"; optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, null); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1574,10 +1582,10 @@ public void setForcedDecisionsAndCallDecideFlagToDecision() { assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); isListenerCalled = false; @@ -1589,22 +1597,22 @@ public void setForcedDecisionsAndCallDecideFlagToDecision() { String variationId = "10389729780"; String experimentId = ""; + DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("") - .setRuleType("feature-test") - .setVariationKey(variationKey) - .setEnabled(true) - .build(); + .setFlagKey(flagKey) + .setRuleKey("") + .setRuleType("feature-test") + .setVariationKey(variationKey) + .setEnabled(true) + .build(); eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap(), metadata); assertNotNull(decision); assertTrue(decision.getReasons().contains( - String.format("Variation (%s) is mapped to flag (%s) and user (%s) in the forced decision map.", variationKey, flagKey, userId) + String.format("Variation (%s) is mapped to flag (%s) and user (%s) in the forced decision map.", variationKey, flagKey, userId) )); } - @Test public void setForcedDecisionsAndCallDecideExperimentRuleToDecision() { String flagKey = "feature_1"; @@ -1612,14 +1620,14 @@ public void setForcedDecisionsAndCallDecideExperimentRuleToDecision() { String variationKey = "a"; optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1627,10 +1635,10 @@ public void setForcedDecisionsAndCallDecideExperimentRuleToDecision() { assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); isListenerCalled = false; @@ -1642,11 +1650,12 @@ public void setForcedDecisionsAndCallDecideExperimentRuleToDecision() { String variationId = "10389729780"; String experimentId = "10390977673"; + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap()); assertNotNull(decision); assertTrue(decision.getReasons().contains( - String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) + String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) )); } @@ -1657,14 +1666,14 @@ public void setForcedDecisionsAndCallDecideDeliveryRuleToDecision() { String variationKey = "3324490633"; optimizely = new Optimizely.Builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); OptimizelyUserContext optimizelyUserContext = new OptimizelyUserContext( - optimizely, - userId, - Collections.emptyMap()); + optimizely, + userId, + Collections.emptyMap()); OptimizelyDecisionContext optimizelyDecisionContext = new OptimizelyDecisionContext(flagKey, ruleKey); OptimizelyForcedDecision optimizelyForcedDecision = new OptimizelyForcedDecision(variationKey); @@ -1672,10 +1681,10 @@ public void setForcedDecisionsAndCallDecideDeliveryRuleToDecision() { assertEquals(variationKey, optimizelyUserContext.getForcedDecision(optimizelyDecisionContext).getVariationKey()); optimizely.addDecisionNotificationHandler( - decisionNotification -> { - Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); - isListenerCalled = true; - }); + decisionNotification -> { + Assert.assertEquals(decisionNotification.getDecisionInfo().get(DECISION_EVENT_DISPATCHED), true); + isListenerCalled = true; + }); isListenerCalled = false; @@ -1687,17 +1696,15 @@ public void setForcedDecisionsAndCallDecideDeliveryRuleToDecision() { String variationId = "3324490633"; String experimentId = "3332020515"; + eventHandler.expectImpression(experimentId, variationId, userId, Collections.emptyMap()); assertNotNull(decision); assertTrue(decision.getReasons().contains( - String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) + String.format("Variation (%s) is mapped to flag (%s), rule (%s) and user (%s) in the forced decision map.", variationKey, flagKey, ruleKey, userId) )); } - - /** - * ******************************************[END DECIDE TESTS WITH FDs]***************************************** - */ + /********************************************[END DECIDE TESTS WITH FDs]******************************************/ @Test public void fetchQualifiedSegments() { @@ -1709,10 +1716,10 @@ public void fetchQualifiedSegments() { Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .withODPManager(mockODPManager) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withODPManager(mockODPManager) + .build(); OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); @@ -1735,9 +1742,9 @@ public void fetchQualifiedSegmentsErrorWhenConfigIsInvalid() { Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); Optimizely optimizely = Optimizely.builder() - .withConfigManager(mockProjectConfigManager) - .withODPManager(mockODPManager) - .build(); + .withConfigManager(mockProjectConfigManager) + .withODPManager(mockODPManager) + .build(); OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); @@ -1748,9 +1755,9 @@ public void fetchQualifiedSegmentsErrorWhenConfigIsInvalid() { @Test public void fetchQualifiedSegmentsError() { Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); assertFalse(userContext.fetchQualifiedSegments()); @@ -1764,20 +1771,20 @@ public void fetchQualifiedSegmentsAsync() throws InterruptedException { ODPManager mockODPManager = mock(ODPManager.class); doAnswer( - invocation -> { - ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(1, ODPSegmentManager.ODPSegmentFetchCallback.class); - callback.onCompleted(Arrays.asList("segment1", "segment2")); - return null; - } + invocation -> { + ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(1, ODPSegmentManager.ODPSegmentFetchCallback.class); + callback.onCompleted(Arrays.asList("segment1", "segment2")); + return null; + } ).when(mockODPSegmentManager).getQualifiedSegments(any(), (ODPSegmentManager.ODPSegmentFetchCallback) any(), any()); Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .withODPManager(mockODPManager) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withODPManager(mockODPManager) + .build(); OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); @@ -1812,20 +1819,20 @@ public void fetchQualifiedSegmentsAsyncWithVUID() throws InterruptedException { ODPManager mockODPManager = mock(ODPManager.class); doAnswer( - invocation -> { - ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(2, ODPSegmentManager.ODPSegmentFetchCallback.class); - callback.onCompleted(Arrays.asList("segment1", "segment2")); - return null; - } + invocation -> { + ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(2, ODPSegmentManager.ODPSegmentFetchCallback.class); + callback.onCompleted(Arrays.asList("segment1", "segment2")); + return null; + } ).when(mockODPSegmentManager).getQualifiedSegments(any(), eq("vuid_f6db3d60ba3a493d8e41bc995bb"), (ODPSegmentManager.ODPSegmentFetchCallback) any(), any()); Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .withODPManager(mockODPManager) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withODPManager(mockODPManager) + .build(); OptimizelyUserContext userContext = optimizely.createUserContext("vuid_f6db3d60ba3a493d8e41bc995bb"); @@ -1848,10 +1855,11 @@ public void fetchQualifiedSegmentsAsyncWithVUID() throws InterruptedException { }, Collections.singletonList(ODPSegmentOption.RESET_CACHE)); countDownLatch2.await(); - verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.VUID), eq("vuid_f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); + verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.VUID) ,eq("vuid_f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); } + @Test public void fetchQualifiedSegmentsAsyncWithUserID() throws InterruptedException { ODPEventManager mockODPEventManager = mock(ODPEventManager.class); @@ -1860,20 +1868,20 @@ public void fetchQualifiedSegmentsAsyncWithUserID() throws InterruptedException ODPManager mockODPManager = mock(ODPManager.class); doAnswer( - invocation -> { - ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(2, ODPSegmentManager.ODPSegmentFetchCallback.class); - callback.onCompleted(Arrays.asList("segment1", "segment2")); - return null; - } + invocation -> { + ODPSegmentManager.ODPSegmentFetchCallback callback = invocation.getArgumentAt(2, ODPSegmentManager.ODPSegmentFetchCallback.class); + callback.onCompleted(Arrays.asList("segment1", "segment2")); + return null; + } ).when(mockODPSegmentManager).getQualifiedSegments(any(), eq("f6db3d60ba3a493d8e41bc995bb"), (ODPSegmentManager.ODPSegmentFetchCallback) any(), any()); Mockito.when(mockODPManager.getEventManager()).thenReturn(mockODPEventManager); Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .withODPManager(mockODPManager) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withODPManager(mockODPManager) + .build(); OptimizelyUserContext userContext = optimizely.createUserContext("f6db3d60ba3a493d8e41bc995bb"); @@ -1896,16 +1904,16 @@ public void fetchQualifiedSegmentsAsyncWithUserID() throws InterruptedException }, Collections.singletonList(ODPSegmentOption.RESET_CACHE)); countDownLatch2.await(); - verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.FS_USER_ID), eq("f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); + verify(mockODPSegmentManager).getQualifiedSegments(eq(ODPUserKey.FS_USER_ID) ,eq("f6db3d60ba3a493d8e41bc995bb"), any(ODPSegmentManager.ODPSegmentFetchCallback.class), eq(Collections.singletonList(ODPSegmentOption.RESET_CACHE))); assertEquals(Arrays.asList("segment1", "segment2"), userContext.getQualifiedSegments()); } @Test public void fetchQualifiedSegmentsAsyncError() throws InterruptedException { Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .build(); OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); @@ -1932,9 +1940,9 @@ public void fetchQualifiedSegmentsAsyncErrorWhenConfigIsInvalid() throws Interru Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); Optimizely optimizely = Optimizely.builder() - .withConfigManager(mockProjectConfigManager) - .withODPManager(mockODPManager) - .build(); + .withConfigManager(mockProjectConfigManager) + .withODPManager(mockODPManager) + .build(); OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); @@ -1960,9 +1968,9 @@ public void identifyUserErrorWhenConfigIsInvalid() { Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); Optimizely optimizely = Optimizely.builder() - .withConfigManager(mockProjectConfigManager) - .withODPManager(mockODPManager) - .build(); + .withConfigManager(mockProjectConfigManager) + .withODPManager(mockODPManager) + .build(); optimizely.createUserContext("test-user"); verify(mockODPEventManager, never()).identifyUser("test-user"); @@ -1981,10 +1989,10 @@ public void identifyUser() { Mockito.when(mockODPManager.getSegmentManager()).thenReturn(mockODPSegmentManager); Optimizely optimizely = Optimizely.builder() - .withDatafile(datafile) - .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) - .withODPManager(mockODPManager) - .build(); + .withDatafile(datafile) + .withEventProcessor(new ForwardingEventProcessor(eventHandler, null)) + .withODPManager(mockODPManager) + .build(); OptimizelyUserContext userContext = optimizely.createUserContext("test-user"); verify(mockODPEventManager).identifyUser("test-user"); @@ -1999,6 +2007,7 @@ public void identifyUser() { } // utils + Map createUserProfileMap(String experimentId, String variationId) { Map userProfileMap = new HashMap(); userProfileMap.put(UserProfileService.userIdKey, userId); @@ -2051,9 +2060,7 @@ void addSpyGroup(Group group) { } void setMockConfig() { - if (config != null) { - return; - } + if (config != null) return; ProjectConfig configReal = null; try { @@ -2077,188 +2084,4 @@ OptimizelyDecision callDecideWithIncludeReasons(String flagKey) { return callDecideWithIncludeReasons(flagKey, Collections.emptyMap()); } - private Optimizely createOptimizelyWithHoldouts() throws Exception { - String holdoutDatafile = com.google.common.io.Resources.toString( - com.google.common.io.Resources.getResource("config/holdouts-project-config.json"), - com.google.common.base.Charsets.UTF_8 - ); - return new Optimizely.Builder().withDatafile(holdoutDatafile).withEventProcessor(new ForwardingEventProcessor(eventHandler, null)).build(); - } - - @Test - public void decisionNotification_with_holdout() throws Exception { - // Use holdouts datafile - Optimizely optWithHoldout = createOptimizelyWithHoldouts(); - String flagKey = "boolean_feature"; - String userId = "user123"; - String ruleKey = "basic_holdout"; // holdout rule key - String variationKey = "ho_off_key"; // holdout (off) variation key - String experimentId = "10075323428"; // holdout experiment id in holdouts-project-config.json - String variationId = "$opt_dummy_variation_id";// dummy variation id used for holdout impressions - String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (" + ruleKey + ")."; - - Map attrs = new HashMap<>(); - attrs.put("$opt_bucketing_id", "ppid160000"); // deterministic bucketing into basic_holdout - attrs.put("nationality", "English"); // non-reserved attribute should appear in impression & notification - - OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); - - // Register notification handler similar to decisionNotification test - isListenerCalled = false; - optWithHoldout.addDecisionNotificationHandler(decisionNotification -> { - Assert.assertEquals(NotificationCenter.DecisionNotificationType.FLAG.toString(), decisionNotification.getType()); - Assert.assertEquals(userId, decisionNotification.getUserId()); - - Assert.assertEquals(attrs, decisionNotification.getAttributes()); - - Map info = decisionNotification.getDecisionInfo(); - Assert.assertEquals(flagKey, info.get(FLAG_KEY)); - Assert.assertEquals(variationKey, info.get(VARIATION_KEY)); - Assert.assertEquals(false, info.get(ENABLED)); - Assert.assertEquals(ruleKey, info.get(RULE_KEY)); - Assert.assertEquals(experimentId, info.get(EXPERIMENT_ID)); - Assert.assertEquals(variationId, info.get(VARIATION_ID)); - // Variables should be empty because feature is disabled by holdout - Assert.assertTrue(((Map) info.get(VARIABLES)).isEmpty()); - // Event should be dispatched (no DISABLE_DECISION_EVENT option) - Assert.assertEquals(true, info.get(DECISION_EVENT_DISPATCHED)); - - @SuppressWarnings("unchecked") - List reasons = (List) info.get(REASONS); - Assert.assertTrue("Expected holdout reason present", reasons.contains(expectedReason)); - isListenerCalled = true; - }); - - // Execute decision with INCLUDE_REASONS so holdout reason is present - OptimizelyDecision decision = user.decide(flagKey, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); - assertTrue(isListenerCalled); - - // Sanity checks on returned decision - assertEquals(variationKey, decision.getVariationKey()); - assertFalse(decision.getEnabled()); - assertTrue(decision.getReasons().contains(expectedReason)); - - // Impression expectation (nationality only) - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey(ruleKey) - .setRuleType("holdout") - .setVariationKey(variationKey) - .setEnabled(false) - .build(); - eventHandler.expectImpression(experimentId, variationId, userId, Collections.singletonMap("nationality", "English"), metadata); - - // Log expectation (reuse existing pattern) - logbackVerifier.expectMessage(Level.INFO, expectedReason); - } - - @Test - public void decide_for_keys_with_holdout() throws Exception { - Optimizely optWithHoldout = createOptimizelyWithHoldouts(); - String userId = "user123"; - Map attrs = new HashMap<>(); - attrs.put("$opt_bucketing_id", "ppid160000"); - OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); - - List flagKeys = Arrays.asList( - "boolean_feature", // previously validated basic_holdout membership - "double_single_variable_feature", // also subject to global/basic holdout - "integer_single_variable_feature" // also subject to global/basic holdout - ); - - Map decisions = user.decideForKeys(flagKeys, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); - assertEquals(3, decisions.size()); - - String holdoutExperimentId = "10075323428"; // basic_holdout id - String variationId = "$opt_dummy_variation_id"; - String variationKey = "ho_off_key"; - String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (basic_holdout)."; - - for (String flagKey : flagKeys) { - OptimizelyDecision d = decisions.get(flagKey); - assertNotNull(d); - assertEquals(flagKey, d.getFlagKey()); - assertEquals(variationKey, d.getVariationKey()); - assertFalse(d.getEnabled()); - assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("basic_holdout") - .setRuleType("holdout") - .setVariationKey(variationKey) - .setEnabled(false) - .build(); - // attributes map expected empty (reserved $opt_ attribute filtered out) - eventHandler.expectImpression(holdoutExperimentId, variationId, userId, Collections.emptyMap(), metadata); - } - - // At least one log message confirming holdout membership - logbackVerifier.expectMessage(Level.INFO, expectedReason); - } - - @Test - public void decide_all_with_holdout() throws Exception { - - Optimizely optWithHoldout = createOptimizelyWithHoldouts(); - String userId = "user123"; - Map attrs = new HashMap<>(); - // ppid120000 buckets user into holdout_included_flags - attrs.put("$opt_bucketing_id", "ppid120000"); - OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); - - // All flag keys present in holdouts-project-config.json - List allFlagKeys = Arrays.asList( - "boolean_feature", - "double_single_variable_feature", - "integer_single_variable_feature", - "boolean_single_variable_feature", - "string_single_variable_feature", - "multi_variate_feature", - "multi_variate_future_feature", - "mutex_group_feature" - ); - - // Flags INCLUDED in holdout_included_flags (only these should be holdout decisions) - List includedInHoldout = Arrays.asList( - "boolean_feature", - "double_single_variable_feature", - "integer_single_variable_feature" - ); - - Map decisions = user.decideAll(Arrays.asList( - OptimizelyDecideOption.INCLUDE_REASONS, - OptimizelyDecideOption.DISABLE_DECISION_EVENT - )); - assertEquals(allFlagKeys.size(), decisions.size()); - - String holdoutExperimentId = "1007543323427"; // holdout_included_flags id - String variationId = "$opt_dummy_variation_id"; - String variationKey = "ho_off_key"; - String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags)."; - - int holdoutCount = 0; - for (String flagKey : allFlagKeys) { - OptimizelyDecision d = decisions.get(flagKey); - assertNotNull("Missing decision for flag " + flagKey, d); - if (includedInHoldout.contains(flagKey)) { - // Should be holdout decision - assertEquals(variationKey, d.getVariationKey()); - assertFalse(d.getEnabled()); - assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); - DecisionMetadata metadata = new DecisionMetadata.Builder() - .setFlagKey(flagKey) - .setRuleKey("holdout_included_flags") - .setRuleType("holdout") - .setVariationKey(variationKey) - .setEnabled(false) - .build(); - holdoutCount++; - } else { - // Should NOT be a holdout decision - assertFalse("Non-included flag should not have holdout reason: " + flagKey, d.getReasons().contains(expectedReason)); - } - } - assertEquals("Expected exactly the included flags to be in holdout", includedInHoldout.size(), holdoutCount); - logbackVerifier.expectMessage(Level.INFO, expectedReason); - } } From 8b36afde82679e4cc5cf5b9e6fdce45e310cddf7 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Mon, 11 Aug 2025 20:04:58 +0600 Subject: [PATCH 16/17] feat: implement decision notification handling with holdouts - Create Optimizely instance with specific configuration - Set up decision notification handler for optimalizely decision notifications - Test decision notification logic with holdout context - Validate decision notification data for various test scenarios --- .../ab/OptimizelyUserContextTest.java | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java index bb2d36192..a0b555d66 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyUserContextTest.java @@ -2084,4 +2084,187 @@ OptimizelyDecision callDecideWithIncludeReasons(String flagKey) { return callDecideWithIncludeReasons(flagKey, Collections.emptyMap()); } + private Optimizely createOptimizelyWithHoldouts() throws Exception { + String holdoutDatafile = com.google.common.io.Resources.toString( + com.google.common.io.Resources.getResource("config/holdouts-project-config.json"), + com.google.common.base.Charsets.UTF_8 + ); + return new Optimizely.Builder().withDatafile(holdoutDatafile).withEventProcessor(new ForwardingEventProcessor(eventHandler, null)).build(); + } + + @Test + public void decisionNotification_with_holdout() throws Exception { + // Use holdouts datafile + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String flagKey = "boolean_feature"; + String userId = "user123"; + String ruleKey = "basic_holdout"; // holdout rule key + String variationKey = "ho_off_key"; // holdout (off) variation key + String experimentId = "10075323428"; // holdout experiment id in holdouts-project-config.json + String variationId = "$opt_dummy_variation_id";// dummy variation id used for holdout impressions + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (" + ruleKey + ")."; + + Map attrs = new HashMap<>(); + attrs.put("$opt_bucketing_id", "ppid160000"); // deterministic bucketing into basic_holdout + attrs.put("nationality", "English"); // non-reserved attribute should appear in impression & notification + + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + + // Register notification handler similar to decisionNotification test + isListenerCalled = false; + optWithHoldout.addDecisionNotificationHandler(decisionNotification -> { + Assert.assertEquals(NotificationCenter.DecisionNotificationType.FLAG.toString(), decisionNotification.getType()); + Assert.assertEquals(userId, decisionNotification.getUserId()); + + Assert.assertEquals(attrs, decisionNotification.getAttributes()); + + Map info = decisionNotification.getDecisionInfo(); + Assert.assertEquals(flagKey, info.get(FLAG_KEY)); + Assert.assertEquals(variationKey, info.get(VARIATION_KEY)); + Assert.assertEquals(false, info.get(ENABLED)); + Assert.assertEquals(ruleKey, info.get(RULE_KEY)); + Assert.assertEquals(experimentId, info.get(EXPERIMENT_ID)); + Assert.assertEquals(variationId, info.get(VARIATION_ID)); + // Variables should be empty because feature is disabled by holdout + Assert.assertTrue(((Map) info.get(VARIABLES)).isEmpty()); + // Event should be dispatched (no DISABLE_DECISION_EVENT option) + Assert.assertEquals(true, info.get(DECISION_EVENT_DISPATCHED)); + + @SuppressWarnings("unchecked") + List reasons = (List) info.get(REASONS); + Assert.assertTrue("Expected holdout reason present", reasons.contains(expectedReason)); + isListenerCalled = true; + }); + + // Execute decision with INCLUDE_REASONS so holdout reason is present + OptimizelyDecision decision = user.decide(flagKey, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); + assertTrue(isListenerCalled); + + // Sanity checks on returned decision + assertEquals(variationKey, decision.getVariationKey()); + assertFalse(decision.getEnabled()); + assertTrue(decision.getReasons().contains(expectedReason)); + + // Impression expectation (nationality only) + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey(ruleKey) + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + eventHandler.expectImpression(experimentId, variationId, userId, Collections.singletonMap("nationality", "English"), metadata); + + // Log expectation (reuse existing pattern) + logbackVerifier.expectMessage(Level.INFO, expectedReason); + } + @Test + public void decide_for_keys_with_holdout() throws Exception { + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String userId = "user123"; + Map attrs = new HashMap<>(); + attrs.put("$opt_bucketing_id", "ppid160000"); + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + + List flagKeys = Arrays.asList( + "boolean_feature", // previously validated basic_holdout membership + "double_single_variable_feature", // also subject to global/basic holdout + "integer_single_variable_feature" // also subject to global/basic holdout + ); + + Map decisions = user.decideForKeys(flagKeys, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); + assertEquals(3, decisions.size()); + + String holdoutExperimentId = "10075323428"; // basic_holdout id + String variationId = "$opt_dummy_variation_id"; + String variationKey = "ho_off_key"; + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (basic_holdout)."; + + for (String flagKey : flagKeys) { + OptimizelyDecision d = decisions.get(flagKey); + assertNotNull(d); + assertEquals(flagKey, d.getFlagKey()); + assertEquals(variationKey, d.getVariationKey()); + assertFalse(d.getEnabled()); + assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("basic_holdout") + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + // attributes map expected empty (reserved $opt_ attribute filtered out) + eventHandler.expectImpression(holdoutExperimentId, variationId, userId, Collections.emptyMap(), metadata); + } + + // At least one log message confirming holdout membership + logbackVerifier.expectMessage(Level.INFO, expectedReason); + } + + @Test + public void decide_all_with_holdout() throws Exception { + + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); + String userId = "user123"; + Map attrs = new HashMap<>(); + // ppid120000 buckets user into holdout_included_flags + attrs.put("$opt_bucketing_id", "ppid120000"); + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); + + // All flag keys present in holdouts-project-config.json + List allFlagKeys = Arrays.asList( + "boolean_feature", + "double_single_variable_feature", + "integer_single_variable_feature", + "boolean_single_variable_feature", + "string_single_variable_feature", + "multi_variate_feature", + "multi_variate_future_feature", + "mutex_group_feature" + ); + + // Flags INCLUDED in holdout_included_flags (only these should be holdout decisions) + List includedInHoldout = Arrays.asList( + "boolean_feature", + "double_single_variable_feature", + "integer_single_variable_feature" + ); + + Map decisions = user.decideAll(Arrays.asList( + OptimizelyDecideOption.INCLUDE_REASONS, + OptimizelyDecideOption.DISABLE_DECISION_EVENT + )); + assertEquals(allFlagKeys.size(), decisions.size()); + + String holdoutExperimentId = "1007543323427"; // holdout_included_flags id + String variationId = "$opt_dummy_variation_id"; + String variationKey = "ho_off_key"; + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags)."; + + int holdoutCount = 0; + for (String flagKey : allFlagKeys) { + OptimizelyDecision d = decisions.get(flagKey); + assertNotNull("Missing decision for flag " + flagKey, d); + if (includedInHoldout.contains(flagKey)) { + // Should be holdout decision + assertEquals(variationKey, d.getVariationKey()); + assertFalse(d.getEnabled()); + assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); + DecisionMetadata metadata = new DecisionMetadata.Builder() + .setFlagKey(flagKey) + .setRuleKey("holdout_included_flags") + .setRuleType("holdout") + .setVariationKey(variationKey) + .setEnabled(false) + .build(); + holdoutCount++; + } else { + // Should NOT be a holdout decision + assertFalse("Non-included flag should not have holdout reason: " + flagKey, d.getReasons().contains(expectedReason)); + } + } + assertEquals("Expected exactly the included flags to be in holdout", includedInHoldout.size(), holdoutCount); + logbackVerifier.expectMessage(Level.INFO, expectedReason); + } } From d8fc1e4cbfb46f9b46d1a8315d187eaf1ea6a809 Mon Sep 17 00:00:00 2001 From: muzahidul-opti Date: Tue, 12 Aug 2025 20:05:20 +0600 Subject: [PATCH 17/17] feat(core-api): deprecate ExperimentCore and replace with Experiment in ActivateNotification --- core-api/src/main/java/com/optimizely/ab/Optimizely.java | 8 ++++++-- .../optimizely/ab/notification/ActivateNotification.java | 8 ++++---- .../ab/notification/ActivateNotificationListener.java | 3 +-- .../ActivateNotificationListenerInterface.java | 4 ++-- .../ab/notification/ActivateNotificationListenerTest.java | 3 +-- .../ab/notification/NotificationCenterTest.java | 6 +++--- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index f18c61283..d041bfad3 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -345,13 +345,17 @@ private boolean sendImpression(@Nonnull ProjectConfig projectConfig, if (experiment != null) { logger.info("Activating user \"{}\" in experiment \"{}\".", userId, experiment.getKey()); } + + // Legacy API methods only apply to the Experiment type and not to Holdout. + boolean isExperimentType = experiment instanceof Experiment; + // Kept For backwards compatibility. // This notification is deprecated and the new DecisionNotifications // are sent via their respective method calls. - if (notificationCenter.getNotificationManager(ActivateNotification.class).size() > 0) { + if (notificationCenter.getNotificationManager(ActivateNotification.class).size() > 0 && isExperimentType) { LogEvent impressionEvent = EventFactory.createLogEvent(userEvent); ActivateNotification activateNotification = new ActivateNotification( - experiment, userId, filteredAttributes, variation, impressionEvent); + (Experiment)experiment, userId, filteredAttributes, variation, impressionEvent); notificationCenter.send(activateNotification); } return true; diff --git a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java index c1c830432..b94db2857 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java @@ -19,7 +19,7 @@ import java.util.Map; import com.optimizely.ab.annotations.VisibleForTesting; -import com.optimizely.ab.config.ExperimentCore; +import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.Variation; import com.optimizely.ab.event.LogEvent; @@ -32,7 +32,7 @@ @Deprecated public final class ActivateNotification { - private final ExperimentCore experiment; + private final Experiment experiment; private final String userId; private final Map attributes; private final Variation variation; @@ -50,7 +50,7 @@ public final class ActivateNotification { * @param variation - The variation that was returned from activate. * @param event - The impression event that was triggered. */ - public ActivateNotification(ExperimentCore experiment, String userId, Map attributes, Variation variation, LogEvent event) { + public ActivateNotification(Experiment experiment, String userId, Map attributes, Variation variation, LogEvent event) { this.experiment = experiment; this.userId = userId; this.attributes = attributes; @@ -58,7 +58,7 @@ public ActivateNotification(ExperimentCore experiment, String userId, Map attributes, @Nonnull Variation variation, diff --git a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java index 6feda2ef6..c5ae2901f 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java @@ -20,7 +20,7 @@ import javax.annotation.Nonnull; -import com.optimizely.ab.config.ExperimentCore; +import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.Variation; import com.optimizely.ab.event.LogEvent; @@ -41,7 +41,7 @@ public interface ActivateNotificationListenerInterface { * @param variation - The variation that was returned from activate. * @param event - The impression event that was triggered. */ - public void onActivate(@Nonnull ExperimentCore experiment, + public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, diff --git a/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java b/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java index 4d2a42114..844e51700 100644 --- a/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java +++ b/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java @@ -27,7 +27,6 @@ import static org.mockito.Mockito.mock; import com.optimizely.ab.config.Experiment; -import com.optimizely.ab.config.ExperimentCore; import com.optimizely.ab.config.Variation; import com.optimizely.ab.event.LogEvent; @@ -66,7 +65,7 @@ public void testNotifyWithActivateNotificationArg() { private static class ActivateNotificationHandler extends ActivateNotificationListener { @Override - public void onActivate(@Nonnull ExperimentCore experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { + public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { assertEquals(EXPERIMENT, experiment); assertEquals(USER_ID, userId); assertEquals(USER_ATTRIBUTES, attributes); diff --git a/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java b/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java index 69f46107d..d3c55cccb 100644 --- a/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java +++ b/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java @@ -32,7 +32,7 @@ import static org.mockito.Mockito.mock; import com.optimizely.ab.OptimizelyRuntimeException; -import com.optimizely.ab.config.ExperimentCore; +import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.Variation; import com.optimizely.ab.event.LogEvent; import com.optimizely.ab.internal.LogbackVerifier; @@ -94,7 +94,7 @@ public void testAddDecisionNotificationTwice() { public void testAddActivateNotificationTwice() { ActivateNotificationListener listener = new ActivateNotificationListener() { @Override - public void onActivate(@Nonnull ExperimentCore experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { + public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { } }; @@ -109,7 +109,7 @@ public void onActivate(@Nonnull ExperimentCore experiment, @Nonnull String userI public void testAddActivateNotification() { int notificationId = notificationCenter.addActivateNotificationListener(new ActivateNotificationListener() { @Override - public void onActivate(@Nonnull ExperimentCore experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { + public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { } });