@@ -322,7 +322,6 @@ public void activateWithUnknownExperimentKeyAndRaiseExceptionErrorHandler() thro
322322 * Verify that {@link Optimizely#activate(String, String, Map)} passes through attributes.
323323 */
324324 @ Test
325- @ SuppressWarnings ("unchecked" )
326325 public void activateWithAttributes () throws Exception {
327326 String datafile = validConfigJsonV3 ();
328327 ProjectConfig projectConfig = validProjectConfigV3 ();
@@ -379,7 +378,6 @@ public void activateWithAttributes() throws Exception {
379378 * In this case, the activate call should remove the unknown attribute from the given map.
380379 */
381380 @ Test
382- @ SuppressWarnings ("unchecked" )
383381 public void activateWithUnknownAttribute () throws Exception {
384382 String datafile = validConfigJsonV3 ();
385383 ProjectConfig projectConfig = validProjectConfigV3 ();
@@ -437,6 +435,117 @@ public void activateWithUnknownAttribute() throws Exception {
437435 verify (mockEventHandler ).dispatchEvent (logEventToDispatch );
438436 }
439437
438+ /**
439+ * Verify that {@link Optimizely#activate(String, String, Map)} ignores null attributes.
440+ */
441+ @ Test
442+ @ SuppressFBWarnings (
443+ value ="NP_NONNULL_PARAM_VIOLATION" ,
444+ justification ="testing nullness contract violation" )
445+ public void activateWithNullAttributes () throws Exception {
446+ String datafile = noAudienceProjectConfigJsonV3 ();
447+ ProjectConfig projectConfig = noAudienceProjectConfigV3 ();
448+ Experiment activatedExperiment = projectConfig .getExperiments ().get (0 );
449+ Variation bucketedVariation = activatedExperiment .getVariations ().get (0 );
450+
451+ // setup a mock event builder to return expected impression params
452+ EventBuilder mockEventBuilder = mock (EventBuilder .class );
453+
454+ Optimizely optimizely = Optimizely .builder (datafile , mockEventHandler )
455+ .withBucketing (mockBucketer )
456+ .withEventBuilder (mockEventBuilder )
457+ .withConfig (projectConfig )
458+ .withErrorHandler (mockErrorHandler )
459+ .build ();
460+
461+ Map <String , String > testParams = new HashMap <String , String >();
462+ testParams .put ("test" , "params" );
463+ LogEvent logEventToDispatch = new LogEvent (RequestMethod .GET , "test_url" , testParams , "" );
464+ when (mockEventBuilder .createImpressionEvent (eq (projectConfig ), eq (activatedExperiment ), eq (bucketedVariation ),
465+ eq ("userId" ), eq (Collections .<String , String >emptyMap ()),
466+ isNull (String .class )))
467+ .thenReturn (logEventToDispatch );
468+
469+ when (mockBucketer .bucket (activatedExperiment , "userId" ))
470+ .thenReturn (bucketedVariation );
471+
472+ // activate the experiment
473+ Map <String , String > attributes = null ;
474+ Variation actualVariation = optimizely .activate (activatedExperiment .getKey (), "userId" , attributes );
475+
476+ logbackVerifier .expectMessage (Level .WARN , "Attributes is null when non-null was expected. Defaulting to an empty attributes map." );
477+
478+ // verify that the bucketing algorithm was called correctly
479+ verify (mockBucketer ).bucket (activatedExperiment , "userId" );
480+ assertThat (actualVariation , is (bucketedVariation ));
481+
482+ // setup the attribute map captor (so we can verify its content)
483+ ArgumentCaptor <Map > attributeCaptor = ArgumentCaptor .forClass (Map .class );
484+ verify (mockEventBuilder ).createImpressionEvent (eq (projectConfig ), eq (activatedExperiment ),
485+ eq (bucketedVariation ), eq ("userId" ), attributeCaptor .capture (),
486+ isNull (String .class ));
487+
488+ Map <String , String > actualValue = attributeCaptor .getValue ();
489+ assertThat (actualValue , is (Collections .<String , String >emptyMap ()));
490+
491+ // verify that dispatchEvent was called with the correct LogEvent object
492+ verify (mockEventHandler ).dispatchEvent (logEventToDispatch );
493+ }
494+
495+ /**
496+ * Verify that {@link Optimizely#activate(String, String, Map)} gracefully handles null attribute values.
497+ */
498+ @ Test
499+ public void activateWithNullAttributeValues () throws Exception {
500+ String datafile = validConfigJsonV3 ();
501+ ProjectConfig projectConfig = validProjectConfigV3 ();
502+ Experiment activatedExperiment = projectConfig .getExperiments ().get (0 );
503+ Variation bucketedVariation = activatedExperiment .getVariations ().get (0 );
504+ Attribute attribute = projectConfig .getAttributes ().get (0 );
505+
506+ // setup a mock event builder to return expected impression params
507+ EventBuilder mockEventBuilder = mock (EventBuilder .class );
508+
509+ Optimizely optimizely = Optimizely .builder (datafile , mockEventHandler )
510+ .withBucketing (mockBucketer )
511+ .withEventBuilder (mockEventBuilder )
512+ .withConfig (projectConfig )
513+ .withErrorHandler (mockErrorHandler )
514+ .build ();
515+
516+ Map <String , String > testParams = new HashMap <String , String >();
517+ testParams .put ("test" , "params" );
518+ LogEvent logEventToDispatch = new LogEvent (RequestMethod .GET , "test_url" , testParams , "" );
519+ when (mockEventBuilder .createImpressionEvent (eq (projectConfig ), eq (activatedExperiment ), eq (bucketedVariation ),
520+ eq ("userId" ), anyMapOf (String .class , String .class ),
521+ isNull (String .class )))
522+ .thenReturn (logEventToDispatch );
523+
524+ when (mockBucketer .bucket (activatedExperiment , "userId" ))
525+ .thenReturn (bucketedVariation );
526+
527+ // activate the experiment
528+ Map <String , String > attributes = new HashMap <String , String >();
529+ attributes .put (attribute .getKey (), null );
530+ Variation actualVariation = optimizely .activate (activatedExperiment .getKey (), "userId" , attributes );
531+
532+ // verify that the bucketing algorithm was called correctly
533+ verify (mockBucketer ).bucket (activatedExperiment , "userId" );
534+ assertThat (actualVariation , is (bucketedVariation ));
535+
536+ // setup the attribute map captor (so we can verify its content)
537+ ArgumentCaptor <Map > attributeCaptor = ArgumentCaptor .forClass (Map .class );
538+ verify (mockEventBuilder ).createImpressionEvent (eq (projectConfig ), eq (activatedExperiment ),
539+ eq (bucketedVariation ), eq ("userId" ), attributeCaptor .capture (),
540+ isNull (String .class ));
541+
542+ Map <String , String > actualValue = attributeCaptor .getValue ();
543+ assertThat (actualValue , hasEntry (attribute .getKey (), null ));
544+
545+ // verify that dispatchEvent was called with the correct LogEvent object
546+ verify (mockEventHandler ).dispatchEvent (logEventToDispatch );
547+ }
548+
440549 /**
441550 * Verify that {@link Optimizely#activate(String, String)} returns null when the experiment id corresponds to a
442551 * non-running experiment.
@@ -773,7 +882,6 @@ public void trackEventWithUnknownEventKeyAndRaiseExceptionErrorHandler() throws
773882 * Verify that {@link Optimizely#track(String, String)} passes through attributes.
774883 */
775884 @ Test
776- @ SuppressWarnings ("unchecked" )
777885 public void trackEventWithAttributes () throws Exception {
778886 String datafile = validConfigJsonV3 ();
779887 ProjectConfig projectConfig = validProjectConfigV3 ();
@@ -821,14 +929,118 @@ public void trackEventWithAttributes() throws Exception {
821929 verify (mockEventHandler ).dispatchEvent (logEventToDispatch );
822930 }
823931
932+ /**
933+ * Verify that {@link Optimizely#track(String, String)} ignores null attributes.
934+ */
935+ @ Test
936+ @ SuppressFBWarnings (
937+ value ="NP_NONNULL_PARAM_VIOLATION" ,
938+ justification ="testing nullness contract violation" )
939+ public void trackEventWithNullAttributes () throws Exception {
940+ String datafile = noAudienceProjectConfigJsonV3 ();
941+ ProjectConfig projectConfig = noAudienceProjectConfigV3 ();
942+ EventType eventType = projectConfig .getEventTypes ().get (0 );
943+
944+ // setup a mock event builder to return expected conversion params
945+ EventBuilder mockEventBuilder = mock (EventBuilder .class );
946+
947+ Optimizely optimizely = Optimizely .builder (datafile , mockEventHandler )
948+ .withBucketing (mockBucketer )
949+ .withEventBuilder (mockEventBuilder )
950+ .withConfig (projectConfig )
951+ .withErrorHandler (mockErrorHandler )
952+ .build ();
953+
954+ Map <String , String > testParams = new HashMap <String , String >();
955+ testParams .put ("test" , "params" );
956+ LogEvent logEventToDispatch = new LogEvent (RequestMethod .GET , "test_url" , testParams , "" );
957+ when (mockEventBuilder .createConversionEvent (eq (projectConfig ), eq (mockBucketer ), eq ("userId" ),
958+ eq (eventType .getId ()), eq (eventType .getKey ()),
959+ eq (Collections .<String , String >emptyMap ()), isNull (Long .class ),
960+ isNull (String .class )))
961+ .thenReturn (logEventToDispatch );
962+
963+ logbackVerifier .expectMessage (Level .INFO , "Tracking event \" clicked_cart\" for user \" userId\" ." );
964+ logbackVerifier .expectMessage (Level .DEBUG , "Dispatching conversion event to URL test_url with params " +
965+ testParams + " and payload \" \" " );
966+
967+ // call track
968+ Map <String , String > attributes = null ;
969+ optimizely .track (eventType .getKey (), "userId" , attributes );
970+
971+ logbackVerifier .expectMessage (Level .WARN , "Attributes is null when non-null was expected. Defaulting to an empty attributes map." );
972+
973+ // setup the attribute map captor (so we can verify its content)
974+ ArgumentCaptor <Map > attributeCaptor = ArgumentCaptor .forClass (Map .class );
975+
976+ // verify that the event builder was called with the expected attributes
977+ verify (mockEventBuilder ).createConversionEvent (eq (projectConfig ), eq (mockBucketer ), eq ("userId" ),
978+ eq (eventType .getId ()), eq (eventType .getKey ()),
979+ attributeCaptor .capture (), isNull (Long .class ),
980+ isNull (String .class ));
981+
982+ Map <String , String > actualValue = attributeCaptor .getValue ();
983+ assertThat (actualValue , is (Collections .<String , String >emptyMap ()));
984+
985+ verify (mockEventHandler ).dispatchEvent (logEventToDispatch );
986+ }
987+
988+ /**
989+ * Verify that {@link Optimizely#track(String, String)} gracefully handles null attribute values.
990+ */
991+ @ Test
992+ public void trackEventWithNullAttributeValues () throws Exception {
993+ String datafile = validConfigJsonV3 ();
994+ ProjectConfig projectConfig = validProjectConfigV3 ();
995+ EventType eventType = projectConfig .getEventTypes ().get (0 );
996+
997+ // setup a mock event builder to return expected conversion params
998+ EventBuilder mockEventBuilder = mock (EventBuilder .class );
999+
1000+ Optimizely optimizely = Optimizely .builder (datafile , mockEventHandler )
1001+ .withBucketing (mockBucketer )
1002+ .withEventBuilder (mockEventBuilder )
1003+ .withConfig (projectConfig )
1004+ .withErrorHandler (mockErrorHandler )
1005+ .build ();
1006+
1007+ Map <String , String > testParams = new HashMap <String , String >();
1008+ testParams .put ("test" , "params" );
1009+ LogEvent logEventToDispatch = new LogEvent (RequestMethod .GET , "test_url" , testParams , "" );
1010+ when (mockEventBuilder .createConversionEvent (eq (projectConfig ), eq (mockBucketer ), eq ("userId" ),
1011+ eq (eventType .getId ()), eq (eventType .getKey ()),
1012+ anyMapOf (String .class , String .class ), isNull (Long .class ),
1013+ isNull (String .class )))
1014+ .thenReturn (logEventToDispatch );
1015+
1016+ logbackVerifier .expectMessage (Level .INFO , "Tracking event \" clicked_cart\" for user \" userId\" ." );
1017+ logbackVerifier .expectMessage (Level .DEBUG , "Dispatching conversion event to URL test_url with params " +
1018+ testParams + " and payload \" \" " );
1019+
1020+ // call track
1021+ Map <String , String > attributes = new HashMap <String , String >();
1022+ attributes .put ("test" , null );
1023+ optimizely .track (eventType .getKey (), "userId" , attributes );
1024+
1025+ // setup the attribute map captor (so we can verify its content)
1026+ ArgumentCaptor <Map > attributeCaptor = ArgumentCaptor .forClass (Map .class );
1027+
1028+ // verify that the event builder was called with the expected attributes
1029+ verify (mockEventBuilder ).createConversionEvent (eq (projectConfig ), eq (mockBucketer ), eq ("userId" ),
1030+ eq (eventType .getId ()), eq (eventType .getKey ()),
1031+ attributeCaptor .capture (), isNull (Long .class ),
1032+ isNull (String .class ));
1033+
1034+ verify (mockEventHandler ).dispatchEvent (logEventToDispatch );
1035+ }
1036+
8241037 /**
8251038 * Verify that {@link Optimizely#track(String, String)} handles the case where an unknown attribute
8261039 * (i.e., not in the config) is passed through.
8271040 *
8281041 * In this case, the track event call should remove the unknown attribute from the given map.
8291042 */
8301043 @ Test
831- @ SuppressWarnings ("unchecked" )
8321044 public void trackEventWithUnknownAttribute () throws Exception {
8331045 String datafile = validConfigJsonV3 ();
8341046 ProjectConfig projectConfig = validProjectConfigV3 ();
0 commit comments