2525using OptimizelySDK . Config ;
2626using OptimizelySDK . Entity ;
2727using OptimizelySDK . ErrorHandler ;
28+ using OptimizelySDK . Event ;
29+ using OptimizelySDK . Event . Entity ;
2830using OptimizelySDK . Logger ;
2931using OptimizelySDK . OptimizelyDecisions ;
3032
@@ -34,6 +36,7 @@ namespace OptimizelySDK.Tests
3436 public class DecisionServiceHoldoutTest
3537 {
3638 private Mock < ILogger > LoggerMock ;
39+ private Mock < EventProcessor > EventProcessorMock ;
3740 private DecisionService DecisionService ;
3841 private DatafileProjectConfig Config ;
3942 private JObject TestData ;
@@ -46,6 +49,7 @@ public class DecisionServiceHoldoutTest
4649 public void Initialize ( )
4750 {
4851 LoggerMock = new Mock < ILogger > ( ) ;
52+ EventProcessorMock = new Mock < EventProcessor > ( ) ;
4953
5054 // Load test data
5155 var testDataPath = Path . Combine ( TestContext . CurrentContext . TestDirectory ,
@@ -242,5 +246,90 @@ public void TestGetVariationsForFeatureList_Holdout_DecisionReasons()
242246 Assert . IsTrue ( decisionWithReasons . DecisionReasons . ToReport ( ) . Count > 0 , "Should have decision reasons" ) ;
243247 }
244248 }
249+
250+ [ Test ]
251+ public void TestImpressionEventForHoldout ( )
252+ {
253+ var featureFlag = Config . FeatureKeyMap [ "test_flag_1" ] ;
254+ var userAttributes = new UserAttributes ( ) ;
255+
256+ var eventDispatcher = new Event . Dispatcher . DefaultEventDispatcher ( LoggerMock . Object ) ;
257+ var optimizelyWithMockedEvents = new Optimizely (
258+ TestData [ "datafileWithHoldouts" ] . ToString ( ) ,
259+ eventDispatcher ,
260+ LoggerMock . Object ,
261+ new ErrorHandler . NoOpErrorHandler ( ) ,
262+ null , // userProfileService
263+ false , // skipJsonValidation
264+ EventProcessorMock . Object
265+ ) ;
266+
267+ EventProcessorMock . Setup ( ep => ep . Process ( It . IsAny < ImpressionEvent > ( ) ) ) ;
268+
269+ var userContext = optimizelyWithMockedEvents . CreateUserContext ( TestUserId , userAttributes ) ;
270+ var decision = userContext . Decide ( featureFlag . Key ) ;
271+
272+ Assert . IsNotNull ( decision , "Decision should not be null" ) ;
273+ Assert . IsNotNull ( decision . RuleKey , "RuleKey should not be null" ) ;
274+
275+ var actualHoldout = Config . Holdouts ? . FirstOrDefault ( h => h . Key == decision . RuleKey ) ;
276+
277+ Assert . IsNotNull ( actualHoldout ,
278+ $ "RuleKey '{ decision . RuleKey } ' should correspond to a holdout experiment") ;
279+ Assert . AreEqual ( featureFlag . Key , decision . FlagKey , "Flag key should match" ) ;
280+
281+ var holdoutVariation = actualHoldout . Variations . FirstOrDefault ( v => v . Key == decision . VariationKey ) ;
282+
283+ Assert . IsNotNull ( holdoutVariation ,
284+ $ "Variation '{ decision . VariationKey } ' should be from the chosen holdout '{ actualHoldout . Key } '") ;
285+
286+ Assert . AreEqual ( holdoutVariation . FeatureEnabled , decision . Enabled ,
287+ "Enabled flag should match holdout variation's featureEnabled value" ) ;
288+
289+ EventProcessorMock . Verify ( ep => ep . Process ( It . IsAny < ImpressionEvent > ( ) ) , Times . Once ,
290+ "Impression event should be processed exactly once for holdout decision" ) ;
291+
292+ EventProcessorMock . Verify ( ep => ep . Process ( It . Is < ImpressionEvent > ( ie =>
293+ ie . Experiment . Key == actualHoldout . Key &&
294+ ie . Experiment . Id == actualHoldout . Id &&
295+ ie . Timestamp > 0 &&
296+ ie . UserId == TestUserId
297+ ) ) , Times . Once , "Impression event should contain correct holdout experiment details" ) ;
298+ }
299+
300+ [ Test ]
301+ public void TestImpressionEventForHoldout_DisableDecisionEvent ( )
302+ {
303+ var featureFlag = Config . FeatureKeyMap [ "test_flag_1" ] ;
304+ var userAttributes = new UserAttributes ( ) ;
305+
306+ var eventDispatcher = new Event . Dispatcher . DefaultEventDispatcher ( LoggerMock . Object ) ;
307+ var optimizelyWithMockedEvents = new Optimizely (
308+ TestData [ "datafileWithHoldouts" ] . ToString ( ) ,
309+ eventDispatcher ,
310+ LoggerMock . Object ,
311+ new ErrorHandler . NoOpErrorHandler ( ) ,
312+ null , // userProfileService
313+ false , // skipJsonValidation
314+ EventProcessorMock . Object
315+ ) ;
316+
317+ EventProcessorMock . Setup ( ep => ep . Process ( It . IsAny < ImpressionEvent > ( ) ) ) ;
318+
319+ var userContext = optimizelyWithMockedEvents . CreateUserContext ( TestUserId , userAttributes ) ;
320+ var decision = userContext . Decide ( featureFlag . Key , new [ ] { OptimizelyDecideOption . DISABLE_DECISION_EVENT } ) ;
321+
322+ Assert . IsNotNull ( decision , "Decision should not be null" ) ;
323+ Assert . IsNotNull ( decision . RuleKey , "User should be bucketed into a holdout" ) ;
324+
325+ var chosenHoldout = Config . Holdouts ? . FirstOrDefault ( h => h . Key == decision . RuleKey ) ;
326+
327+ Assert . IsNotNull ( chosenHoldout , $ "Holdout '{ decision . RuleKey } ' should exist in config") ;
328+
329+ Assert . AreEqual ( featureFlag . Key , decision . FlagKey , "Flag key should match" ) ;
330+
331+ EventProcessorMock . Verify ( ep => ep . Process ( It . IsAny < ImpressionEvent > ( ) ) , Times . Never ,
332+ "No impression event should be processed when DISABLE_DECISION_EVENT option is used" ) ;
333+ }
245334 }
246335}
0 commit comments