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