|
57 | 57 | import static junit.framework.TestCase.assertEquals;
|
58 | 58 | import static junit.framework.TestCase.assertTrue;
|
59 | 59 | import 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; |
60 | 63 | import static org.mockito.Mockito.*;
|
61 | 64 |
|
62 | 65 | public class OptimizelyUserContextTest {
|
@@ -2074,4 +2077,188 @@ OptimizelyDecision callDecideWithIncludeReasons(String flagKey) {
|
2074 | 2077 | return callDecideWithIncludeReasons(flagKey, Collections.emptyMap());
|
2075 | 2078 | }
|
2076 | 2079 |
|
| 2080 | + private Optimizely createOptimizelyWithHoldouts() throws Exception { |
| 2081 | + String holdoutDatafile = com.google.common.io.Resources.toString( |
| 2082 | + com.google.common.io.Resources.getResource("config/holdouts-project-config.json"), |
| 2083 | + com.google.common.base.Charsets.UTF_8 |
| 2084 | + ); |
| 2085 | + return new Optimizely.Builder().withDatafile(holdoutDatafile).withEventProcessor(new ForwardingEventProcessor(eventHandler, null)).build(); |
| 2086 | + } |
| 2087 | + |
| 2088 | + @Test |
| 2089 | + public void decisionNotification_with_holdout() throws Exception { |
| 2090 | + // Use holdouts datafile |
| 2091 | + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); |
| 2092 | + String flagKey = "boolean_feature"; |
| 2093 | + String userId = "user123"; |
| 2094 | + String ruleKey = "basic_holdout"; // holdout rule key |
| 2095 | + String variationKey = "ho_off_key"; // holdout (off) variation key |
| 2096 | + String experimentId = "10075323428"; // holdout experiment id in holdouts-project-config.json |
| 2097 | + String variationId = "$opt_dummy_variation_id";// dummy variation id used for holdout impressions |
| 2098 | + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (" + ruleKey + ")."; |
| 2099 | + |
| 2100 | + Map<String, Object> attrs = new HashMap<>(); |
| 2101 | + attrs.put("$opt_bucketing_id", "ppid160000"); // deterministic bucketing into basic_holdout |
| 2102 | + attrs.put("nationality", "English"); // non-reserved attribute should appear in impression & notification |
| 2103 | + |
| 2104 | + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); |
| 2105 | + |
| 2106 | + // Register notification handler similar to decisionNotification test |
| 2107 | + isListenerCalled = false; |
| 2108 | + optWithHoldout.addDecisionNotificationHandler(decisionNotification -> { |
| 2109 | + Assert.assertEquals(NotificationCenter.DecisionNotificationType.FLAG.toString(), decisionNotification.getType()); |
| 2110 | + Assert.assertEquals(userId, decisionNotification.getUserId()); |
| 2111 | + |
| 2112 | + Assert.assertEquals(attrs, decisionNotification.getAttributes()); |
| 2113 | + |
| 2114 | + Map<String, ?> info = decisionNotification.getDecisionInfo(); |
| 2115 | + Assert.assertEquals(flagKey, info.get(FLAG_KEY)); |
| 2116 | + Assert.assertEquals(variationKey, info.get(VARIATION_KEY)); |
| 2117 | + Assert.assertEquals(false, info.get(ENABLED)); |
| 2118 | + Assert.assertEquals(ruleKey, info.get(RULE_KEY)); |
| 2119 | + Assert.assertEquals(experimentId, info.get(EXPERIMENT_ID)); |
| 2120 | + Assert.assertEquals(variationId, info.get(VARIATION_ID)); |
| 2121 | + // Variables should be empty because feature is disabled by holdout |
| 2122 | + Assert.assertTrue(((Map<?, ?>) info.get(VARIABLES)).isEmpty()); |
| 2123 | + // Event should be dispatched (no DISABLE_DECISION_EVENT option) |
| 2124 | + Assert.assertEquals(true, info.get(DECISION_EVENT_DISPATCHED)); |
| 2125 | + |
| 2126 | + @SuppressWarnings("unchecked") |
| 2127 | + List<String> reasons = (List<String>) info.get(REASONS); |
| 2128 | + Assert.assertTrue("Expected holdout reason present", reasons.contains(expectedReason)); |
| 2129 | + isListenerCalled = true; |
| 2130 | + }); |
| 2131 | + |
| 2132 | + // Execute decision with INCLUDE_REASONS so holdout reason is present |
| 2133 | + OptimizelyDecision decision = user.decide(flagKey, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); |
| 2134 | + assertTrue(isListenerCalled); |
| 2135 | + |
| 2136 | + // Sanity checks on returned decision |
| 2137 | + assertEquals(variationKey, decision.getVariationKey()); |
| 2138 | + assertFalse(decision.getEnabled()); |
| 2139 | + assertTrue(decision.getReasons().contains(expectedReason)); |
| 2140 | + |
| 2141 | + // Impression expectation (nationality only) |
| 2142 | + DecisionMetadata metadata = new DecisionMetadata.Builder() |
| 2143 | + .setFlagKey(flagKey) |
| 2144 | + .setRuleKey(ruleKey) |
| 2145 | + .setRuleType("holdout") |
| 2146 | + .setVariationKey(variationKey) |
| 2147 | + .setEnabled(false) |
| 2148 | + .build(); |
| 2149 | + eventHandler.expectImpression(experimentId, variationId, userId, Collections.singletonMap("nationality", "English"), metadata); |
| 2150 | + |
| 2151 | + // Log expectation (reuse existing pattern) |
| 2152 | + logbackVerifier.expectMessage(Level.INFO, expectedReason); |
| 2153 | + } |
| 2154 | + |
| 2155 | + @Test |
| 2156 | + public void decide_for_keys_with_holdout() throws Exception { |
| 2157 | + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); |
| 2158 | + String userId = "user123"; |
| 2159 | + Map<String, Object> attrs = new HashMap<>(); |
| 2160 | + attrs.put("$opt_bucketing_id", "ppid160000"); |
| 2161 | + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); |
| 2162 | + |
| 2163 | + List<String> flagKeys = Arrays.asList( |
| 2164 | + "boolean_feature", // previously validated basic_holdout membership |
| 2165 | + "double_single_variable_feature", // also subject to global/basic holdout |
| 2166 | + "integer_single_variable_feature" // also subject to global/basic holdout |
| 2167 | + ); |
| 2168 | + |
| 2169 | + Map<String, OptimizelyDecision> decisions = user.decideForKeys(flagKeys, Collections.singletonList(OptimizelyDecideOption.INCLUDE_REASONS)); |
| 2170 | + assertEquals(3, decisions.size()); |
| 2171 | + |
| 2172 | + String holdoutExperimentId = "10075323428"; // basic_holdout id |
| 2173 | + String variationId = "$opt_dummy_variation_id"; |
| 2174 | + String variationKey = "ho_off_key"; |
| 2175 | + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (basic_holdout)."; |
| 2176 | + |
| 2177 | + for (String flagKey : flagKeys) { |
| 2178 | + OptimizelyDecision d = decisions.get(flagKey); |
| 2179 | + assertNotNull(d); |
| 2180 | + assertEquals(flagKey, d.getFlagKey()); |
| 2181 | + assertEquals(variationKey, d.getVariationKey()); |
| 2182 | + assertFalse(d.getEnabled()); |
| 2183 | + assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); |
| 2184 | + DecisionMetadata metadata = new DecisionMetadata.Builder() |
| 2185 | + .setFlagKey(flagKey) |
| 2186 | + .setRuleKey("basic_holdout") |
| 2187 | + .setRuleType("holdout") |
| 2188 | + .setVariationKey(variationKey) |
| 2189 | + .setEnabled(false) |
| 2190 | + .build(); |
| 2191 | + // attributes map expected empty (reserved $opt_ attribute filtered out) |
| 2192 | + eventHandler.expectImpression(holdoutExperimentId, variationId, userId, Collections.emptyMap(), metadata); |
| 2193 | + } |
| 2194 | + |
| 2195 | + // At least one log message confirming holdout membership |
| 2196 | + logbackVerifier.expectMessage(Level.INFO, expectedReason); |
| 2197 | + } |
| 2198 | + |
| 2199 | + @Test |
| 2200 | + public void decide_all_with_holdout() throws Exception { |
| 2201 | + |
| 2202 | + Optimizely optWithHoldout = createOptimizelyWithHoldouts(); |
| 2203 | + String userId = "user123"; |
| 2204 | + Map<String, Object> attrs = new HashMap<>(); |
| 2205 | + // ppid120000 buckets user into holdout_included_flags |
| 2206 | + attrs.put("$opt_bucketing_id", "ppid120000"); |
| 2207 | + OptimizelyUserContext user = optWithHoldout.createUserContext(userId, attrs); |
| 2208 | + |
| 2209 | + // All flag keys present in holdouts-project-config.json |
| 2210 | + List<String> allFlagKeys = Arrays.asList( |
| 2211 | + "boolean_feature", |
| 2212 | + "double_single_variable_feature", |
| 2213 | + "integer_single_variable_feature", |
| 2214 | + "boolean_single_variable_feature", |
| 2215 | + "string_single_variable_feature", |
| 2216 | + "multi_variate_feature", |
| 2217 | + "multi_variate_future_feature", |
| 2218 | + "mutex_group_feature" |
| 2219 | + ); |
| 2220 | + |
| 2221 | + // Flags INCLUDED in holdout_included_flags (only these should be holdout decisions) |
| 2222 | + List<String> includedInHoldout = Arrays.asList( |
| 2223 | + "boolean_feature", |
| 2224 | + "double_single_variable_feature", |
| 2225 | + "integer_single_variable_feature" |
| 2226 | + ); |
| 2227 | + |
| 2228 | + Map<String, OptimizelyDecision> decisions = user.decideAll(Arrays.asList( |
| 2229 | + OptimizelyDecideOption.INCLUDE_REASONS, |
| 2230 | + OptimizelyDecideOption.DISABLE_DECISION_EVENT |
| 2231 | + )); |
| 2232 | + assertEquals(allFlagKeys.size(), decisions.size()); |
| 2233 | + |
| 2234 | + String holdoutExperimentId = "1007543323427"; // holdout_included_flags id |
| 2235 | + String variationId = "$opt_dummy_variation_id"; |
| 2236 | + String variationKey = "ho_off_key"; |
| 2237 | + String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags)."; |
| 2238 | + |
| 2239 | + int holdoutCount = 0; |
| 2240 | + for (String flagKey : allFlagKeys) { |
| 2241 | + OptimizelyDecision d = decisions.get(flagKey); |
| 2242 | + assertNotNull("Missing decision for flag " + flagKey, d); |
| 2243 | + if (includedInHoldout.contains(flagKey)) { |
| 2244 | + // Should be holdout decision |
| 2245 | + assertEquals(variationKey, d.getVariationKey()); |
| 2246 | + assertFalse(d.getEnabled()); |
| 2247 | + assertTrue("Expected holdout reason for flag " + flagKey, d.getReasons().contains(expectedReason)); |
| 2248 | + DecisionMetadata metadata = new DecisionMetadata.Builder() |
| 2249 | + .setFlagKey(flagKey) |
| 2250 | + .setRuleKey("holdout_included_flags") |
| 2251 | + .setRuleType("holdout") |
| 2252 | + .setVariationKey(variationKey) |
| 2253 | + .setEnabled(false) |
| 2254 | + .build(); |
| 2255 | + holdoutCount++; |
| 2256 | + } else { |
| 2257 | + // Should NOT be a holdout decision |
| 2258 | + assertFalse("Non-included flag should not have holdout reason: " + flagKey, d.getReasons().contains(expectedReason)); |
| 2259 | + } |
| 2260 | + } |
| 2261 | + assertEquals("Expected exactly the included flags to be in holdout", includedInHoldout.size(), holdoutCount); |
| 2262 | + logbackVerifier.expectMessage(Level.INFO, expectedReason); |
| 2263 | + } |
2077 | 2264 | }
|
0 commit comments