5757import static junit .framework .TestCase .assertEquals ;
5858import static junit .framework .TestCase .assertTrue ;
5959import static org .junit .Assert .*;
60+ import static org .mockito .Matchers .any ;
61+ import static org .mockito .Matchers .anyObject ;
62+ import static org .mockito .Matchers .eq ;
6063import static org .mockito .Mockito .*;
6164
6265public class OptimizelyUserContextTest {
@@ -76,6 +79,8 @@ public class OptimizelyUserContextTest {
7679 Map <String , FeatureFlag > featureKeyMapping ;
7780 Map <String , Group > groupIdMapping ;
7881
82+ private String holdoutDatafile ;
83+
7984 @ Before
8085 public void setUp () throws Exception {
8186 datafile = Resources .toString (Resources .getResource ("config/decide-project-config.json" ), Charsets .UTF_8 );
@@ -85,6 +90,16 @@ public void setUp() throws Exception {
8590 .build ();
8691 }
8792
93+ private Optimizely createOptimizelyWithHoldouts () throws Exception {
94+ if (holdoutDatafile == null ) {
95+ holdoutDatafile = com .google .common .io .Resources .toString (
96+ com .google .common .io .Resources .getResource ("config/holdouts-project-config.json" ),
97+ com .google .common .base .Charsets .UTF_8
98+ );
99+ }
100+ return new Optimizely .Builder ().withDatafile (holdoutDatafile ).withEventProcessor (new ForwardingEventProcessor (eventHandler , null )).build ();
101+ }
102+
88103 @ Test
89104 public void optimizelyUserContext_withAttributes () {
90105 Map <String , Object > attributes = Collections .singletonMap (ATTRIBUTE_HOUSE_KEY , AUDIENCE_GRYFFINDOR_VALUE );
@@ -749,7 +764,7 @@ public void decisionNotification() {
749764 public void decideOptions_bypassUPS () throws Exception {
750765 String flagKey = "feature_2" ; // embedding experiment: "exp_no_audience"
751766 String experimentId = "10420810910" ; // "exp_no_audience"
752- String variationId1 = "10418551353" ;
767+ String variationId = "10418551353" ;
753768 String variationId2 = "10418510624" ;
754769 String variationKey1 = "variation_with_traffic" ;
755770 String variationKey2 = "variation_no_traffic" ;
@@ -1786,6 +1801,8 @@ public void fetchQualifiedSegmentsAsync() throws InterruptedException {
17861801 .withODPManager (mockODPManager )
17871802 .build ();
17881803
1804+
1805+
17891806 OptimizelyUserContext userContext = optimizely .createUserContext ("test-user" );
17901807
17911808 CountDownLatch countDownLatch = new CountDownLatch (1 );
@@ -2084,4 +2101,42 @@ OptimizelyDecision callDecideWithIncludeReasons(String flagKey) {
20842101 return callDecideWithIncludeReasons (flagKey , Collections .emptyMap ());
20852102 }
20862103
2104+ @ Test
2105+ public void decide_holdoutApplied_basic () throws Exception {
2106+ Optimizely optWithHoldout = createOptimizelyWithHoldouts ();
2107+ // pick a flag that is eligible for basic_holdout. Using boolean_feature from config.
2108+ String flagKey = "boolean_feature" ;
2109+ String userId = "user123" ;
2110+ Map <String , Object > attrs = new HashMap <>();
2111+ attrs .put ("$opt_bucketing_id" , "ppid160000" );
2112+ OptimizelyUserContext user = optWithHoldout .createUserContext (userId , attrs );
2113+
2114+ // include reasons to surface holdout logs in decision reasons if implemented
2115+ OptimizelyDecision decision = user .decide (flagKey , Collections .singletonList (OptimizelyDecideOption .INCLUDE_REASONS ));
2116+
2117+ assertNotNull (decision );
2118+ assertEquals (flagKey , decision .getFlagKey ());
2119+ // holdout off variation => feature should be disabled
2120+ assertFalse (decision .getEnabled ());
2121+ // Expect decision source to be holdout (either via metadata map or reasons text)
2122+ boolean hasHoldoutReason = false ;
2123+ String expectedMString = "User (" + userId + ") is in variation (ho_off_key) of holdout (basic_holdout)." ;
2124+ if (decision .getReasons ().contains (expectedMString )) {
2125+ hasHoldoutReason = true ;
2126+ }
2127+ assertTrue ("Expected holdout to influence decision (reason containing 'holdout')" , hasHoldoutReason );
2128+ logbackVerifier .expectMessage (Level .INFO , expectedMString );
2129+
2130+ // Impression expectation: Holdout decisions still dispatch an impression with holdout context.
2131+ String variationId = "$opt_dummy_variation_id" ; // from holdouts-project-config.json
2132+ String experimentId = "10075323428" ; // holdout id for basic_holdout
2133+ DecisionMetadata metadata = new DecisionMetadata .Builder ()
2134+ .setFlagKey (flagKey )
2135+ .setRuleKey ("basic_holdout" )
2136+ .setRuleType ("holdout" )
2137+ .setVariationKey ("ho_off_key" )
2138+ .setEnabled (false )
2139+ .build ();
2140+ eventHandler .expectImpression (experimentId , variationId , userId , Collections .emptyMap (), metadata );
2141+ }
20872142}
0 commit comments