From 42a285af1677439eb4a769e1a1bc7012fb056b00 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Wed, 15 Nov 2017 16:19:20 +0500 Subject: [PATCH 1/7] Feature Notification Center (#13) --- src/Optimizely/Entity/Variation.php | 33 +- .../InvalidCallbackArgumentCountException.php | 23 + .../InvalidFeatureVariableException.php | 23 + .../InvalidNotificationTypeException.php | 23 + .../Notification/NotificationCenter.php | 203 +++ .../Notification/NotificationType.php | 39 + src/Optimizely/Optimizely.php | 355 +++++- src/Optimizely/ProjectConfig.php | 72 ++ src/Optimizely/Utils/Validator.php | 32 + src/Optimizely/Utils/VariableTypeUtils.php | 59 + .../DecisionServiceTest.php | 348 +++--- .../NotificationCenterTest.php | 685 ++++++++++ tests/OptimizelyTest.php | 1106 +++++++++++++++-- tests/TestData.php | 24 + tests/UtilsTests/ValidatorTest.php | 28 + tests/UtilsTests/VariableTypeUtilsTest.php | 92 ++ 16 files changed, 2861 insertions(+), 284 deletions(-) create mode 100644 src/Optimizely/Exceptions/InvalidCallbackArgumentCountException.php create mode 100644 src/Optimizely/Exceptions/InvalidFeatureVariableException.php create mode 100644 src/Optimizely/Exceptions/InvalidNotificationTypeException.php create mode 100644 src/Optimizely/Notification/NotificationCenter.php create mode 100644 src/Optimizely/Notification/NotificationType.php create mode 100644 src/Optimizely/Utils/VariableTypeUtils.php create mode 100644 tests/NotificationTests/NotificationCenterTest.php create mode 100644 tests/UtilsTests/VariableTypeUtilsTest.php diff --git a/src/Optimizely/Entity/Variation.php b/src/Optimizely/Entity/Variation.php index a1f20f1b..88b408dc 100644 --- a/src/Optimizely/Entity/Variation.php +++ b/src/Optimizely/Entity/Variation.php @@ -52,11 +52,7 @@ public function __construct($id = null, $key = null, $variableUsageInstances = [ $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances, null, VariableUsage::class); - if(!empty($this->_variableUsageInstances)){ - foreach(array_values($this->_variableUsageInstances) as $variableUsage){ - $_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; - } - } + $this->generateVariableIdToVariableUsageMap(); } /** @@ -91,16 +87,41 @@ public function setKey($key) $this->_key = $key; } + /** + * @return [VariableUsage] Variable usage instances in this variation + */ public function getVariables(){ return $this->_variableUsageInstances; } + /** + * @param string Variable ID + * + * @return VariableUsage Variable usage instance corresponding to given variable ID + */ + public function getVariableUsageById($variableId){ + if(isset($this->_variableIdToVariableUsageInstanceMap[$variableId])) + return $this->_variableIdToVariableUsageInstanceMap[$variableId]; + else + return null; + } + + /** + * @param [VariableUsage] array of variable usage instances + */ public function setVariables($variableUsageInstances){ $this->_variableUsageInstances = ConfigParser::generateMap($variableUsageInstances, null , VariableUsage::class); + $this->generateVariableIdToVariableUsageMap(); + } + /** + * Generates variable ID to Variable usage instance map + * from variable usage instances + */ + private function generateVariableIdToVariableUsageMap(){ if(!empty($this->_variableUsageInstances)){ foreach(array_values($this->_variableUsageInstances) as $variableUsage){ - $_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; + $this->_variableIdToVariableUsageInstanceMap[$variableUsage->getId()] = $variableUsage; } } } diff --git a/src/Optimizely/Exceptions/InvalidCallbackArgumentCountException.php b/src/Optimizely/Exceptions/InvalidCallbackArgumentCountException.php new file mode 100644 index 00000000..9813dfe7 --- /dev/null +++ b/src/Optimizely/Exceptions/InvalidCallbackArgumentCountException.php @@ -0,0 +1,23 @@ +_notificationId = 1; + $this->_notifications = []; + foreach (array_values(NotificationType::getAll()) as $type) { + $this->_notifications[$type] = []; + } + + $this->_logger = $logger; + $this->_errorHandler = $errorHandler; + } + + public function getNotificationId() + { + return $this->_notificationId; + } + + public function getNotifications() + { + return $this->_notifications; + } + + /** + * Adds a notification callback for a notification type to the notification center + * @param string $notification_type One of the constants defined in NotificationType + * @param string $notification_callback A valid PHP callback + * + * @return null Given invalid notification type/callback + * -1 Given callback has been already added + * int Notification ID for the added callback + */ + public function addNotificationListener($notification_type, $notification_callback) + { + if (!NotificationType::isNotificationTypeValid($notification_type)) { + $this->_logger->log(Logger::ERROR, "Invalid notification type."); + $this->_errorHandler->handleError(new InvalidNotificationTypeException('Invalid notification type.')); + return null; + } + + if (!is_callable($notification_callback)) { + $this->_logger->log(Logger::ERROR, "Invalid notification callback."); + return null; + } + + foreach (array_values($this->_notifications[$notification_type]) as $callback) { + if ($notification_callback == $callback) { + // Note: anonymous methods sent with the same body will be re-added. + // Only variable and object methods can be checked for duplication + $this->_logger->log(Logger::DEBUG, "Callback already added for notification type '{$notification_type}'."); + return -1; + } + } + + $this->_notifications[$notification_type][$this->_notificationId] = $notification_callback; + $this->_logger->log(Logger::INFO, "Callback added for notification type '{$notification_type}'."); + $returnVal = $this->_notificationId++; + return $returnVal; + } + + /** + * Removes notification callback from the notification center + * @param int $notification_id notification IT + * + * @return true When callback removed + * false When no callback found for the given notification ID + */ + public function removeNotificationListener($notification_id) + { + foreach ($this->_notifications as $notification_type => $notifications) { + foreach (array_keys($notifications) as $id) { + if ($notification_id == $id) { + unset($this->_notifications[$notification_type][$id]); + $this->_logger->log(Logger::INFO, "Callback with notification ID '{$notification_id}' has been removed."); + return true; + } + } + } + + $this->_logger->log(Logger::DEBUG, "No Callback found with notification ID '{$notification_id}'."); + return false; + } + + /** + * Removes all notification callbacks for the given notification type + * @param string $notification_type One of the constants defined in NotificationType + * + */ + public function clearNotifications($notification_type) + { + if (!NotificationType::isNotificationTypeValid($notification_type)) { + $this->_logger->log(Logger::ERROR, "Invalid notification type."); + $this->_errorHandler->handleError(new InvalidNotificationTypeException('Invalid notification type.')); + return; + } + + $this->_notifications[$notification_type] = []; + $this->_logger->log(Logger::INFO, "All callbacks for notification type '{$notification_type}' have been removed."); + } + + /** + * Removes all notifications for all notification types + * from the notification center + * + */ + public function cleanAllNotifications() + { + foreach (array_values(NotificationType::getAll()) as $type) { + $this->_notifications[$type] = []; + } + } + + /** + * Executes all registered callbacks for the given notification type + * @param [type] $notification_type One of the constants defined in NotificationType + * @param array $args Array of items to pass as arguments to the callback + * + */ + public function fireNotifications($notification_type, array $args = []) + { + if (!isset($this->_notifications[$notification_type])) { + // No exception thrown and error logged since this method will be called from + // within the SDK + return; + } + + /** + * Note: Before PHP 7, if the callback in call_user_func is called with less number of arguments than expected, + * a warning is issued but the method is still executed with assigning null to the remaining + * arguments. From PHP 7, ArgumentCountError is thrown in such case and the method isn't executed. + * Therefore, we set error handler for warnings so that we raise an exception and notify the + * user that the registered callback has more number of arguments than + * expected. This should be done to keep a consistent behavior across all PHP versions. + */ + + set_error_handler(array($this, 'reportArgumentCountError'), E_WARNING); + + foreach (array_values($this->_notifications[$notification_type]) as $callback) { + try { + call_user_func_array($callback, $args); + } catch (ArgumentCountError $e) { + $this->reportArgumentCountError(); + } catch (Throwable $e) { + $this->_logger->log(Logger::ERROR, "Problem calling notify callback."); + } catch (Exception $e) { + $this->_logger->log(Logger::ERROR, "Problem calling notify callback."); + } + } + + restore_error_handler(); + } + + /** + * Logs and raises an exception when registered callback expects more number of arguments when executed + * + */ + public function reportArgumentCountError() + { + $this->_logger->log(Logger::ERROR, "Problem calling notify callback."); + $this->_errorHandler->handleError( + new InvalidCallbackArgumentCountException('Registered callback expects more number of arguments than the actual number') + ); + } +} diff --git a/src/Optimizely/Notification/NotificationType.php b/src/Optimizely/Notification/NotificationType.php new file mode 100644 index 00000000..28c8ca50 --- /dev/null +++ b/src/Optimizely/Notification/NotificationType.php @@ -0,0 +1,39 @@ +getConstants()); + + return in_array($notification_type, $notificationTypeList); + } + + public static function getAll() + { + $oClass = new \ReflectionClass(__CLASS__); + return $oClass->getConstants(); + } +} diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 64503011..75c315d7 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -22,6 +22,11 @@ use Throwable; use Monolog\Logger; use Optimizely\DecisionService\DecisionService; +use Optimizely\DecisionService\FeatureDecision; +use Optimizely\Entity\Experiment; +use Optimizely\Entity\FeatureFlag; +use Optimizely\Entity\FeatureVariable; +use Optimizely\Entity\Rollout; use Optimizely\Logger\DefaultLogger; use Optimizely\ErrorHandler\ErrorHandlerInterface; use Optimizely\ErrorHandler\NoOpErrorHandler; @@ -30,9 +35,12 @@ use Optimizely\Event\Dispatcher\EventDispatcherInterface; use Optimizely\Logger\LoggerInterface; use Optimizely\Logger\NoOpLogger; +use Optimizely\Notification\NotificationCenter; +use Optimizely\Notification\NotificationType; use Optimizely\UserProfile\UserProfileServiceInterface; use Optimizely\Utils\EventTagUtils; use Optimizely\Utils\Validator; +use Optimizely\Utils\VariableTypeUtils; /** * Class Optimizely @@ -76,6 +84,11 @@ class Optimizely */ private $_logger; + /** + * @var NotificationCenter + */ + private $_notificationCenter; + /** * Optimizely constructor for managing Full Stack PHP projects. * @@ -123,6 +136,7 @@ public function __construct($datafile, $this->_eventBuilder = new EventBuilder(); $this->_decisionService = new DecisionService($this->_logger, $this->_config, $userProfileService); + $this->_notificationCenter = new NotificationCenter($this->_logger, $this->_errorHandler); } /** @@ -174,10 +188,10 @@ private function validateUserInputs($attributes, $eventTags = null) { * is one that is in "Running" state and into which the user has been bucketed. * * @param $event string Event key representing the event which needs to be recorded. - * @param $userId string ID for user. + * @param $user string ID for user. * @param $attributes array Attributes of the user. * - * @return array Of objects where each object contains the ID of the experiment to track and the ID of the variation the user is bucketed into. + * @return Array Of objects where each object contains the ID of the experiment to track and the ID of the variation the user is bucketed into. */ private function getValidExperimentsForEvent($event, $userId, $attributes = null) { $validExperiments = []; @@ -199,6 +213,52 @@ private function getValidExperimentsForEvent($event, $userId, $attributes = null return $validExperiments; } + /** + * @param string Experiment key + * @param string Variation key + * @param string User ID + * @param array Associative array of user attributes + */ + protected function sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes) + { + $impressionEvent = $this->_eventBuilder + ->createImpressionEvent($this->_config, $experimentKey, $variationKey, $userId, $attributes); + $this->_logger->log(Logger::INFO, sprintf('Activating user "%s" in experiment "%s".', $userId, $experimentKey)); + $this->_logger->log( + Logger::DEBUG, + sprintf( + 'Dispatching impression event to URL %s with params %s.', + $impressionEvent->getUrl(), + http_build_query($impressionEvent->getParams()) + ) + ); + + try { + $this->_eventDispatcher->dispatchEvent($impressionEvent); + } catch (Throwable $exception) { + $this->_logger->log(Logger::ERROR, sprintf( + 'Unable to dispatch impression event. Error %s', + $exception->getMessage() + )); + } catch (Exception $exception) { + $this->_logger->log(Logger::ERROR, sprintf( + 'Unable to dispatch impression event. Error %s', + $exception->getMessage() + )); + } + + $this->_notificationCenter->fireNotifications( + NotificationType::DECISION, + array( + $this->_config->getExperimentFromKey($experimentKey), + $userId, + $attributes, + $this->_config->getVariationFromKey($experimentKey, $variationKey), + $impressionEvent + ) + ); + } + /** * Buckets visitor and sends impression event to Optimizely. * @@ -218,31 +278,11 @@ public function activate($experimentKey, $userId, $attributes = null) $variationKey = $this->getVariation($experimentKey, $userId, $attributes); if (is_null($variationKey)) { $this->_logger->log(Logger::INFO, sprintf('Not activating user "%s".', $userId)); - return $variationKey; - } - - $impressionEvent = $this->_eventBuilder - ->createImpressionEvent($this->_config, $experimentKey, $variationKey, $userId, $attributes); - $this->_logger->log(Logger::INFO, sprintf('Activating user "%s" in experiment "%s".', $userId, $experimentKey)); - $this->_logger->log( - Logger::DEBUG, - sprintf('Dispatching impression event to URL %s with params %s.', - $impressionEvent->getUrl(), http_build_query($impressionEvent->getParams()) - ) - ); - - try { - $this->_eventDispatcher->dispatchEvent($impressionEvent); - } - catch (Throwable $exception) { - $this->_logger->log(Logger::ERROR, sprintf( - 'Unable to dispatch impression event. Error %s', $exception->getMessage())); - } - catch (Exception $exception) { - $this->_logger->log(Logger::ERROR, sprintf( - 'Unable to dispatch impression event. Error %s', $exception->getMessage())); + return null; } + $this->sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes); + return $variationKey; } @@ -313,6 +353,17 @@ public function track($eventKey, $userId, $attributes = null, $eventTags = null) 'Unable to dispatch conversion event. Error %s', $exception->getMessage())); } + $this->_notificationCenter->fireNotifications( + NotificationType::TRACK, + array( + $eventKey, + $userId, + $attributes, + $eventTags, + $conversionEvent + ) + ); + } else { $this->_logger->log( Logger::INFO, @@ -387,4 +438,258 @@ public function getForcedVariation($experimentKey, $userId) return null; } } + + /** + * Determine whether a feature is enabled. + * Sends an impression event if the user is bucketed into an experiment using the feature. + * @param string Feature flag key + * @param string User ID + * @param array Associative array of user attributes + * + * @return boolean + */ + public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null) + { + if (!$this->_isValid) { + $this->_logger->log(Logger::ERROR, "Datafile has invalid format. Failing '".__FUNCTION__."'."); + return null; + } + + if (!$featureFlagKey) { + $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty."); + return null; + } + + if (!$userId) { + $this->_logger->log(Logger::ERROR, "User ID cannot be empty."); + return null; + } + + $feature_flag = $this->_config->getFeatureFlagFromKey($featureFlagKey); + if ($feature_flag == new FeatureFlag) { + // Error logged in ProjectConfig - getFeatureFlagFromKey + return null; + } + + //validate feature flag + if (!Validator::isFeatureFlagValid($this->_config, $feature_flag)) { + return null; + } + + $decision = $this->_decisionService->getVariationForFeature($feature_flag, $userId, $attributes); + if (!$decision) { + $this->_logger->log(Logger::INFO, "Feature Flag '{$featureFlagKey}' is not enabled for user '{$userId}'."); + return false; + } + + $experiment_id = $decision->getExperimentId(); + $variation_id = $decision->getVariationId(); + + if ($decision->getSource() == FeatureDecision::DECISION_SOURCE_EXPERIMENT) { + $experiment = $this->_config->getExperimentFromId($experiment_id); + $variation = $this->_config->getVariationFromId($experiment->getKey(), $variation_id); + + $this->sendImpressionEvent($experiment->getKey(), $variation->getKey(), $userId, $attributes); + } else { + $variation = $this->_config->getVariationFromRolloutExperiment($experiment_id, $variation_id); + $this->_logger->log(Logger::INFO, "The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'."); + } + + $this->_logger->log(Logger::INFO, "Feature Flag '{$featureFlagKey}' is enabled for user '{$userId}'."); + + $this->_notificationCenter->fireNotifications( + NotificationType::FEATURE_ACCESSED, + array( + $featureFlagKey, + $userId, + $attributes, + $variation + ) + ); + + return true; + } + + /** + * Get the string value of the specified variable in the feature flag. + * @param string Feature flag key + * @param string Variable key + * @param string User ID + * @param array Associative array of user attributes + * @param string Variable type + * + * @return string Feature variable value / null + */ + public function getFeatureVariableValueForType( + $featureFlagKey, + $variableKey, + $userId, + $attributes = null, + $variableType = null + ) { + if (!$featureFlagKey) { + $this->_logger->log(Logger::ERROR, "Feature Flag key cannot be empty."); + return null; + } + + if (!$variableKey) { + $this->_logger->log(Logger::ERROR, "Variable key cannot be empty."); + return null; + } + + if (!$userId) { + $this->_logger->log(Logger::ERROR, "User ID cannot be empty."); + return null; + } + + $feature_flag = $this->_config->getFeatureFlagFromKey($featureFlagKey); + if ($feature_flag && (!$feature_flag->getId())) { + // Error logged in ProjectConfig - getFeatureFlagFromKey + return null; + } + + $variable = $this->_config->getFeatureVariableFromKey($featureFlagKey, $variableKey); + if (!$variable) { + // Error message logged in ProjectConfig- getFeatureVariableFromKey + return null; + } + + if ($variableType != $variable->getType()) { + $this->_logger->log( + Logger::ERROR, + "Variable is of type '{$variable->getType()}', but you requested it as type '{$variableType}'." + ); + return null; + } + + $decision = $this->_decisionService->getVariationForFeature($feature_flag, $userId, $attributes); + $variable_value = $variable->getDefaultValue(); + + if (!$decision) { + $this->_logger->log(Logger::INFO, "User '{$userId}'is not in any variation, ". + "returning default value '{$variable_value}'."); + } else { + $experiment_id = $decision->getExperimentId(); + $variation_id = $decision->getVariationId(); + $experiment = $this->_config->getExperimentFromId($experiment_id); + $variation = $this->_config->getVariationFromId($experiment->getKey(), $variation_id); + $variable_usage = $variation->getVariableUsageById($variable->getId()); + if ($variable_usage) { + $variable_value = $variable_usage->getValue(); + $this->_logger->log( + Logger::INFO, + "Returning variable value '{$variable_value}' for variation '{$variation->getKey()}' ". + "of feature flag '{$featureFlagKey}'" + ); + } else { + $this->_logger->log( + Logger::INFO, + "Variable '{$variableKey}' is not used in variation '{$variation->getKey()}', ". + "returning default value '{$variable_value}'." + ); + } + } + + return $variable_value; + } + + /** + * Get the Boolean value of the specified variable in the feature flag. + * @param string Feature flag key + * @param string Variable key + * @param string User ID + * @param array Associative array of user attributes + * + * @return string boolean variable value / null + */ + public function getFeatureVariableBoolean($featureFlagKey, $variableKey, $userId, $attributes = null) + { + $variable_value = $this->getFeatureVariableValueForType( + $featureFlagKey, + $variableKey, + $userId, + $attributes, + FeatureVariable::BOOLEAN_TYPE + ); + + if (!is_null($variable_value)) { + return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::BOOLEAN_TYPE, $this->_logger); + } + + return $variable_value; + } + + /** + * Get the Integer value of the specified variable in the feature flag. + * @param string Feature flag key + * @param string Variable key + * @param string User ID + * @param array Associative array of user attributes + * + * @return string integer variable value / null + */ + public function getFeatureVariableInteger($featureFlagKey, $variableKey, $userId, $attributes = null) + { + $variable_value = $this->getFeatureVariableValueForType( + $featureFlagKey, + $variableKey, + $userId, + $attributes, + FeatureVariable::INTEGER_TYPE + ); + + if (!is_null($variable_value)) { + return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::INTEGER_TYPE, $this->_logger); + } + + return $variable_value; + } + + /** + * Get the Double value of the specified variable in the feature flag. + * @param string Feature flag key + * @param string Variable key + * @param string User ID + * @param array Associative array of user attributes + * + * @return string double variable value / null + */ + public function getFeatureVariableDouble($featureFlagKey, $variableKey, $userId, $attributes = null) + { + $variable_value = $this->getFeatureVariableValueForType( + $featureFlagKey, + $variableKey, + $userId, + $attributes, + FeatureVariable::DOUBLE_TYPE + ); + + if (!is_null($variable_value)) { + return VariableTypeUtils::castStringToType($variable_value, FeatureVariable::DOUBLE_TYPE, $this->_logger); + } + + return $variable_value; + } + + /** + * Get the String value of the specified variable in the feature flag. + * @param string Feature flag key + * @param string Variable key + * @param string User ID + * @param array Associative array of user attributes + * + * @return string variable value / null + */ + public function getFeatureVariableString($featureFlagKey, $variableKey, $userId, $attributes = null) + { + $variable_value = $this->getFeatureVariableValueForType( + $featureFlagKey, + $variableKey, + $userId, + $attributes, + FeatureVariable::STRING_TYPE + ); + + return $variable_value; + } } diff --git a/src/Optimizely/ProjectConfig.php b/src/Optimizely/ProjectConfig.php index f4da41ae..7388e106 100644 --- a/src/Optimizely/ProjectConfig.php +++ b/src/Optimizely/ProjectConfig.php @@ -23,6 +23,7 @@ use Optimizely\Entity\Event; use Optimizely\Entity\Experiment; use Optimizely\Entity\FeatureFlag; +use Optimizely\Entity\FeatureVariable; use Optimizely\Entity\Group; use Optimizely\Entity\Rollout; use Optimizely\Entity\Variation; @@ -32,6 +33,7 @@ use Optimizely\Exceptions\InvalidEventException; use Optimizely\Exceptions\InvalidExperimentException; use Optimizely\Exceptions\InvalidFeatureFlagException; +use Optimizely\Exceptions\InvalidFeatureVariableException; use Optimizely\Exceptions\InvalidGroupException; use Optimizely\Exceptions\InvalidRolloutException; use Optimizely\Exceptions\InvalidVariationException; @@ -154,6 +156,12 @@ class ProjectConfig */ private $_rolloutIdMap; + /** + * Feature Flag key to Feature Variable key to Feature Variable map + * @var > + */ + private $_featureFlagVariableMap; + /** * ProjectConfig constructor to load and set project configuration data. * @@ -222,6 +230,13 @@ public function __construct($datafile, $logger, $errorHandler) foreach(array_values($this->_featureFlags) as $featureFlag){ $this->_featureKeyMap[$featureFlag->getKey()] = $featureFlag; } + + if($this->_featureKeyMap){ + foreach($this->_featureKeyMap as $featureKey => $featureFlag){ + $this->_featureFlagVariableMap[$featureKey] = ConfigParser::generateMap( + $featureFlag->getVariables(), 'key', FeatureVariable::class); + } + } } /** @@ -432,6 +447,63 @@ public function getVariationFromId($experimentKey, $variationId) return new Variation(); } + /** + * Gets the feature variable instance given feature flag key and variable key + * @param string Feature flag key + * @param string Variable key + * + * @return FeatureVariable / null + */ + public function getFeatureVariableFromKey($featureFlagKey, $variableKey) + { + $feature_flag = $this->getFeatureFlagFromKey($featureFlagKey); + if($feature_flag && !($feature_flag->getKey())) + return null; + + if(isset($this->_featureFlagVariableMap[$featureFlagKey]) && + isset($this->_featureFlagVariableMap[$featureFlagKey][$variableKey])) { + return $this->_featureFlagVariableMap[$featureFlagKey][$variableKey]; + } + + $this->_logger->log(Logger::ERROR, sprintf( + 'No variable key "%s" defined in datafile for feature flag "%s".', $variableKey, $featureFlagKey)); + $this->_errorHandler->handleError( + new InvalidFeatureVariableException('Provided feature variable is not in datafile.')); + return null; + } + + /** + * Gets the variation object from a rollout rule/experiment + * + * Note: This method is needed in the case where we want to fetch a variation inside + * a rollout rule/experiment. Other methods getVariationById/Key can't be used because we do + * not store rollout rules/experiments in our experiment/variation ID maps. + * No need to throw an exception, since this method will not be directly called as a result of + * a public API method param and will receive pre-verified experimentId and variationId + * + * @param string $experimentId Experiment ID + * @param string $variationId VariationID + * + * @return Variation Variation object corresponding to given experiment and variation IDs + */ + public function getVariationFromRolloutExperiment($experimentId, $variationId){ + foreach($this->_rollouts as $rollout){ + $experiments = $rollout->getExperiments(); + foreach($experiments as $experiment){ + if($experiment->getId() == $experimentId) + break 2; + } + } + + $variations = $experiment->getVariations(); + foreach($variations as $variation){ + if($variation->getId() == $variationId) + return $variation; + } + + return null; + } + public function isVariationIdValid($experimentKey, $variationId) { return isset($this->_variationIdMap[$experimentKey]) && diff --git a/src/Optimizely/Utils/Validator.php b/src/Optimizely/Utils/Validator.php index 1e892c45..2f775fab 100644 --- a/src/Optimizely/Utils/Validator.php +++ b/src/Optimizely/Utils/Validator.php @@ -104,4 +104,36 @@ public static function isUserInExperiment($config, $experiment, $userAttributes) return false; } + + /** + * Checks that if there are more than one experiment IDs + * in the feature flag, they must belong to the same mutex group + * + * @param ProjectConfig $config The project config to verify against + * @param FeatureFlag $featureFlag The feature to validate + * + * @return boolean True if feature flag is valid + */ + public static function isFeatureFlagValid($config, $featureFlag) + { + $experimentIds = $featureFlag->getExperimentIds(); + + if (empty($experimentIds)) { + return true; + } + if (sizeof($experimentIds) == 1) { + return true; + } + + $groupId = $config->getExperimentFromId($experimentIds[0])->getGroupId(); + foreach ($experimentIds as $id) { + $experiment = $config->getExperimentFromId($id); + $grpId = $experiment->getGroupId(); + if ($groupId != $grpId) { + return false; + } + } + + return true; + } } diff --git a/src/Optimizely/Utils/VariableTypeUtils.php b/src/Optimizely/Utils/VariableTypeUtils.php new file mode 100644 index 00000000..c5841966 --- /dev/null +++ b/src/Optimizely/Utils/VariableTypeUtils.php @@ -0,0 +1,59 @@ +log(Logger::ERROR, "Unable to cast variable value '{$value}' to type '{$variableType}'."); + } + + return $return_value; + } +} diff --git a/tests/DecisionServiceTests/DecisionServiceTest.php b/tests/DecisionServiceTests/DecisionServiceTest.php index 1efcfa3b..9055d176 100644 --- a/tests/DecisionServiceTests/DecisionServiceTest.php +++ b/tests/DecisionServiceTests/DecisionServiceTest.php @@ -73,9 +73,9 @@ public function setUp() $this->userProvideServiceMock = $this->getMockBuilder(UserProfileServiceInterface::class) ->getMock(); - $this->decisionService = new DecisionService($this->loggerMock, $this->config); + $this->decisionService = new DecisionService($this->loggerMock, $this->config); - $this->decisionServiceMock = $this->getMockBuilder(DecisionService::class) + $this->decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock, $this->config)) ->setMethods(array('getVariation')) ->getMock(); @@ -126,9 +126,9 @@ public function testGetVariationReturnsWhitelistedVariation() $callIndex = 0; $this->bucketerMock->expects($this->never()) ->method('bucket'); - $this->loggerMock->expects($this->at($callIndex++)) + $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, 'User "user1" is not in the forced variation map.'); + ->with(Logger::DEBUG, 'User "user1" is not in the forced variation map.'); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'User "user1" is forced in variation "control" of experiment "test_experiment".'); @@ -158,9 +158,9 @@ public function testGetVariationReturnsWhitelistedVariationForGroupedExperiment( $callIndex = 0; $this->bucketerMock->expects($this->never()) ->method('bucket'); - $this->loggerMock->expects($this->at($callIndex++)) + $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, 'User "user1" is not in the forced variation map.'); + ->with(Logger::DEBUG, 'User "user1" is not in the forced variation map.'); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'User "user1" is forced in variation "group_exp_1_var_1" of experiment "group_experiment_1".'); @@ -277,9 +277,9 @@ public function testGetVariationReturnsStoredVariationIfAvailable() $callIndex = 0; $this->bucketerMock->expects($this->never()) ->method('bucket'); - $this->loggerMock->expects($this->at($callIndex++)) + $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, 'User "not_whitelisted_user" is not in the forced variation map.'); + ->with(Logger::DEBUG, 'User "not_whitelisted_user" is not in the forced variation map.'); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'Returning previously activated variation "control" of experiment "test_experiment" for user "not_whitelisted_user" from user profile.'); @@ -315,9 +315,9 @@ public function testGetVariationBucketsIfNoStoredVariation() $this->bucketerMock->expects($this->once()) ->method('bucket') ->willReturn($expectedVariation); - $this->loggerMock->expects($this->at($callIndex++)) + $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); + ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'No previously activated variation of experiment "test_experiment" for user "testUserId" found in user profile.'); @@ -365,7 +365,7 @@ public function testGetVariationBucketsIfStoredVariationIsInvalid() ->willReturn($expectedVariation); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); + ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'User "testUserId" was previously bucketed into variation with ID "invalid" for experiment "test_experiment", but no matching variation was found for that user. We will re-bucket the user.'); @@ -417,7 +417,7 @@ public function testGetVariationBucketsIfUserProfileServiceLookupThrows() ->willReturn($expectedVariation); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); + ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::ERROR, 'The User Profile Service lookup method failed: I am error.'); @@ -469,7 +469,7 @@ public function testGetVariationBucketsIfUserProfileServiceSaveThrows() ->willReturn($expectedVariation); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') - ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); + ->with(Logger::DEBUG, sprintf('User "%s" is not in the forced variation map.', $userId)); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') ->with(Logger::INFO, 'No user profile found for user with ID "testUserId".'); @@ -580,11 +580,11 @@ public function testGetVariationWithBucketingId() // check forced variation $this->assertTrue($optlyObject->setForcedVariation($this->experimentKey, $userId, $this->variationKeyControl), sprintf('Set variation to "%s" failed.', $this->variationKeyControl)); $variationKey = $optlyObject->getVariation($this->experimentKey, $userId, $userAttributesWithBucketingId); - $this->assertEquals( $this->variationKeyControl, $variationKey); + $this->assertEquals($this->variationKeyControl, $variationKey); // check whitelisted variation $variationKey = $optlyObject->getVariation($this->experimentKey, $this->testUserIdWhitelisted, $userAttributesWithBucketingId); - $this->assertEquals( $this->variationKeyControl, $variationKey); + $this->assertEquals($this->variationKeyControl, $variationKey); // check user profile $storedUserProfile = array( @@ -608,23 +608,23 @@ public function testGetVariationWithBucketingId() $this->assertEquals($this->variationKeyControl, $variationKey, sprintf('Variation "%s" does not match expected user profile variation "%s".', $variationKey, $this->variationKeyControl)); } - //should return nil and log a message when the feature flag's experiment ids array is empty - public function testGetVariationForFeatureExperimentGivenNullExperimentIds(){ - + // should return nil and log a message when the feature flag's experiment ids array is empty + public function testGetVariationForFeatureExperimentGivenNullExperimentIds() + { $feature_flag = $this->config->getFeatureFlagFromKey('empty_feature'); $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::DEBUG, "The feature flag 'empty_feature' is not used in any experiments."); $this->assertSame( - $this->decisionService->getVariationForFeatureExperiment($feature_flag,'user1',[]), - null + null, + $this->decisionService->getVariationForFeatureExperiment($feature_flag, 'user1', []) ); } - //should return nil and log a message when the experiment is not in the datafile - public function testGetVariationForFeatureExperimentGivenExperimentNotInDataFile(){ - + // should return nil and log a message when the experiment is not in the datafile + public function testGetVariationForFeatureExperimentGivenExperimentNotInDataFile() + { $boolean_feature = $this->config->getFeatureFlagFromKey('boolean_feature'); $feature_flag = clone $boolean_feature; // Use any string that is not an experiment id in the data file @@ -636,40 +636,47 @@ public function testGetVariationForFeatureExperimentGivenExperimentNotInDataFile $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::INFO, - "The user 'user1' is not bucketed into any of the experiments using the feature 'boolean_feature'."); + ->with( + Logger::INFO, + "The user 'user1' is not bucketed into any of the experiments using the feature 'boolean_feature'." + ); $this->assertSame( - $this->decisionService->getVariationForFeatureExperiment($feature_flag,'user1',[]), - null + null, + $this->decisionService->getVariationForFeatureExperiment($feature_flag, 'user1', []) ); } - //should return nil and log when the user is not bucketed into the feature flag's experiments - public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBucketed(){ + // should return nil and log when the user is not bucketed into the feature flag's experiments + public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserNotBucketed() + { $multivariate_experiment = $this->config->getExperimentFromKey('test_experiment_multivariate'); $map = [ [$multivariate_experiment, 'user1', [], null] ]; - //make sure the user is not bucketed into the feature experiment + // make sure the user is not bucketed into the feature experiment $this->decisionServiceMock->expects($this->at(0)) ->method('getVariation') ->will($this->returnValueMap($map)); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "The user 'user1' is not bucketed into any of the experiments using the feature 'multi_variate_feature'."); + ->with( + Logger::INFO, + "The user 'user1' is not bucketed into any of the experiments using the feature 'multi_variate_feature'." + ); $feature_flag = $this->config->getFeatureFlagFromKey('multi_variate_feature'); $this->assertSame( - $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []), - null); + null, + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []) + ); } // should return the variation when the user is bucketed into a variation for the experiment on the feature flag - public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucketed(){ + public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsBucketed() + { // return the first variation of the `test_experiment_multivariate` experiment, which is attached to the `multi_variate_feature` $experiment = $this->config->getExperimentFromKey('test_experiment_multivariate'); - $variation = $this->config->getVariationFromId('test_experiment_multivariate','122231'); + $variation = $this->config->getVariationFromId('test_experiment_multivariate', '122231'); $this->decisionServiceMock->expects($this->at(0)) ->method('getVariation') ->will($this->returnValue($variation)); @@ -679,17 +686,20 @@ public function testGetVariationForFeatureExperimentGivenNonMutexGroupAndUserIsB $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "The user 'user1' is bucketed into experiment 'test_experiment_multivariate' of feature 'multi_variate_feature'."); + ->with( + Logger::INFO, + "The user 'user1' is bucketed into experiment 'test_experiment_multivariate' of feature 'multi_variate_feature'." + ); $this->assertEquals( - $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []), - $expected_decision + $expected_decision, + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user1', []) ); } // should return the variation the user is bucketed into when the user is bucketed into one of the experiments - public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed(){ + public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBucketed() + { $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); $variation = $mutex_exp->getVariations()[0]; $this->decisionServiceMock->expects($this->at(0)) @@ -703,16 +713,19 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserIsBuck $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "The user 'user_1' is bucketed into experiment 'group_experiment_1' of feature 'boolean_feature'."); + ->with( + Logger::INFO, + "The user 'user_1' is bucketed into experiment 'group_experiment_1' of feature 'boolean_feature'." + ); $this->assertEquals( - $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []), - $expected_decision + $expected_decision, + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []) ); } - // should return nil and log a message when the user is not bucketed into any of the mutex experiments - public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed(){ + // should return nil and log a message when the user is not bucketed into any of the mutex experiments + public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBucketed() + { $mutex_exp = $this->config->getExperimentFromKey('group_experiment_1'); $variation = $mutex_exp->getVariations()[0]; $this->decisionServiceMock->expects($this->at(0)) @@ -724,44 +737,47 @@ public function testGetVariationForFeatureExperimentGivenMutexGroupAndUserNotBuc $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "The user 'user_1' is not bucketed into any of the experiments using the feature 'boolean_feature'."); + ->with( + Logger::INFO, + "The user 'user_1' is not bucketed into any of the experiments using the feature 'boolean_feature'." + ); $this->assertEquals( - $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []), - null + null, + $this->decisionServiceMock->getVariationForFeatureExperiment($feature_flag, 'user_1', []) ); } - //should return the bucketed experiment and variation - public function testGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment(){ - - $decisionServiceMock = $this->getMockBuilder(DecisionService::class) - ->setConstructorArgs(array($this->loggerMock, $this->config)) - ->setMethods(array('getVariationForFeatureExperiment')) - ->getMock(); - - $feature_flag = $this->config->getFeatureFlagFromKey('string_single_variable_feature'); - $expected_experiment_id = $feature_flag->getExperimentIds()[0]; - $expected_experiment = $this->config->getExperimentFromId($expected_experiment_id); - $expected_variation = $expected_experiment->getVariations()[0]; - $expected_decision = [ - 'experiment' => $expected_experiment, - 'variation' => $expected_variation - ]; + // should return the bucketed experiment and variation + public function testGetVariationForFeatureWhenTheUserIsBucketedIntoFeatureExperiment() + { + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->config)) + ->setMethods(array('getVariationForFeatureExperiment')) + ->getMock(); + + $feature_flag = $this->config->getFeatureFlagFromKey('string_single_variable_feature'); + $expected_experiment_id = $feature_flag->getExperimentIds()[0]; + $expected_experiment = $this->config->getExperimentFromId($expected_experiment_id); + $expected_variation = $expected_experiment->getVariations()[0]; + $expected_decision = new FeatureDecision( + $expected_experiment->getId(), + $expected_variation->getId(), + FeatureDecision::DECISION_SOURCE_EXPERIMENT + ); $decisionServiceMock->expects($this->at(0)) ->method('getVariationForFeatureExperiment') ->will($this->returnValue($expected_decision)); $this->assertEquals( - $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), - $expected_decision + $expected_decision, + $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []) ); } // should return the bucketed variation and null experiment - public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ - + public function testGetVariationForFeatureWhenBucketedToFeatureRollout() + { $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock, $this->config)) ->setMethods(array('getVariationForFeatureExperiment','getVariationForFeatureRollout')) @@ -773,7 +789,11 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ $experiment = $rollout->getExperiments()[0]; $expected_variation = $experiment->getVariations()[0]; $expected_decision = new FeatureDecision( - $experiment->getId(), $expected_variation->getId(), FeatureDecision::DECISION_SOURCE_ROLLOUT); + $experiment->getId(), + $expected_variation->getId(), + FeatureDecision::DECISION_SOURCE_ROLLOUT + ); + $decisionServiceMock ->method('getVariationForFeatureExperiment') @@ -785,17 +805,20 @@ public function testGetVariationForFeatureWhenBucketedToFeatureRollout(){ $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "User 'user_1' is bucketed into rollout for feature flag 'string_single_variable_feature'."); + ->with( + Logger::INFO, + "User 'user_1' is bucketed into rollout for feature flag 'string_single_variable_feature'." + ); $this->assertEquals( - $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), - $expected_decision + $expected_decision, + $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []) ); } // should return null - public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExperimentNorToFeatureRollout(){ + public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatureExperimentNorToFeatureRollout() + { $decisionServiceMock = $this->getMockBuilder(DecisionService::class) ->setConstructorArgs(array($this->loggerMock, $this->config)) ->setMethods(array('getVariationForFeatureExperiment','getVariationForFeatureRollout')) @@ -803,7 +826,7 @@ public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatur $feature_flag = $this->config->getFeatureFlagFromKey('string_single_variable_feature'); - $decisionServiceMock + $decisionServiceMock ->method('getVariationForFeatureExperiment') ->will($this->returnValue(null)); @@ -813,56 +836,65 @@ public function testGetVariationForFeatureWhenTheUserIsNeitherBucketedIntoFeatur $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, - "User 'user_1' is not bucketed into rollout for feature flag 'string_single_variable_feature'."); + ->with( + Logger::INFO, + "User 'user_1' is not bucketed into rollout for feature flag 'string_single_variable_feature'." + ); $this->assertEquals( - $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []), - null + null, + $decisionServiceMock->getVariationForFeature($feature_flag, 'user_1', []) ); } // should return null - public function testGetVariationForFeatureRolloutWhenNoRolloutIsAssociatedToFeatureFlag(){ + public function testGetVariationForFeatureRolloutWhenNoRolloutIsAssociatedToFeatureFlag() + { // No rollout id is associated to boolean_feature - $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); - $this->loggerMock->expects($this->at(0)) + $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, - "Feature flag 'boolean_feature' is not used in a rollout."); + ->with( + Logger::DEBUG, + "Feature flag 'boolean_feature' is not used in a rollout." + ); $this->assertEquals( - $this->decisionServiceMock->getVariationForFeatureRollout($feature_flag, 'user_1', []), - null + null, + $this->decisionServiceMock->getVariationForFeatureRollout($feature_flag, 'user_1', []) ); } // should return null - public function testGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile(){ - $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); - $feature_flag = clone $feature_flag; - // Set any string which is not a rollout id in the data file - $feature_flag->setRolloutId('invalid_rollout_id'); + public function testGetVariationForFeatureRolloutWhenRolloutIsNotInDataFile() + { + $feature_flag = $this->config->getFeatureFlagFromKey('boolean_feature'); + $feature_flag = clone $feature_flag; + // Set any string which is not a rollout id in the data file + $feature_flag->setRolloutId('invalid_rollout_id'); - $this->loggerMock->expects($this->at(0)) + $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::ERROR, - 'Rollout with ID "invalid_rollout_id" is not in the datafile.'); + ->with( + Logger::ERROR, + 'Rollout with ID "invalid_rollout_id" is not in the datafile.' + ); $this->assertEquals( - $this->decisionServiceMock->getVariationForFeatureRollout($feature_flag, 'user_1', []), - null + null, + $this->decisionServiceMock->getVariationForFeatureRollout($feature_flag, 'user_1', []) ); } // should return null - public function testGetVariationForFeatureRolloutWhenRolloutDoesNotHaveExperiment(){ + public function testGetVariationForFeatureRolloutWhenRolloutDoesNotHaveExperiment() + { // Mock Project Config $configMock = $this->getMockBuilder(ProjectConfig::class) ->setConstructorArgs(array(DATAFILE, $this->loggerMock, new NoOpErrorHandler())) ->setMethods(array('getRolloutFromId')) - ->getMock(); + ->getMock(); $this->decisionService = new DecisionService($this->loggerMock, $configMock); @@ -877,23 +909,27 @@ public function testGetVariationForFeatureRolloutWhenRolloutDoesNotHaveExperimen ->will($this->returnValue($experiment_less_rollout)); $this->assertEquals( - $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', []), - null + null, + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', []) ); } // ============== when the user qualifies for targeting rule (audience match) ====================== // should return the variation the user is bucketed into when the user is bucketed into the targeting rule - public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRule(){ + public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetingRule() + { $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); $rollout_id = $feature_flag->getRolloutId(); $rollout = $this->config->getRolloutFromId($rollout_id); $experiment = $rollout->getExperiments()[0]; $expected_variation = $experiment->getVariations()[0]; - $expected_decision = new FeatureDecision( - $experiment->getId(), $expected_variation->getId(), FeatureDecision::DECISION_SOURCE_ROLLOUT); + $experiment->getId(), + $expected_variation->getId(), + FeatureDecision::DECISION_SOURCE_ROLLOUT + ); + // Provide attributes such that user qualifies for audience $user_attributes = ["browser_type" => "chrome"]; @@ -907,28 +943,33 @@ public function testGetVariationForFeatureRolloutWhenUserIsBucketedInTheTargetin $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, - "Attempting to bucket user 'user_1' into rollout rule '{$experiment->getKey()}'."); + ->with( + Logger::DEBUG, + "Attempting to bucket user 'user_1' into rollout rule '{$experiment->getKey()}'." + ); $this->assertEquals( - $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - $expected_decision + $expected_decision, + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes) ); } // should return the variation the user is bucketed into when the user is bucketed into the "Everyone Else" rule' // and the user is not bucketed into the targeting rule - public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargetingRuleButBucketedToEveryoneElseRule(){ + public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTargetingRuleButBucketedToEveryoneElseRule() + { $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); $rollout_id = $feature_flag->getRolloutId(); $rollout = $this->config->getRolloutFromId($rollout_id); $experiment0 = $rollout->getExperiments()[0]; // Everyone Else Rule $experiment2 = $rollout->getExperiments()[2]; - $expected_variation = $experiment2->getVariations()[0]; - + $expected_variation = $experiment2->getVariations()[0]; $expected_decision = new FeatureDecision( - $experiment2->getId(), $expected_variation->getId(), FeatureDecision::DECISION_SOURCE_ROLLOUT); + $experiment2->getId(), + $expected_variation->getId(), + FeatureDecision::DECISION_SOURCE_ROLLOUT + ); // Provide attributes such that user qualifies for audience $user_attributes = ["browser_type" => "chrome"]; @@ -940,30 +981,35 @@ public function testGetVariationForFeatureRolloutWhenUserIsNotBucketedInTheTarge $this->bucketerMock->expects($this->at(0)) ->method('bucket') ->willReturn(null); - //Make bucket return expected variation when called second time for everyone else + // Make bucket return expected variation when called second time for everyone else $this->bucketerMock->expects($this->at(1)) ->method('bucket') ->willReturn($expected_variation); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, - "Attempting to bucket user 'user_1' into rollout rule '{$experiment0->getKey()}'."); + ->with( + Logger::DEBUG, + "Attempting to bucket user 'user_1' into rollout rule '{$experiment0->getKey()}'." + ); $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::DEBUG, - "User 'user_1' was excluded due to traffic allocation. Checking 'Everyone Else' rule now."); + ->with( + Logger::DEBUG, + "User 'user_1' was excluded due to traffic allocation. Checking 'Everyone Else' rule now." + ); $this->assertEquals( - $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - $expected_decision + $expected_decision, + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes) ); } - // should log and return nil when the user is not bucketed into the targeting rule and + // should log and return nil when the user is not bucketed into the targeting rule and // the user is not bucketed into the "Everyone Else" rule' - public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTargetingRuleNorToEveryoneElseRule(){ + public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheTargetingRuleNorToEveryoneElseRule() + { $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); $rollout_id = $feature_flag->getRolloutId(); $rollout = $this->config->getRolloutFromId($rollout_id); @@ -981,27 +1027,33 @@ public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheT $this->bucketerMock->expects($this->at(0)) ->method('bucket') ->willReturn(null); - //Make bucket return null when called second time for everyone else + // Make bucket return null when called second time for everyone else $this->bucketerMock->expects($this->at(1)) ->method('bucket') ->willReturn(null); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, - "Attempting to bucket user 'user_1' into rollout rule '{$experiment0->getKey()}'."); + ->with( + Logger::DEBUG, + "Attempting to bucket user 'user_1' into rollout rule '{$experiment0->getKey()}'." + ); $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::DEBUG, - "User 'user_1' was excluded due to traffic allocation. Checking 'Everyone Else' rule now."); + ->with( + Logger::DEBUG, + "User 'user_1' was excluded due to traffic allocation. Checking 'Everyone Else' rule now." + ); $this->loggerMock->expects($this->at(2)) ->method('log') - ->with(Logger::DEBUG, - "User 'user_1' was excluded from the 'Everyone Else' rule for feature flag"); + ->with( + Logger::DEBUG, + "User 'user_1' was excluded from the 'Everyone Else' rule for feature flag" + ); $this->assertEquals( - $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - null + null, + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes) ); } @@ -1011,18 +1063,21 @@ public function testGetVariationForFeatureRolloutWhenUserIsNeitherBucketedInTheT // should return expected variation when the user is attempted to be bucketed into all targeting rules // including Everyone Else rule - public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargetingRule(){ + public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTargetingRule() + { $feature_flag = $this->config->getFeatureFlagFromKey('boolean_single_variable_feature'); $rollout_id = $feature_flag->getRolloutId(); $rollout = $this->config->getRolloutFromId($rollout_id); $experiment0 = $rollout->getExperiments()[0]; $experiment1 = $rollout->getExperiments()[1]; // Everyone Else Rule - $experiment2 = $rollout->getExperiments()[2]; - $expected_variation = $experiment2->getVariations()[0]; - + $experiment2 = $rollout->getExperiments()[2]; + $expected_variation = $experiment2->getVariations()[0]; $expected_decision = new FeatureDecision( - $experiment2->getId(), $expected_variation->getId(), FeatureDecision::DECISION_SOURCE_ROLLOUT); + $experiment2->getId(), + $expected_variation->getId(), + FeatureDecision::DECISION_SOURCE_ROLLOUT + ); // Provide null attributes so that user does not qualify for audience $user_attributes = []; @@ -1031,7 +1086,7 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar $bucketer->setAccessible(true); $bucketer->setValue($this->decisionService, $this->bucketerMock); - // Expect bucket to be called exactly once for the everyone else/last rule. + // Expect bucket to be called exactly once for the everyone else/last rule. // As we ignore Audience check only for thelast rule $this->bucketerMock->expects($this->exactly(1)) ->method('bucket') @@ -1039,18 +1094,21 @@ public function testGetVariationForFeatureRolloutWhenUserDoesNotQualifyForAnyTar $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, - "User 'user_1' did not meet the audience conditions to be in rollout rule '{$experiment0->getKey()}'."); + ->with( + Logger::DEBUG, + "User 'user_1' did not meet the audience conditions to be in rollout rule '{$experiment0->getKey()}'." + ); $this->loggerMock->expects($this->at(1)) ->method('log') - ->with(Logger::DEBUG, - "User 'user_1' did not meet the audience conditions to be in rollout rule '{$experiment1->getKey()}'."); + ->with( + Logger::DEBUG, + "User 'user_1' did not meet the audience conditions to be in rollout rule '{$experiment1->getKey()}'." + ); $this->assertEquals( - $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes), - $expected_decision + $expected_decision, + $this->decisionService->getVariationForFeatureRollout($feature_flag, 'user_1', $user_attributes) ); } } - diff --git a/tests/NotificationTests/NotificationCenterTest.php b/tests/NotificationTests/NotificationCenterTest.php new file mode 100644 index 00000000..43127c90 --- /dev/null +++ b/tests/NotificationTests/NotificationCenterTest.php @@ -0,0 +1,685 @@ +loggerMock = $this->getMockBuilder(NoOpLogger::class) + ->setMethods(array('log')) + ->getMock(); + + $this->errorHandlerMock = $this->getMockBuilder(NoOpErrorHandler::class) + ->setMethods(array('handleError')) + ->getMock(); + + $this->notificationCenterObj = new NotificationCenter($this->loggerMock, $this->errorHandlerMock); + } + + public function testAddNotificationWithInvalidParams() + { + // should log, throw an exception and return null if invalid notification type given + $invalid_type = "HelloWorld"; + + $this->errorHandlerMock->expects($this->at(0)) + ->method('handleError') + ->with(new InvalidNotificationTypeException('Invalid notification type.')); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Invalid notification type."); + + $this->assertSame( + null, + $this->notificationCenterObj->addNotificationListener($invalid_type, function () { + }) + ); + + // should log and return null if invalid callable given + $invalid_callable = "HelloWorld"; + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Invalid notification callback."); + + $this->assertSame( + null, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, $invalid_callable) + ); + } + + public function testAddNotificationWithValidTypeAndCallback() + { + $notificationType = NotificationType::DECISION; + $this->notificationCenterObj->cleanAllNotifications(); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // === should add, log and return notification ID when a plain function is passed as an argument === // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + $simple_method = function () { + }; + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, "Callback added for notification type '{$notificationType}'."); + $this->assertSame( + 1, + $this->notificationCenterObj->addNotificationListener($notificationType, $simple_method) + ); + // verify that notifications length has incremented by 1 + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[$notificationType]) + ); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // === should add, log and return notification ID when an anonymous function is passed as an argument === // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, "Callback added for notification type '{$notificationType}'."); + $this->assertSame( + 2, + $this->notificationCenterObj->addNotificationListener($notificationType, function () { + }) + ); + // verify that notifications length has incremented by 1 + $this->assertSame( + 2, + sizeof($this->notificationCenterObj->getNotifications()[$notificationType]) + ); + + /////////////////////////////////////////////////////////////////////////////////////////////////////// + // === should add, log and return notification ID when an object method is passed as an argument === // + /////////////////////////////////////////////////////////////////////////////////////////////////////// + $eBuilder = new EventBuilder; + $callbackInput = array($eBuilder, 'createImpressionEvent'); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, "Callback added for notification type '{$notificationType}'."); + $this->assertSame( + 3, + $this->notificationCenterObj->addNotificationListener($notificationType, $callbackInput) + ); + // verify that notifications length has incremented by 1 + $this->assertSame( + 3, + sizeof($this->notificationCenterObj->getNotifications()[$notificationType]) + ); + } + + public function testAddNotificationForMultipleNotificationTypes() + { + $this->notificationCenterObj->cleanAllNotifications(); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // === should add, log and return notification ID when a valid callback is added for each notification type === // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::DECISION)); + $this->assertSame( + 1, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + }) + ); + + // verify that notifications length for NotificationType::DECISION has incremented by 1 + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + ); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::TRACK)); + $this->assertSame( + 2, + $this->notificationCenterObj->addNotificationListener(NotificationType::TRACK, function () { + }) + ); + + // verify that notifications length for NotificationType::TRACK has incremented by 1 + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::TRACK]) + ); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::FEATURE_ACCESSED)); + $this->assertSame( + 3, + $this->notificationCenterObj->addNotificationListener(NotificationType::FEATURE_ACCESSED, function () { + }) + ); + + // verify that notifications length for NotificationType::FEATURE_ACCESSED has incremented by 1 + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::FEATURE_ACCESSED]) + ); + } + + public function testAddNotificationForMultipleCallbacksForASingleNotificationType() + { + $this->notificationCenterObj->cleanAllNotifications(); + + /////////////////////////////////////////////////////////////////////////////////////// + // === should add, log and return notification ID when multiple valid callbacks + // are added for a single notification type === // + /////////////////////////////////////////////////////////////////////////////////////// + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::DECISION)); + $this->assertSame( + 1, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + }) + ); + + // verify that notifications length for NotificationType::DECISION has incremented by 1 + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + ); + + $this->assertSame( + 2, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + echo "HelloWorld"; + }) + ); + + // verify that notifications length for NotificationType::DECISION has incremented by 1 + $this->assertSame( + 2, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + ); + + $this->assertSame( + 3, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + $a = 1; + }) + ); + + // verify that notifications length for NotificationType::DECISION has incremented by 1 + $this->assertSame( + 3, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + ); + } + + public function testAddNotificationThatAlreadyAddedCallbackIsNotReAdded() + { + // Note: anonymous methods sent with the same body will be re-added. + // Only variable and object methods can be checked for duplication + + $functionToSend = function () { + }; + $this->notificationCenterObj->cleanAllNotifications(); + + /////////////////////////////////////////////////////////////////////////// + // ===== verify that a variable method with same body isn't re-added ===== // + /////////////////////////////////////////////////////////////////////////// + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::DECISION)); + + // verify that notification ID 1 is returned + $this->assertSame( + 1, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, $functionToSend) + ); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG, sprintf("Callback already added for notification type '%s'.", NotificationType::DECISION)); + + // verify that -1 is returned when adding the same callback + $this->assertSame( + -1, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, $functionToSend) + ); + + // verify that same method is added for a different notification type + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::TRACK)); + $this->assertSame( + 2, + $this->notificationCenterObj->addNotificationListener(NotificationType::TRACK, $functionToSend) + ); + + ///////////////////////////////////////////////////////////////////////// + // ===== verify that an object method with same body isn't re-added ===== // + ///////////////////////////////////////////////////////////////////////// + $eBuilder = new EventBuilder; + $callbackInput = array($eBuilder, 'createImpressionEvent'); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::DECISION)); + $this->assertSame( + 3, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, $callbackInput) + ); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG, sprintf("Callback already added for notification type '%s'.", NotificationType::DECISION)); + + // verify that -1 is returned when adding the same callback + $this->assertSame( + -1, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, $callbackInput) + ); + + // verify that same method is added for a different notification type + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::TRACK)); + $this->assertSame( + 4, + $this->notificationCenterObj->addNotificationListener(NotificationType::TRACK, $callbackInput) + ); + } + + public function testRemoveNotification() + { + $this->notificationCenterObj->cleanAllNotifications(); + + // add a callback for multiple notification types + $this->assertSame( + 1, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + }) + ); + $this->assertSame( + 2, + $this->notificationCenterObj->addNotificationListener(NotificationType::TRACK, function () { + }) + ); + // add another callback for NotificationType::DECISION + $this->assertSame( + 3, + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + //doSomething + }) + ); + + // Verify that notifications length for NotificationType::DECISION is 2 + $this->assertSame( + 2, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + ); + + // Verify that notifications length for NotificationType::TRACK is 1 + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::TRACK]) + ); + + + /////////////////////////////////////////////////////////////////////////////// + // === Verify that no callback is removed for an invalid notification ID === // + /////////////////////////////////////////////////////////////////////////////// + $invalid_id = 4; + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG, sprintf("No Callback found with notification ID '%s'.", $invalid_id)); + $this->assertSame( + false, + $this->notificationCenterObj->removeNotificationListener($invalid_id) + ); + + ///////////////////////////////////////////////////////////////////// + // === Verify that callback is removed for a valid notification ID // + ///////////////////////////////////////////////////////////////////// + + $valid_id = 3; + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, sprintf("Callback with notification ID '%s' has been removed.", $valid_id)); + $this->assertSame( + true, + $this->notificationCenterObj->removeNotificationListener($valid_id) + ); + + // verify that notifications length for NotificationType::DECISION is now 1 + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + ); + + //verify that notifications length for NotificationType::TRACK remains same + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::TRACK]) + ); + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // === Verify that no callback is removed once a callback has been already removed against a notification ID === // + ///////////////////////////////////////////////////////////////////////////////////////////////// + $valid_id = 3; + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::DEBUG, sprintf("No Callback found with notification ID '%s'.", $valid_id)); + $this->assertSame( + false, + $this->notificationCenterObj->removeNotificationListener($valid_id) + ); + + //verify that notifications lengths for NotificationType::DECISION and NotificationType::TRACK remain same + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + ); + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::TRACK]) + ); + } + + public function testClearNotifications() + { + // ensure that notifications length is zero for each notification type + $this->notificationCenterObj->cleanAllNotifications(); + + // add a callback for multiple notification types + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + }); + $this->notificationCenterObj->addNotificationListener(NotificationType::TRACK, function () { + }); + + // add another callback for NotificationType::DECISION + $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + }); + + // Verify that notifications length for NotificationType::DECISION is 2 + $this->assertSame( + 2, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + ); + + // Verify that notifications length for NotificationType::TRACK is 1 + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::TRACK]) + ); + + ///////////////////////////////////////////////////////////////////////////////////////// + // === Verify that no notifications are removed given an invalid notification type === // + ///////////////////////////////////////////////////////////////////////////////////////// + + $invalid_type = "HelloWorld"; + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Invalid notification type."); + + $this->errorHandlerMock->expects($this->at(0)) + ->method('handleError') + ->with(new InvalidNotificationTypeException('Invalid notification type.')); + + $this->assertSame( + null, + $this->notificationCenterObj->clearNotifications($invalid_type) + ); + + // Verify that notifications length for NotificationType::DECISION is still 2 + $this->assertSame( + 2, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + ); + + // Verify that notifications length for NotificationType::TRACK is still 1 + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::TRACK]) + ); + + /////////////////////////////////////////////////////////////////////////////////////// + // === Verify that all notifications are removed given a valid notification type === // + /////////////////////////////////////////////////////////////////////////////////////// + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with( + Logger::INFO, + sprintf("All callbacks for notification type '%s' have been removed.", NotificationType::DECISION) + ); + + $this->notificationCenterObj->clearNotifications(NotificationType::DECISION); + + // Verify that notifications length for NotificationType::DECISION is now 0 + $this->assertSame( + 0, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + ); + + // Verify that notifications length for NotificationType::TRACK is still 1 + $this->assertSame( + 1, + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::TRACK]) + ); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // == Verify that no error is thrown when clearNotification is called again for the same notification type === // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + $this->notificationCenterObj->clearNotifications(NotificationType::DECISION); + } + + + public function testCleanAllNotifications() + { + // using a new notification center object to avoid using the method being tested, + // to reset notifications list + $notificationCenterA = new NotificationCenter($this->loggerMock, $this->errorHandlerMock); + + // verify that for each of the notification types, the notifications length is zero + $this->assertSame( + 0, + sizeof($notificationCenterA->getNotifications()[NotificationType::DECISION]) + ); + $this->assertSame( + 0, + sizeof($notificationCenterA->getNotifications()[NotificationType::TRACK]) + ); + $this->assertSame( + 0, + sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_ACCESSED]) + ); + + // add a callback for multiple notification types + $notificationCenterA->addNotificationListener(NotificationType::DECISION, function () { + }); + $notificationCenterA->addNotificationListener(NotificationType::DECISION, function () { + }); + $notificationCenterA->addNotificationListener(NotificationType::DECISION, function () { + }); + + $notificationCenterA->addNotificationListener(NotificationType::TRACK, function () { + }); + $notificationCenterA->addNotificationListener(NotificationType::TRACK, function () { + }); + + $notificationCenterA->addNotificationListener(NotificationType::FEATURE_ACCESSED, function () { + }); + + + // verify that notifications length for each type reflects the just added callbacks + $this->assertSame( + 3, + sizeof($notificationCenterA->getNotifications()[NotificationType::DECISION]) + ); + $this->assertSame( + 2, + sizeof($notificationCenterA->getNotifications()[NotificationType::TRACK]) + ); + $this->assertSame( + 1, + sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_ACCESSED]) + ); + + //////////////////////////////////////////////////////////////////////////////////////////////////// + // === verify that cleanAllNotifications removes all notifications for each notification type === // + //////////////////////////////////////////////////////////////////////////////////////////////////// + + $notificationCenterA->cleanAllNotifications(); + + // verify that notifications length for each type is now set to 0 + $this->assertSame( + 0, + sizeof($notificationCenterA->getNotifications()[NotificationType::DECISION]) + ); + $this->assertSame( + 0, + sizeof($notificationCenterA->getNotifications()[NotificationType::TRACK]) + ); + $this->assertSame( + 0, + sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_ACCESSED]) + ); + + /////////////////////////////////////////////////////////////////////////////////////// + //=== verify that cleanAllNotifications doesn't throw an error when called again === // + /////////////////////////////////////////////////////////////////////////////////////// + $notificationCenterA->cleanAllNotifications(); + } + + public function testFireNotificationsGivenLessThanExpectedNumberOfArguments() + { + $clientObj = new FireNotificationTester; + $this->notificationCenterObj->cleanAllNotifications(); + + // add a notification callback with arguments + $this->notificationCenterObj->addNotificationListener( + NotificationType::DECISION, + array($clientObj, 'decision_callback_with_args') + ); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // === Verify that an exception is thrown and message logged when less number of args passed than expected === // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// + $this->errorHandlerMock->expects($this->at(0)) + ->method('handleError') + ->with(new InvalidCallbackArgumentCountException('Registered callback expects more number of arguments than the actual number')); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Problem calling notify callback."); + + $this->notificationCenterObj->fireNotifications(NotificationType::DECISION, array("HelloWorld")); + } + + public function testFireNotificationsAndVerifyThatAllCallbacksWithoutArgsAreCalled() + { + $clientMock = $this->getMockBuilder(FireNotificationTester::class) + ->setMethods(array('decision_callback_no_args', 'decision_callback_no_args_2', 'track_callback_no_args')) + ->getMock(); + + $this->notificationCenterObj->cleanAllNotifications(); + + //add notification callbacks + $this->notificationCenterObj->addNotificationListener( + NotificationType::DECISION, + array($clientMock, 'decision_callback_no_args') + ); + $this->notificationCenterObj->addNotificationListener( + NotificationType::DECISION, + array($clientMock, 'decision_callback_no_args_2') + ); + $this->notificationCenterObj->addNotificationListener( + NotificationType::TRACK, + array($clientMock, 'track_callback_no_args') + ); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // === Verify that all callbacks for NotificationType::DECISION are called and no other callbacks are called === // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + $clientMock->expects($this->exactly(1)) + ->method('decision_callback_no_args'); + $clientMock->expects($this->exactly(1)) + ->method('decision_callback_no_args_2'); + + $clientMock->expects($this->never()) + ->method('track_callback_no_args'); + + $this->notificationCenterObj->fireNotifications(NotificationType::DECISION); + } + + public function testFireNotificationsAndVerifyThatAllCallbacksWithArgsAreCalled() + { + $clientMock = $this->getMockBuilder(FireNotificationTester::class) + ->setMethods(array('decision_callback_with_args', 'decision_callback_with_args_2', 'track_callback_no_args')) + ->getMock(); + + $this->notificationCenterObj->cleanAllNotifications(); + + //add notification callbacks with args + $this->notificationCenterObj->addNotificationListener( + NotificationType::DECISION, + array($clientMock, 'decision_callback_with_args') + ); + $this->notificationCenterObj->addNotificationListener( + NotificationType::DECISION, + array($clientMock, 'decision_callback_with_args_2') + ); + $this->notificationCenterObj->addNotificationListener( + NotificationType::TRACK, + array($clientMock, 'track_callback_no_args') + ); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // === Verify that all callbacks for NotificationType::DECISION are called and no other callbacks are called === // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + $clientMock->expects($this->exactly(1)) + ->method('decision_callback_with_args') + ->with(5, 5.5, 'string', array(5,6), function () { + }); + $clientMock->expects($this->exactly(1)) + ->method('decision_callback_with_args_2') + ->with(5, 5.5, 'string', array(5,6), function () { + }); + $clientMock->expects($this->never()) + ->method('track_callback_no_args'); + + $this->notificationCenterObj->fireNotifications( + NotificationType::DECISION, + array(5, 5.5, 'string', array(5,6), function () { + }) + ); + } +} diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 1c9cc8cc..a1167a7a 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -18,16 +18,20 @@ use Exception; use Monolog\Logger; -use Optimizely\ErrorHandler\DefaultErrorHandler; +use Optimizely\DecisionService\DecisionService; +use Optimizely\DecisionService\FeatureDecision; use Optimizely\ErrorHandler\NoOpErrorHandler; -use Optimizely\Event\Builder\EventBuilder; use Optimizely\Event\LogEvent; use Optimizely\Exceptions\InvalidAttributeException; -use Optimizely\Logger\DefaultLogger; use Optimizely\Logger\NoOpLogger; -use Optimizely\Optimizely; +use Optimizely\Notification\NotificationCenter; +use Optimizely\Notification\NotificationType; use Optimizely\ProjectConfig; use TypeError; +use Optimizely\ErrorHandler\DefaultErrorHandler; +use Optimizely\Event\Builder\EventBuilder; +use Optimizely\Logger\DefaultLogger; +use Optimizely\Optimizely; class OptimizelyTest extends \PHPUnit_Framework_TestCase @@ -60,6 +64,11 @@ public function setUp() $this->eventBuilderMock = $this->getMockBuilder(EventBuilder::class) ->setMethods(array('createImpressionEvent', 'createConversionEvent')) ->getMock(); + + $this->notificationCenterMock = $this->getMockBuilder(NotificationCenter::class) + ->setConstructorArgs(array($this->loggerMock, new NoOpErrorHandler)) + ->setMethods(array('fireNotifications')) + ->getMock(); } public function testInitValidEventDispatcher() @@ -149,8 +158,16 @@ public function testValidateDatafileInvalidFileJsonValidationSkipped() public function testActivateInvalidOptimizelyObject() { - $optlyObject = new Optimizely('Random datafile'); - $optlyObject->activate('some_experiment', 'some_user'); + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array('Random datafile', null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + // verify that sendImpression isn't called + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); + + $optimizelyMock->activate('some_experiment', 'some_user'); $this->expectOutputRegex('/Datafile has invalid format. Failing "activate"./'); } @@ -172,12 +189,17 @@ public function testActivateInvalidAttributes() ->method('handleError') ->with(new InvalidAttributeException('Provided attributes are in an invalid format.')); - $optlyObject = new Optimizely( - $this->datafile, new ValidEventDispatcher(), $this->loggerMock, $errorHandlerMock - ); + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, new ValidEventDispatcher(), $this->loggerMock, $errorHandlerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + // verify that sendImpression isn't called + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); // Call activate - $this->assertNull($optlyObject->activate('test_experiment', 'test_user', 42)); + $this->assertNull($optimizelyMock->activate('test_experiment', 'test_user', 42)); } public function testActivateUserInNoVariation() @@ -208,32 +230,32 @@ public function testActivateUserInNoVariation() ->method('log') ->with(Logger::INFO, 'Not activating user "not_in_variation_user".'); - $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, new ValidEventDispatcher(), $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); $eventBuilder->setAccessible(true); - $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); + $eventBuilder->setValue($optimizelyMock, $this->eventBuilderMock); + + // verify that sendImpression isn't called + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); // Call activate - $this->assertNull($optlyObject->activate('test_experiment', 'not_in_variation_user', $userAttributes)); + $this->assertNull($optimizelyMock->activate('test_experiment', 'not_in_variation_user', $userAttributes)); } public function testActivateNoAudienceNoAttributes() { - $this->eventBuilderMock->expects($this->once()) - ->method('createImpressionEvent') - ->with( - $this->projectConfig, - 'group_experiment_1', - 'group_exp_1_var_2', 'user_1', null - ) - ->willReturn(new LogEvent( - 'logx.optimizely.com/decision', - ['param1' => 'val1', 'param2' => 'val2'], 'POST', []) - ); + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, new ValidEventDispatcher(), $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); $callIndex = 0; - $this->loggerMock->expects($this->exactly(7)) + $this->loggerMock->expects($this->exactly(5)) ->method('log'); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') @@ -253,22 +275,14 @@ public function testActivateNoAudienceNoAttributes() ->method('log') ->with(Logger::INFO, 'User "user_1" is in variation group_exp_1_var_2 of experiment group_experiment_1.'); - $this->loggerMock->expects($this->at($callIndex++)) - ->method('log') - ->with(Logger::INFO, - 'Activating user "user_1" in experiment "group_experiment_1".'); - $this->loggerMock->expects($this->at($callIndex++)) - ->method('log') - ->with(Logger::DEBUG, - 'Dispatching impression event to URL logx.optimizely.com/decision with params param1=val1¶m2=val2.'); - - $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); - $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); - $eventBuilder->setAccessible(true); - $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); + + // Verify that sendImpression is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with('group_experiment_1', 'group_exp_1_var_2', 'user_1', null); // Call activate - $this->assertEquals('group_exp_1_var_2', $optlyObject->activate('group_experiment_1', 'user_1')); + $this->assertSame('group_exp_1_var_2', $optimizelyMock->activate('group_experiment_1', 'user_1')); } public function testActivateNoAudienceNoAttributesAfterSetForcedVariation() @@ -279,20 +293,13 @@ public function testActivateNoAudienceNoAttributesAfterSetForcedVariation() $variationKey = 'control'; $variationId = '7722370027'; - $this->eventBuilderMock->expects($this->once()) - ->method('createImpressionEvent') - ->with( - $this->projectConfig, - 'group_experiment_1', - 'group_exp_1_var_2', 'user_1', null - ) - ->willReturn(new LogEvent( - 'logx.optimizely.com/decision', - ['param1' => 'val1', 'param2' => 'val2'], 'POST', []) - ); + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, new ValidEventDispatcher(), $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); $callIndex = 0; - $this->loggerMock->expects($this->exactly(8)) + $this->loggerMock->expects($this->exactly(6)) ->method('log'); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') @@ -315,31 +322,29 @@ public function testActivateNoAudienceNoAttributesAfterSetForcedVariation() ->method('log') ->with(Logger::INFO, 'User "user_1" is in variation group_exp_1_var_2 of experiment group_experiment_1.'); - $this->loggerMock->expects($this->at($callIndex++)) - ->method('log') - ->with(Logger::INFO, - 'Activating user "user_1" in experiment "group_experiment_1".'); - $this->loggerMock->expects($this->at($callIndex++)) - ->method('log') - ->with(Logger::DEBUG, - 'Dispatching impression event to URL logx.optimizely.com/decision with params param1=val1¶m2=val2.'); - - $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); - $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); - $eventBuilder->setAccessible(true); - $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); + + // Verify that sendImpression is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with('group_experiment_1', 'group_exp_1_var_2', 'user_1', null); // set forced variation - $this->assertTrue($this->optimizelyObject->setForcedVariation($experimentKey, $userId, $variationKey), 'Set variation for paused experiment should have failed.'); + $this->assertTrue($optimizelyMock->setForcedVariation($experimentKey, $userId, $variationKey), 'Set variation for paused experiment should have failed.'); // Call activate - $this->assertEquals('group_exp_1_var_2', $optlyObject->activate('group_experiment_1', 'user_1')); + $this->assertEquals('group_exp_1_var_2', $optimizelyMock->activate('group_experiment_1', 'user_1')); } public function testActivateAudienceNoAttributes() { - $this->eventBuilderMock->expects($this->never()) - ->method('createImpressionEvent'); + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + // Verify that sendImpressionEvent is not called + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); $callIndex = 0; $this->loggerMock->expects($this->exactly(3)) @@ -355,14 +360,8 @@ public function testActivateAudienceNoAttributes() ->with(Logger::INFO, 'Not activating user "test_user".'); - $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); - - $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); - $eventBuilder->setAccessible(true); - $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); - // Call activate - $this->assertNull($optlyObject->activate('test_experiment', 'test_user')); + $this->assertNull($optimizelyMock->activate('test_experiment', 'test_user')); } public function testActivateWithAttributes() @@ -373,16 +372,13 @@ public function testActivateWithAttributes() 'location' => 'San Francisco' ]; - $this->eventBuilderMock->expects($this->once()) - ->method('createImpressionEvent') - ->with( - $this->projectConfig, - 'test_experiment', - 'control', 'test_user', $userAttributes - ) - ->willReturn(new LogEvent('logx.optimizely.com/decision', ['param1' => 'val1'], 'POST', [])); + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + $callIndex = 0; - $this->loggerMock->expects($this->exactly(5)) + $this->loggerMock->expects($this->exactly(3)) ->method('log'); $this->loggerMock->expects($this->at($callIndex++)) ->method('log') @@ -394,28 +390,26 @@ public function testActivateWithAttributes() ->method('log') ->with(Logger::INFO, 'User "test_user" is in variation control of experiment test_experiment.'); - $this->loggerMock->expects($this->at($callIndex++)) - ->method('log') - ->with(Logger::INFO, 'Activating user "test_user" in experiment "test_experiment".'); - $this->loggerMock->expects($this->at($callIndex++)) - ->method('log') - ->with(Logger::DEBUG, - 'Dispatching impression event to URL logx.optimizely.com/decision with params param1=val1.'); - - $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); - - $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); - $eventBuilder->setAccessible(true); - $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); + + // Verify that sendImpressionEvent is called with expected attributes + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with('test_experiment', 'control', 'test_user', $userAttributes); // Call activate - $this->assertEquals('control', $optlyObject->activate('test_experiment', 'test_user', $userAttributes)); + $this->assertEquals('control', $optimizelyMock->activate('test_experiment', 'test_user', $userAttributes)); } public function testActivateExperimentNotRunning() { - $this->eventBuilderMock->expects($this->never()) - ->method('createImpressionEvent'); + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + // Verify that sendImpressionEvent is not called + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); $this->loggerMock->expects($this->exactly(2)) ->method('log'); @@ -427,14 +421,8 @@ public function testActivateExperimentNotRunning() ->with(Logger::INFO, 'Not activating user "test_user".'); - $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); - - $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); - $eventBuilder->setAccessible(true); - $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); - - // Call activate - $this->assertNull($optlyObject->activate('paused_experiment', 'test_user', null)); + // Call activate + $this->assertNull($optimizelyMock->activate('paused_experiment', 'test_user', null)); } public function testGetVariationInvalidOptimizelyObject() @@ -648,6 +636,15 @@ public function testValidatePreconditionsUserNotInForcedVariationInExperiment() public function testTrackInvalidOptimizelyObject() { $optlyObject = new Optimizely('Random datafile'); + + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + // Verify that fireNotifications isn't called + $this->notificationCenterMock->expects($this->never()) + ->method('fireNotifications'); + $optlyObject->track('some_event', 'some_user'); $this->expectOutputRegex('/Datafile has invalid format. Failing "track"./'); } @@ -669,6 +666,14 @@ public function testTrackInvalidAttributes() $this->datafile, new ValidEventDispatcher(), $this->loggerMock, $errorHandlerMock ); + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + // Verify that fireNotifications isn't called + $this->notificationCenterMock->expects($this->never()) + ->method('fireNotifications'); + // Call activate $this->assertNull($optlyObject->track('purchase', 'test_user', 42)); } @@ -753,6 +758,26 @@ public function testTrackNoAttributesNoEventValue() $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + // Verify that fireNotifications is called with expected params + $arrayParam = array( + 'purchase', + 'test_user', + null, + null, + new LogEvent('logx.optimizely.com/track', ['param1' => 'val1'], 'POST', []) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( + NotificationType::TRACK, + $arrayParam + ); + $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); $eventBuilder->setAccessible(true); $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); @@ -850,6 +875,26 @@ public function testTrackNoAttributesNoEventValueAfterSetForcedVariation() $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + // Verify that fireNotifications is called with expected params + $arrayParam = array( + 'purchase', + 'test_user', + null, + null, + new LogEvent('logx.optimizely.com/track', ['param1' => 'val1'], 'POST', []) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( + NotificationType::TRACK, + $arrayParam + ); + $this->assertTrue($this->optimizelyObject->setForcedVariation($experimentKey, $userId, $variationKey), 'Set variation for paused experiment should have failed.'); $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); @@ -949,6 +994,26 @@ public function testTrackWithAttributesNoEventValue() $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + // Verify that fireNotifications is called with expected params + $arrayParam = array( + 'purchase', + 'test_user', + $userAttributes, + null, + new LogEvent('logx.optimizely.com/track', ['param1' => 'val1'], 'POST', []) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( + NotificationType::TRACK, + $arrayParam + ); + $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); $eventBuilder->setAccessible(true); $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); @@ -1041,6 +1106,26 @@ public function testTrackNoAttributesWithDeprecatedEventValue() $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + // Verify that fireNotifications is called with expected params + $arrayParam = array( + 'purchase', + 'test_user', + null, + array('revenue' => 42), + new LogEvent('logx.optimizely.com/track', ['param1' => 'val1'], 'POST', []) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( + NotificationType::TRACK, + $arrayParam + ); + $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); $eventBuilder->setAccessible(true); $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); @@ -1128,6 +1213,26 @@ public function testTrackNoAttributesWithEventValue() $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + // Verify that fireNotifications is called with expected params + $arrayParam = array( + 'purchase', + 'test_user', + null, + array('revenue' => 42), + new LogEvent('logx.optimizely.com/track', ['param1' => 'val1'], 'POST', []) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( + NotificationType::TRACK, + $arrayParam + ); + $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); $eventBuilder->setAccessible(true); $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); @@ -1204,6 +1309,26 @@ public function testTrackNoAttributesWithInvalidEventValue() $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + // Verify that fireNotifications is called with expected params + $arrayParam = array( + 'purchase', + 'test_user', + null, + array('revenue' => '4200'), + new LogEvent('logx.optimizely.com/track', ['param1' => 'val1'], 'POST', []) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( + NotificationType::TRACK, + $arrayParam + ); + $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); $eventBuilder->setAccessible(true); $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); @@ -1306,6 +1431,26 @@ public function testTrackWithAttributesWithDeprecatedEventValue() $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + // Verify that fireNotifications is called with expected params + $arrayParam = array( + 'purchase', + 'test_user', + $userAttributes, + array('revenue' => 42), + new LogEvent('logx.optimizely.com/track', ['param1' => 'val1'], 'POST', []) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( + NotificationType::TRACK, + $arrayParam + ); + $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); $eventBuilder->setAccessible(true); $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); @@ -1403,6 +1548,26 @@ public function testTrackWithAttributesWithEventValue() $optlyObject = new Optimizely($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + // Verify that fireNotifications is called with expected params + $arrayParam = array( + 'purchase', + 'test_user', + $userAttributes, + array('revenue' => 42), + new LogEvent('logx.optimizely.com/track', ['param1' => 'val1'], 'POST', []) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( + NotificationType::TRACK, + $arrayParam + ); + $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); $eventBuilder->setAccessible(true); $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); @@ -1730,4 +1895,729 @@ public function testGetVariationBucketingIdAttribute() $this->assertEquals(null, $variationKey, sprintf('Invalid variation key "%s" for getVariation with bucketing ID "%s".', $variationKey, $this->testBucketingIdControl)); } + public function testIsFeatureEnabledGivenInvalidDataFile() + { + $optlyObject = new Optimizely('Random datafile', null, $this->loggerMock); + + $this->expectOutputRegex("/Datafile has invalid format. Failing 'isFeatureEnabled'./"); + $optlyObject->isFeatureEnabled("boolean_feature", "user_id"); + } + + public function testIsFeatureEnabledGivenInvalidArguments() + { + // should return null and log a message when feature flag key is empty + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + + $this->assertSame($this->optimizelyObject->isFeatureEnabled("", "user_id"), null); + + // should return null and log a message when feature flag key is null + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + + $this->assertSame($this->optimizelyObject->isFeatureEnabled(null, "user_id"), null); + + // should return null and log a message when user id is empty + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "User ID cannot be empty."); + + $this->assertSame($this->optimizelyObject->isFeatureEnabled("boolean_feature", ""), null); + + // should return null and log a message when user id is null + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "User ID cannot be empty."); + + $this->assertSame($this->optimizelyObject->isFeatureEnabled("boolean_feature", null), null); + } + + public function testIsFeatureEnabledGivenFeatureFlagNotFound() + { + $feature_key = "abcd"; // Any string that is not a feature flag key in the data file + + //should return null and log a message when no feature flag found against a valid feature key + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "FeatureFlag Key \"{$feature_key}\" is not in datafile."); + $this->assertSame($this->optimizelyObject->isFeatureEnabled($feature_key, "user_id"), null); + } + + public function testIsFeatureEnabledGivenInvalidFeatureFlag() + { + // Create local config copy for this method to add error + $projectConfig = new ProjectConfig($this->datafile, $this->loggerMock, new NoOpErrorHandler()); + $optimizelyObj = new Optimizely($this->datafile); + + $config = new \ReflectionProperty(Optimizely::class, '_config'); + $config->setAccessible(true); + $config->setValue($optimizelyObj, $projectConfig); + + $feature_flag = $projectConfig->getFeatureFlagFromKey('mutex_group_feature'); + // Add such an experiment to the list of experiment ids, that does not belong to the same mutex group + $experimentIds = $feature_flag->getExperimentIds(); + $experimentIds [] = '122241'; + $feature_flag->setExperimentIds($experimentIds); + + //should return null when feature flag is invalid + $this->assertSame($optimizelyObj->isFeatureEnabled('mutex_group_feature', "user_id"), null); + } + + public function testIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser() + { + // should return false when no variation is returned for user + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // mock getVariationForFeature to return null + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue(null)); + + // assert that impression event is not sent + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); + + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optimizelyMock, $this->notificationCenterMock); + + // verify that fireNotifications isn't called + $this->notificationCenterMock->expects($this->never()) + ->method('fireNotifications'); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, "Feature Flag 'double_single_variable_feature' is not enabled for user 'user_id'."); + + $this->assertSame( + $optimizelyMock->isFeatureEnabled('double_single_variable_feature', 'user_id'), + false + ); + } + + public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExperimented() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + + $expected_decision = new FeatureDecision( + $experiment->getId(), + $variation->getId(), + FeatureDecision::DECISION_SOURCE_EXPERIMENT + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // assert that sendImpressionEvent is called with expected params + $optimizelyMock->expects($this->exactly(1)) + ->method('sendImpressionEvent') + ->with('test_experiment_double_feature', 'control', 'user_id', []); + + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optimizelyMock, $this->notificationCenterMock); + + // verify that fireNotifications is called with expected params + $arrayParam = array( + 'double_single_variable_feature', + 'user_id', + [], + $variation + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( NotificationType::FEATURE_ACCESSED, $arrayParam); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, "Feature Flag 'double_single_variable_feature' is enabled for user 'user_id'."); + + $this->assertSame( + $optimizelyMock->isFeatureEnabled('double_single_variable_feature', 'user_id', []), + true + ); + } + + public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingExperimented() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('sendImpressionEvent')) + ->getMock(); + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($optimizelyMock, $decisionServiceMock); + + // Mock getVariationForFeature to return a valid decision with experiment and variation keys + $rollout = $this->projectConfig->getRolloutFromId('166660'); + $experiment = $rollout->getExperiments()[0]; + $variation = $experiment->getVariations()[0]; + $expected_decision = new FeatureDecision( + $experiment->getId(), + $variation->getId(), + FeatureDecision::DECISION_SOURCE_ROLLOUT + ); + + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optimizelyMock, $this->notificationCenterMock); + + // verify that fireNotifications is called with expected params + $arrayParam = array( + 'boolean_single_variable_feature', + 'user_id', + [], + $this->projectConfig->getVariationFromRolloutExperiment($experiment->getId(), $variation->getId()) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( NotificationType::FEATURE_ACCESSED, $arrayParam); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + // assert that sendImpressionEvent is not called + $optimizelyMock->expects($this->never()) + ->method('sendImpressionEvent'); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with( + Logger::INFO, + "The user 'user_id' is not being experimented on Feature Flag 'boolean_single_variable_feature'." + ); + + $this->loggerMock->expects($this->at(1)) + ->method('log') + ->with(Logger::INFO, "Feature Flag 'boolean_single_variable_feature' is enabled for user 'user_id'."); + + $this->assertSame( + $optimizelyMock->isFeatureEnabled('boolean_single_variable_feature', 'user_id', []), + true + ); + } + + public function testGetFeatureVariableValueForTypeGivenInvalidArguments() + { + // should return null and log a message when feature flag key is empty + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "", + "double_variable", + "user_id" + ), null); + + // should return null and log a message when feature flag key is null + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Feature Flag key cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + null, + "double_variable", + "user_id" + ), null); + + // should return null and log a message when variable key is empty + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Variable key cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "boolean_feature", + "", + "user_id" + ), null); + + // should return null and log a message when variable key is null + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Variable key cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "boolean_feature", + null, + "user_id" + ), null); + + // should return null and log a message when user id is empty + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "User ID cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "boolean_feature", + "double_variable", + "" + ), null); + + // should return null and log a message when user id is null + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "User ID cannot be empty."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "boolean_feature", + "double_variable", + null + ), null); + } + + public function testGetFeatureVariableValueForTypeGivenFeatureFlagNotFound() + { + $feature_key = "abcd"; // Any string that is not a feature flag key in the data file + + //should return null and log a message when no feature flag found against a valid feature key + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "FeatureFlag Key \"{$feature_key}\" is not in datafile."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + $feature_key, + "double_variable", + 'user_id' + ), null); + } + + public function testGetFeatureVariableValueForTypeGivenFeatureVariableNotFound() + { + $feature_key = "boolean_feature"; // Any exisiting feature key in the data file + $variable_key = "abcd"; // Any string that is not a variable key in the data file + + //should return null and log a message when no feature flag found against a valid feature key + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "No variable key \"{$variable_key}\" defined in datafile ". + "for feature flag \"{$feature_key}\"."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + $feature_key, + $variable_key, + 'user_id' + ), null); + } + + public function testGetFeatureVariableValueForTypeGivenInvalidFeatureVariableType() + { + // should return null and log a message when a feature variable does exist but is + // called for another type + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, "Variable is of type 'double', but you requested it as type 'string'."); + + $this->assertSame($this->optimizelyObject->getFeatureVariableValueForType( + "double_single_variable_feature", + "double_variable", + "user_id", + null, + "string" + ), null); + } + + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsNotEnabledForUser() + { + // should return default value + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($this->optimizelyObject, $decisionServiceMock); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue(null)); + + $this->loggerMock->expects($this->exactly(1)) + ->method('log') + ->with( + Logger::INFO, + "User 'user_id'is not in any variation, returning default value '14.99'." + ); + + $this->assertSame( + $this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', [], 'double'), + '14.99' + ); + } + + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVariableIsInVariation() + { + // should return specific value + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($this->optimizelyObject, $decisionServiceMock); + + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_double_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_double_feature', 'control'); + $expected_decision = new FeatureDecision( + $experiment->getId(), + $variation->getId(), + FeatureDecision::DECISION_SOURCE_EXPERIMENT + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + $this->loggerMock->expects($this->exactly(1)) + ->method('log') + ->with( + Logger::INFO, + "Returning variable value '42.42' for variation 'control' ". + "of feature flag 'double_single_variable_feature'" + ); + + $this->assertSame( + $this->optimizelyObject->getFeatureVariableValueForType('double_single_variable_feature', 'double_variable', 'user_id', [], 'double'), + '42.42' + ); + } + + public function testGetFeatureVariableValueForTypeGivenFeatureFlagIsEnabledForUserAndVariableNotInVariation() + { + // should return default value + + $decisionServiceMock = $this->getMockBuilder(DecisionService::class) + ->setConstructorArgs(array($this->loggerMock, $this->projectConfig)) + ->setMethods(array('getVariationForFeature')) + ->getMock(); + + $decisionService = new \ReflectionProperty(Optimizely::class, '_decisionService'); + $decisionService->setAccessible(true); + $decisionService->setValue($this->optimizelyObject, $decisionServiceMock); + + // Mock getVariationForFeature to return experiment/variation from a different feature + $experiment = $this->projectConfig->getExperimentFromKey('test_experiment_integer_feature'); + $variation = $this->projectConfig->getVariationFromKey('test_experiment_integer_feature', 'control'); + $expected_decision = new FeatureDecision( + $experiment->getId(), + $variation->getId(), + FeatureDecision::DECISION_SOURCE_EXPERIMENT + ); + + $decisionServiceMock->expects($this->exactly(1)) + ->method('getVariationForFeature') + ->will($this->returnValue($expected_decision)); + + $this->loggerMock->expects($this->exactly(1)) + ->method('log') + ->with( + Logger::INFO, + "Variable 'double_variable' is not used in variation 'control', returning default value '14.99'." + ); + + $this->assertSame( + $this->optimizelyObject->getFeatureVariableValueForType( + 'double_single_variable_feature', + 'double_variable', + 'user_id', + [], + 'double' + ), + '14.99' + ); + } + + + public function testGetFeatureVariableBooleanCaseTrue() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return 'true' + $map = [['boolean_single_variable_feature', 'boolean_variable', 'user_id', [], 'boolean', 'true']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('boolean_single_variable_feature', 'boolean_variable', 'user_id', [], 'boolean') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableBoolean('boolean_single_variable_feature', 'boolean_variable', 'user_id', []), + true + ); + } + + public function testGetFeatureVariableBooleanCaseFalse() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return any string but 'true' + $map = [['boolean_single_variable_feature', 'boolean_variable', 'user_id', [], 'boolean', '14.33']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('boolean_single_variable_feature', 'boolean_variable', 'user_id', [], 'boolean') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableBoolean('boolean_single_variable_feature', 'boolean_variable', 'user_id', []), + false + ); + } + + public function testGetFeatureVariableIntegerWhenCasted() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return a numeric string + $map = [['integer_single_variable_feature', 'integer_variable', 'user_id', [], 'integer', '90']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('integer_single_variable_feature', 'integer_variable', 'user_id', [], 'integer') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableInteger('integer_single_variable_feature', 'integer_variable', 'user_id', []), + 90 + ); + } + + public function testGetFeatureVariableIntegerWhenNotCasted() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return a non-numeric string + $map = [['integer_single_variable_feature', 'integer_variable', 'user_id', [], 'integer', 'abc90']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('integer_single_variable_feature', 'integer_variable', 'user_id', [], 'integer') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableInteger('integer_single_variable_feature', 'integer_variable', 'user_id', []), + null + ); + } + + public function testGetFeatureVariableDoubleWhenCasted() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return a numeric string + $map = [['double_single_variable_feature', 'double_variable', 'user_id', [], 'double', '5.789']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('double_single_variable_feature', 'double_variable', 'user_id', [], 'double') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableDouble('double_single_variable_feature', 'double_variable', 'user_id', []), + 5.789 + ); + } + + public function testGetFeatureVariableDoubleWhenNotCasted() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + // assert that getFeatureVariableValueForType is called with expected arguments and mock to return a non-numeric string + $map = [['double_single_variable_feature', 'double_variable', 'user_id', [], 'double', 'abc5.789']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('double_single_variable_feature', 'double_variable', 'user_id', [], 'double') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableDouble('double_single_variable_feature', 'double_variable', 'user_id', []), + null + ); + } + + public function testGetFeatureVariableString() + { + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + + $map = [['string_single_variable_feature', 'string_variable', 'user_id', [], 'string', '59abc0p']]; + $optimizelyMock->expects($this->exactly(1)) + ->method('getFeatureVariableValueForType') + ->with('string_single_variable_feature', 'string_variable', 'user_id', [], 'string') + ->will($this->returnValueMap($map)); + + $this->assertSame( + $optimizelyMock->getFeatureVariableString('string_single_variable_feature', 'string_variable', 'user_id', []), + '59abc0p' + ); + } + + public function testSendImpressionEventWithNoAttributes(){ + $optlyObject = new OptimizelyTester($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + + // verify that createImpressionEvent is called + $this->eventBuilderMock->expects($this->once()) + ->method('createImpressionEvent') + ->with( + $this->projectConfig, + 'group_experiment_1', + 'group_exp_1_var_2', 'user_1', null + ) + ->willReturn(new LogEvent( + 'logx.optimizely.com/decision', + ['param1' => 'val1', 'param2' => 'val2'], 'POST', []) + ); + + // verify that fireNotifications is called with expected params + $arrayParam = array( + $this->projectConfig->getExperimentFromKey('group_experiment_1'), + 'user_1', + null, + $this->projectConfig->getVariationFromKey('group_experiment_1', 'group_exp_1_var_2'), + new LogEvent( + 'logx.optimizely.com/decision', + ['param1' => 'val1', 'param2' => 'val2'], 'POST', []) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); + $eventBuilder->setAccessible(true); + $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); + + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, + 'Activating user "user_1" in experiment "group_experiment_1".'); + $this->loggerMock->expects($this->at(1)) + ->method('log') + ->with(Logger::DEBUG, + 'Dispatching impression event to URL logx.optimizely.com/decision with params param1=val1¶m2=val2.'); + + $optlyObject->sendImpressionEvent('group_experiment_1', 'group_exp_1_var_2', 'user_1', null); + } + + + public function testSendImpressionEventWithAttributes(){ + $optlyObject = new OptimizelyTester($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + + $userAttributes = [ + 'device_type' => 'iPhone', + 'company' => 'Optimizely', + 'location' => 'San Francisco' + ]; + + // verify that createImpressionEvent is called + $this->eventBuilderMock->expects($this->once()) + ->method('createImpressionEvent') + ->with( + $this->projectConfig, + 'test_experiment', + 'control', 'test_user', $userAttributes + ) + ->willReturn(new LogEvent('logx.optimizely.com/decision', ['param1' => 'val1'], 'POST', [])); + + // verify that fireNotifications is called with expected params + $arrayParam = array( + $this->projectConfig->getExperimentFromKey('test_experiment'), + 'test_user', + $userAttributes, + $this->projectConfig->getVariationFromKey('test_experiment', 'control'), + new LogEvent( + 'logx.optimizely.com/decision', + ['param1' => 'val1'], 'POST', []) + ); + + $this->notificationCenterMock->expects($this->once()) + ->method('fireNotifications') + ->with( + NotificationType::DECISION, + $arrayParam + ); + + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::INFO, 'Activating user "test_user" in experiment "test_experiment".'); + $this->loggerMock->expects($this->at(1)) + ->method('log') + ->with(Logger::DEBUG, + 'Dispatching impression event to URL logx.optimizely.com/decision with params param1=val1.'); + + $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); + $eventBuilder->setAccessible(true); + $eventBuilder->setValue($optlyObject, $this->eventBuilderMock); + + $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); + $notificationCenter->setAccessible(true); + $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); + + $optlyObject->sendImpressionEvent('test_experiment', 'control', 'test_user', $userAttributes); + } + } diff --git a/tests/TestData.php b/tests/TestData.php index 0559b072..5d5a2f87 100644 --- a/tests/TestData.php +++ b/tests/TestData.php @@ -20,6 +20,7 @@ use Optimizely\Bucketer; use Optimizely\Event\Dispatcher\EventDispatcherInterface; use Optimizely\Event\LogEvent; +use Optimizely\Optimizely; define('DATAFILE','{ "experiments": [ @@ -754,6 +755,29 @@ public function generateBucketValue($bucketingId) } } +/** + * Class OptimizelyTester + * Extending Optimizely for the sake of tests. + */ +class OptimizelyTester extends Optimizely +{ + public function sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes){ + parent::sendImpressionEvent($experimentKey, $variationKey, $userId, $attributes); + } +} + +class FireNotificationTester{ + public function decision_callback_no_args(){} + + public function decision_callback_no_args_2(){} + + public function decision_callback_with_args($anInt, $aDouble, $aString, $anArray, $aFunction){} + + public function decision_callback_with_args_2($anInt, $aDouble, $aString, $anArray, $aFunction){} + + public function track_callback_no_args(){} +} + class ValidEventDispatcher implements EventDispatcherInterface { diff --git a/tests/UtilsTests/ValidatorTest.php b/tests/UtilsTests/ValidatorTest.php index 7406deee..6d89fee8 100644 --- a/tests/UtilsTests/ValidatorTest.php +++ b/tests/UtilsTests/ValidatorTest.php @@ -170,4 +170,32 @@ public function testIsUserInExperimentAudienceNoMatch() ['device_type' => 'Android', 'location' => 'San Francisco'] )); } + + public function testIsFeatureFlagValid() + { + $config = new ProjectConfig(DATAFILE, new NoOpLogger(), new NoOpErrorHandler()); + $feature_flag_source = $config->getFeatureFlagFromKey('mutex_group_feature'); + + // should return true when no experiment ids exist + $feature_flag = clone $feature_flag_source; + $feature_flag->setExperimentIds([]); + $this->assertTrue(Validator::isFeatureFlagValid($config, $feature_flag)); + + // should return true when only one experiment id exists + $feature_flag = clone $feature_flag_source; + $feature_flag->setExperimentIds(['122241']); + $this->assertTrue(Validator::isFeatureFlagValid($config, $feature_flag)); + + // should return true when more than one experiment ids exist that belong to the same group + $feature_flag = clone $feature_flag_source; + $this->assertTrue(Validator::isFeatureFlagValid($config, $feature_flag)); + + //should return false when more than one experiment ids exist that belong to different group + $feature_flag = clone $feature_flag_source; + $experimentIds = $feature_flag->getExperimentIds(); + $experimentIds [] = '122241'; + $feature_flag->setExperimentIds($experimentIds); + + $this->assertFalse(Validator::isFeatureFlagValid($config, $feature_flag)); + } } diff --git a/tests/UtilsTests/VariableTypeUtilsTest.php b/tests/UtilsTests/VariableTypeUtilsTest.php new file mode 100644 index 00000000..1d52fd62 --- /dev/null +++ b/tests/UtilsTests/VariableTypeUtilsTest.php @@ -0,0 +1,92 @@ +loggerMock = $this->getMockBuilder(NoOpLogger::class) + ->setMethods(array('log')) + ->getMock(); + + $this->variableUtilObj = new VariableTypeUtils(); + } + + public function testValueCastingToBoolean() + { + $this->assertTrue($this->variableUtilObj->castStringToType('true', 'boolean')); + $this->assertTrue($this->variableUtilObj->castStringToType('True', 'boolean')); + $this->assertFalse($this->variableUtilObj->castStringToType('false', 'boolean')); + $this->assertFalse($this->variableUtilObj->castStringToType('somestring', 'boolean')); + } + + public function testValueCastingToInteger() + { + $this->assertSame($this->variableUtilObj->castStringToType('1000', 'integer'), 1000); + $this->assertSame($this->variableUtilObj->castStringToType('123', 'integer'), 123); + + // should return nulll and log a message if value can not be casted to an integer + $value = '123.5'; // any string with non-decimal digits + $type = 'integer'; + $this->loggerMock->expects($this->exactly(1)) + ->method('log') + ->with( + Logger::ERROR, + "Unable to cast variable value '{$value}' to type '{$type}'." + ); + + $this->assertNull($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock)); + } + + public function testValueCastingToDouble() + { + $this->assertSame($this->variableUtilObj->castStringToType('1000', 'double'), 1000.0); + $this->assertSame($this->variableUtilObj->castStringToType('3.0', 'double'), 3.0); + $this->assertSame($this->variableUtilObj->castStringToType('13.37', 'double'), 13.37); + + // should return nil and log a message if value can not be casted to a double + $value = 'any-non-numeric-string'; + $type = 'double'; + $this->loggerMock->expects($this->exactly(1)) + ->method('log') + ->with( + Logger::ERROR, + "Unable to cast variable value '{$value}' to type '{$type}'." + ); + + $this->assertNull($this->variableUtilObj->castStringToType($value, $type, $this->loggerMock)); + } + + public function testValueCastingToString() + { + $this->assertSame($this->variableUtilObj->castStringToType('13.37', 'string'), '13.37'); + $this->assertSame($this->variableUtilObj->castStringToType('a string', 'string'), 'a string'); + $this->assertSame($this->variableUtilObj->castStringToType('3', 'string'), '3'); + $this->assertSame($this->variableUtilObj->castStringToType('false', 'string'), 'false'); + } +} From bd218fc2bd27ce55e52819edb243093bde873a70 Mon Sep 17 00:00:00 2001 From: Owais Date: Thu, 16 Nov 2017 18:55:17 +0500 Subject: [PATCH 2/7] :pen: Renamed notification type constants --- .../Notification/NotificationType.php | 5 +- src/Optimizely/Optimizely.php | 43 ++++-- src/Optimizely/ProjectConfig.php | 22 +-- .../NotificationCenterTest.php | 138 +++++++++--------- tests/OptimizelyTest.php | 11 +- 5 files changed, 116 insertions(+), 103 deletions(-) diff --git a/src/Optimizely/Notification/NotificationType.php b/src/Optimizely/Notification/NotificationType.php index 28c8ca50..b12fa677 100644 --- a/src/Optimizely/Notification/NotificationType.php +++ b/src/Optimizely/Notification/NotificationType.php @@ -19,9 +19,10 @@ class NotificationType { // format is EVENT: list of parameters to callback. - const DECISION = "DECISION:experiment, user_id,attributes, variation, event"; + const ACTIVATE = "ACTIVATE:experiment, user_id,attributes, variation, event"; const TRACK = "TRACK:event_key, user_id, attributes, event_tags, event"; - const FEATURE_ACCESSED = "FEATURE:feature_key, user_id, attributes, variation"; + const FEATURE_EXPERIMENT = "FEATURE_EXPERIMENT:feature_key, user_id, attributes, experiment, variation"; + const FEATURE_ROLLOUT = "FEATURE_ROLLOUT:feature_key, user_id, attributes, audience"; public static function isNotificationTypeValid($notification_type) { diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 75c315d7..f9c3fc73 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -248,7 +248,7 @@ protected function sendImpressionEvent($experimentKey, $variationKey, $userId, $ } $this->_notificationCenter->fireNotifications( - NotificationType::DECISION, + NotificationType::ACTIVATE, array( $this->_config->getExperimentFromKey($experimentKey), $userId, @@ -490,23 +490,42 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null) $variation = $this->_config->getVariationFromId($experiment->getKey(), $variation_id); $this->sendImpressionEvent($experiment->getKey(), $variation->getKey(), $userId, $attributes); + + $this->_notificationCenter->fireNotifications( + NotificationType::FEATURE_EXPERIMENT, + array( + $featureFlagKey, + $userId, + $attributes, + $experiment, + $variation + ) + ); + } else { - $variation = $this->_config->getVariationFromRolloutExperiment($experiment_id, $variation_id); $this->_logger->log(Logger::INFO, "The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'."); + + $experiment = $this->_config->getRolloutExperimentFromId($experiment_id); + $audience = null; + if($experiment->getAudienceIds() && !empty($experiment->getAudienceIds())){ + $audienceId = $experiment->getAudienceIds()[0]; + $audience = $this->_config->getAudience($audienceId); + } + + + $this->_notificationCenter->fireNotifications( + NotificationType::FEATURE_ROLLOUT, + array( + $featureFlagKey, + $userId, + $attributes, + $audience + ) + ); } $this->_logger->log(Logger::INFO, "Feature Flag '{$featureFlagKey}' is enabled for user '{$userId}'."); - $this->_notificationCenter->fireNotifications( - NotificationType::FEATURE_ACCESSED, - array( - $featureFlagKey, - $userId, - $attributes, - $variation - ) - ); - return true; } diff --git a/src/Optimizely/ProjectConfig.php b/src/Optimizely/ProjectConfig.php index 7388e106..01c761fb 100644 --- a/src/Optimizely/ProjectConfig.php +++ b/src/Optimizely/ProjectConfig.php @@ -473,34 +473,26 @@ public function getFeatureVariableFromKey($featureFlagKey, $variableKey) } /** - * Gets the variation object from a rollout rule/experiment + * Gets the experiment object from a rollout * - * Note: This method is needed in the case where we want to fetch a variation inside - * a rollout rule/experiment. Other methods getVariationById/Key can't be used because we do + * Note: This method is needed in the case where we want to fetch an experiment/rule inside + * a rollout. Other methods getExperimentById/Key can't be used because we do * not store rollout rules/experiments in our experiment/variation ID maps. * No need to throw an exception, since this method will not be directly called as a result of - * a public API method param and will receive pre-verified experimentId and variationId + * a public API method param and will receive pre-verified experimentId * * @param string $experimentId Experiment ID - * @param string $variationId VariationID * - * @return Variation Variation object corresponding to given experiment and variation IDs + * @return Experiment Experiment object corresponding to given experiment/rollout rule ID */ - public function getVariationFromRolloutExperiment($experimentId, $variationId){ + public function getRolloutExperimentFromId($experimentId){ foreach($this->_rollouts as $rollout){ $experiments = $rollout->getExperiments(); foreach($experiments as $experiment){ if($experiment->getId() == $experimentId) - break 2; + return $experiment; } } - - $variations = $experiment->getVariations(); - foreach($variations as $variation){ - if($variation->getId() == $variationId) - return $variation; - } - return null; } diff --git a/tests/NotificationTests/NotificationCenterTest.php b/tests/NotificationTests/NotificationCenterTest.php index 43127c90..39d25660 100644 --- a/tests/NotificationTests/NotificationCenterTest.php +++ b/tests/NotificationTests/NotificationCenterTest.php @@ -72,13 +72,13 @@ public function testAddNotificationWithInvalidParams() $this->assertSame( null, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, $invalid_callable) + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, $invalid_callable) ); } public function testAddNotificationWithValidTypeAndCallback() { - $notificationType = NotificationType::DECISION; + $notificationType = NotificationType::ACTIVATE; $this->notificationCenterObj->cleanAllNotifications(); //////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -145,17 +145,17 @@ public function testAddNotificationForMultipleNotificationTypes() ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::DECISION)); + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::ACTIVATE)); $this->assertSame( 1, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, function () { }) ); - // verify that notifications length for NotificationType::DECISION has incremented by 1 + // verify that notifications length for NotificationType::ACTIVATE has incremented by 1 $this->assertSame( 1, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::ACTIVATE]) ); $this->loggerMock->expects($this->at(0)) @@ -175,17 +175,17 @@ public function testAddNotificationForMultipleNotificationTypes() $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::FEATURE_ACCESSED)); + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::FEATURE_EXPERIMENT)); $this->assertSame( 3, - $this->notificationCenterObj->addNotificationListener(NotificationType::FEATURE_ACCESSED, function () { + $this->notificationCenterObj->addNotificationListener(NotificationType::FEATURE_EXPERIMENT, function () { }) ); - // verify that notifications length for NotificationType::FEATURE_ACCESSED has incremented by 1 + // verify that notifications length for NotificationType::FEATURE_EXPERIMENT has incremented by 1 $this->assertSame( 1, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::FEATURE_ACCESSED]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::FEATURE_EXPERIMENT]) ); } @@ -199,43 +199,43 @@ public function testAddNotificationForMultipleCallbacksForASingleNotificationTyp /////////////////////////////////////////////////////////////////////////////////////// $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::DECISION)); + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::ACTIVATE)); $this->assertSame( 1, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, function () { }) ); - // verify that notifications length for NotificationType::DECISION has incremented by 1 + // verify that notifications length for NotificationType::ACTIVATE has incremented by 1 $this->assertSame( 1, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::ACTIVATE]) ); $this->assertSame( 2, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, function () { echo "HelloWorld"; }) ); - // verify that notifications length for NotificationType::DECISION has incremented by 1 + // verify that notifications length for NotificationType::ACTIVATE has incremented by 1 $this->assertSame( 2, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::ACTIVATE]) ); $this->assertSame( 3, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, function () { $a = 1; }) ); - // verify that notifications length for NotificationType::DECISION has incremented by 1 + // verify that notifications length for NotificationType::ACTIVATE has incremented by 1 $this->assertSame( 3, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::ACTIVATE]) ); } @@ -253,22 +253,22 @@ public function testAddNotificationThatAlreadyAddedCallbackIsNotReAdded() /////////////////////////////////////////////////////////////////////////// $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::DECISION)); + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::ACTIVATE)); // verify that notification ID 1 is returned $this->assertSame( 1, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, $functionToSend) + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, $functionToSend) ); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, sprintf("Callback already added for notification type '%s'.", NotificationType::DECISION)); + ->with(Logger::DEBUG, sprintf("Callback already added for notification type '%s'.", NotificationType::ACTIVATE)); // verify that -1 is returned when adding the same callback $this->assertSame( -1, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, $functionToSend) + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, $functionToSend) ); // verify that same method is added for a different notification type @@ -288,20 +288,20 @@ public function testAddNotificationThatAlreadyAddedCallbackIsNotReAdded() $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::DECISION)); + ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::ACTIVATE)); $this->assertSame( 3, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, $callbackInput) + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, $callbackInput) ); $this->loggerMock->expects($this->at(0)) ->method('log') - ->with(Logger::DEBUG, sprintf("Callback already added for notification type '%s'.", NotificationType::DECISION)); + ->with(Logger::DEBUG, sprintf("Callback already added for notification type '%s'.", NotificationType::ACTIVATE)); // verify that -1 is returned when adding the same callback $this->assertSame( -1, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, $callbackInput) + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, $callbackInput) ); // verify that same method is added for a different notification type @@ -321,7 +321,7 @@ public function testRemoveNotification() // add a callback for multiple notification types $this->assertSame( 1, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, function () { }) ); $this->assertSame( @@ -329,18 +329,18 @@ public function testRemoveNotification() $this->notificationCenterObj->addNotificationListener(NotificationType::TRACK, function () { }) ); - // add another callback for NotificationType::DECISION + // add another callback for NotificationType::ACTIVATE $this->assertSame( 3, - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, function () { //doSomething }) ); - // Verify that notifications length for NotificationType::DECISION is 2 + // Verify that notifications length for NotificationType::ACTIVATE is 2 $this->assertSame( 2, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::ACTIVATE]) ); // Verify that notifications length for NotificationType::TRACK is 1 @@ -375,10 +375,10 @@ public function testRemoveNotification() $this->notificationCenterObj->removeNotificationListener($valid_id) ); - // verify that notifications length for NotificationType::DECISION is now 1 + // verify that notifications length for NotificationType::ACTIVATE is now 1 $this->assertSame( 1, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::ACTIVATE]) ); //verify that notifications length for NotificationType::TRACK remains same @@ -399,10 +399,10 @@ public function testRemoveNotification() $this->notificationCenterObj->removeNotificationListener($valid_id) ); - //verify that notifications lengths for NotificationType::DECISION and NotificationType::TRACK remain same + //verify that notifications lengths for NotificationType::ACTIVATE and NotificationType::TRACK remain same $this->assertSame( 1, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::ACTIVATE]) ); $this->assertSame( 1, @@ -416,19 +416,19 @@ public function testClearNotifications() $this->notificationCenterObj->cleanAllNotifications(); // add a callback for multiple notification types - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, function () { }); $this->notificationCenterObj->addNotificationListener(NotificationType::TRACK, function () { }); - // add another callback for NotificationType::DECISION - $this->notificationCenterObj->addNotificationListener(NotificationType::DECISION, function () { + // add another callback for NotificationType::ACTIVATE + $this->notificationCenterObj->addNotificationListener(NotificationType::ACTIVATE, function () { }); - // Verify that notifications length for NotificationType::DECISION is 2 + // Verify that notifications length for NotificationType::ACTIVATE is 2 $this->assertSame( 2, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::ACTIVATE]) ); // Verify that notifications length for NotificationType::TRACK is 1 @@ -456,10 +456,10 @@ public function testClearNotifications() $this->notificationCenterObj->clearNotifications($invalid_type) ); - // Verify that notifications length for NotificationType::DECISION is still 2 + // Verify that notifications length for NotificationType::ACTIVATE is still 2 $this->assertSame( 2, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::ACTIVATE]) ); // Verify that notifications length for NotificationType::TRACK is still 1 @@ -476,15 +476,15 @@ public function testClearNotifications() ->method('log') ->with( Logger::INFO, - sprintf("All callbacks for notification type '%s' have been removed.", NotificationType::DECISION) + sprintf("All callbacks for notification type '%s' have been removed.", NotificationType::ACTIVATE) ); - $this->notificationCenterObj->clearNotifications(NotificationType::DECISION); + $this->notificationCenterObj->clearNotifications(NotificationType::ACTIVATE); - // Verify that notifications length for NotificationType::DECISION is now 0 + // Verify that notifications length for NotificationType::ACTIVATE is now 0 $this->assertSame( 0, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::DECISION]) + sizeof($this->notificationCenterObj->getNotifications()[NotificationType::ACTIVATE]) ); // Verify that notifications length for NotificationType::TRACK is still 1 @@ -496,7 +496,7 @@ public function testClearNotifications() /////////////////////////////////////////////////////////////////////////////////////////////////////////// // == Verify that no error is thrown when clearNotification is called again for the same notification type === // /////////////////////////////////////////////////////////////////////////////////////////////////////////// - $this->notificationCenterObj->clearNotifications(NotificationType::DECISION); + $this->notificationCenterObj->clearNotifications(NotificationType::ACTIVATE); } @@ -509,7 +509,7 @@ public function testCleanAllNotifications() // verify that for each of the notification types, the notifications length is zero $this->assertSame( 0, - sizeof($notificationCenterA->getNotifications()[NotificationType::DECISION]) + sizeof($notificationCenterA->getNotifications()[NotificationType::ACTIVATE]) ); $this->assertSame( 0, @@ -517,15 +517,15 @@ public function testCleanAllNotifications() ); $this->assertSame( 0, - sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_ACCESSED]) + sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_EXPERIMENT]) ); // add a callback for multiple notification types - $notificationCenterA->addNotificationListener(NotificationType::DECISION, function () { + $notificationCenterA->addNotificationListener(NotificationType::ACTIVATE, function () { }); - $notificationCenterA->addNotificationListener(NotificationType::DECISION, function () { + $notificationCenterA->addNotificationListener(NotificationType::ACTIVATE, function () { }); - $notificationCenterA->addNotificationListener(NotificationType::DECISION, function () { + $notificationCenterA->addNotificationListener(NotificationType::ACTIVATE, function () { }); $notificationCenterA->addNotificationListener(NotificationType::TRACK, function () { @@ -533,14 +533,14 @@ public function testCleanAllNotifications() $notificationCenterA->addNotificationListener(NotificationType::TRACK, function () { }); - $notificationCenterA->addNotificationListener(NotificationType::FEATURE_ACCESSED, function () { + $notificationCenterA->addNotificationListener(NotificationType::FEATURE_EXPERIMENT, function () { }); // verify that notifications length for each type reflects the just added callbacks $this->assertSame( 3, - sizeof($notificationCenterA->getNotifications()[NotificationType::DECISION]) + sizeof($notificationCenterA->getNotifications()[NotificationType::ACTIVATE]) ); $this->assertSame( 2, @@ -548,7 +548,7 @@ public function testCleanAllNotifications() ); $this->assertSame( 1, - sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_ACCESSED]) + sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_EXPERIMENT]) ); //////////////////////////////////////////////////////////////////////////////////////////////////// @@ -560,7 +560,7 @@ public function testCleanAllNotifications() // verify that notifications length for each type is now set to 0 $this->assertSame( 0, - sizeof($notificationCenterA->getNotifications()[NotificationType::DECISION]) + sizeof($notificationCenterA->getNotifications()[NotificationType::ACTIVATE]) ); $this->assertSame( 0, @@ -568,7 +568,7 @@ public function testCleanAllNotifications() ); $this->assertSame( 0, - sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_ACCESSED]) + sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_EXPERIMENT]) ); /////////////////////////////////////////////////////////////////////////////////////// @@ -584,7 +584,7 @@ public function testFireNotificationsGivenLessThanExpectedNumberOfArguments() // add a notification callback with arguments $this->notificationCenterObj->addNotificationListener( - NotificationType::DECISION, + NotificationType::ACTIVATE, array($clientObj, 'decision_callback_with_args') ); @@ -599,7 +599,7 @@ public function testFireNotificationsGivenLessThanExpectedNumberOfArguments() ->method('log') ->with(Logger::ERROR, "Problem calling notify callback."); - $this->notificationCenterObj->fireNotifications(NotificationType::DECISION, array("HelloWorld")); + $this->notificationCenterObj->fireNotifications(NotificationType::ACTIVATE, array("HelloWorld")); } public function testFireNotificationsAndVerifyThatAllCallbacksWithoutArgsAreCalled() @@ -612,11 +612,11 @@ public function testFireNotificationsAndVerifyThatAllCallbacksWithoutArgsAreCall //add notification callbacks $this->notificationCenterObj->addNotificationListener( - NotificationType::DECISION, + NotificationType::ACTIVATE, array($clientMock, 'decision_callback_no_args') ); $this->notificationCenterObj->addNotificationListener( - NotificationType::DECISION, + NotificationType::ACTIVATE, array($clientMock, 'decision_callback_no_args_2') ); $this->notificationCenterObj->addNotificationListener( @@ -625,7 +625,7 @@ public function testFireNotificationsAndVerifyThatAllCallbacksWithoutArgsAreCall ); ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // === Verify that all callbacks for NotificationType::DECISION are called and no other callbacks are called === // + // === Verify that all callbacks for NotificationType::ACTIVATE are called and no other callbacks are called === // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// $clientMock->expects($this->exactly(1)) @@ -636,7 +636,7 @@ public function testFireNotificationsAndVerifyThatAllCallbacksWithoutArgsAreCall $clientMock->expects($this->never()) ->method('track_callback_no_args'); - $this->notificationCenterObj->fireNotifications(NotificationType::DECISION); + $this->notificationCenterObj->fireNotifications(NotificationType::ACTIVATE); } public function testFireNotificationsAndVerifyThatAllCallbacksWithArgsAreCalled() @@ -649,11 +649,11 @@ public function testFireNotificationsAndVerifyThatAllCallbacksWithArgsAreCalled( //add notification callbacks with args $this->notificationCenterObj->addNotificationListener( - NotificationType::DECISION, + NotificationType::ACTIVATE, array($clientMock, 'decision_callback_with_args') ); $this->notificationCenterObj->addNotificationListener( - NotificationType::DECISION, + NotificationType::ACTIVATE, array($clientMock, 'decision_callback_with_args_2') ); $this->notificationCenterObj->addNotificationListener( @@ -662,7 +662,7 @@ public function testFireNotificationsAndVerifyThatAllCallbacksWithArgsAreCalled( ); ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // === Verify that all callbacks for NotificationType::DECISION are called and no other callbacks are called === // + // === Verify that all callbacks for NotificationType::ACTIVATE are called and no other callbacks are called === // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////// $clientMock->expects($this->exactly(1)) @@ -677,7 +677,7 @@ public function testFireNotificationsAndVerifyThatAllCallbacksWithArgsAreCalled( ->method('track_callback_no_args'); $this->notificationCenterObj->fireNotifications( - NotificationType::DECISION, + NotificationType::ACTIVATE, array(5, 5.5, 'string', array(5,6), function () { }) ); diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index a1167a7a..cbde4d00 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -2053,12 +2053,13 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExper 'double_single_variable_feature', 'user_id', [], + $experiment, $variation ); $this->notificationCenterMock->expects($this->once()) ->method('fireNotifications') - ->with( NotificationType::FEATURE_ACCESSED, $arrayParam); + ->with( NotificationType::FEATURE_EXPERIMENT, $arrayParam); $this->loggerMock->expects($this->at(0)) ->method('log') @@ -2105,12 +2106,12 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingEx 'boolean_single_variable_feature', 'user_id', [], - $this->projectConfig->getVariationFromRolloutExperiment($experiment->getId(), $variation->getId()) + $this->projectConfig->getAudience($experiment->getAudienceIds()[0]) ); $this->notificationCenterMock->expects($this->once()) ->method('fireNotifications') - ->with( NotificationType::FEATURE_ACCESSED, $arrayParam); + ->with( NotificationType::FEATURE_ROLLOUT, $arrayParam); $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') @@ -2539,7 +2540,7 @@ public function testSendImpressionEventWithNoAttributes(){ $this->notificationCenterMock->expects($this->once()) ->method('fireNotifications') ->with( - NotificationType::DECISION, + NotificationType::ACTIVATE, $arrayParam ); @@ -2597,7 +2598,7 @@ public function testSendImpressionEventWithAttributes(){ $this->notificationCenterMock->expects($this->once()) ->method('fireNotifications') ->with( - NotificationType::DECISION, + NotificationType::ACTIVATE, $arrayParam ); From 898242f62157fe196c8854b2afd84e6122b85fdb Mon Sep 17 00:00:00 2001 From: Owais Date: Fri, 17 Nov 2017 16:13:48 +0500 Subject: [PATCH 3/7] :pen: Renamed fireNotifications to sendNot.. --- .../Notification/NotificationCenter.php | 2 +- src/Optimizely/Optimizely.php | 10 +-- .../NotificationCenterTest.php | 12 ++-- tests/OptimizelyTest.php | 64 +++++++++---------- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/Optimizely/Notification/NotificationCenter.php b/src/Optimizely/Notification/NotificationCenter.php index 52fa1105..eeb15d4f 100644 --- a/src/Optimizely/Notification/NotificationCenter.php +++ b/src/Optimizely/Notification/NotificationCenter.php @@ -155,7 +155,7 @@ public function cleanAllNotifications() * @param array $args Array of items to pass as arguments to the callback * */ - public function fireNotifications($notification_type, array $args = []) + public function sendNotifications($notification_type, array $args = []) { if (!isset($this->_notifications[$notification_type])) { // No exception thrown and error logged since this method will be called from diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index f9c3fc73..516976f5 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -247,7 +247,7 @@ protected function sendImpressionEvent($experimentKey, $variationKey, $userId, $ )); } - $this->_notificationCenter->fireNotifications( + $this->_notificationCenter->sendNotifications( NotificationType::ACTIVATE, array( $this->_config->getExperimentFromKey($experimentKey), @@ -353,7 +353,7 @@ public function track($eventKey, $userId, $attributes = null, $eventTags = null) 'Unable to dispatch conversion event. Error %s', $exception->getMessage())); } - $this->_notificationCenter->fireNotifications( + $this->_notificationCenter->sendNotifications( NotificationType::TRACK, array( $eventKey, @@ -491,7 +491,7 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null) $this->sendImpressionEvent($experiment->getKey(), $variation->getKey(), $userId, $attributes); - $this->_notificationCenter->fireNotifications( + $this->_notificationCenter->sendNotifications( NotificationType::FEATURE_EXPERIMENT, array( $featureFlagKey, @@ -513,13 +513,13 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null) } - $this->_notificationCenter->fireNotifications( + $this->_notificationCenter->sendNotifications( NotificationType::FEATURE_ROLLOUT, array( $featureFlagKey, $userId, $attributes, - $audience + [$audience] ) ); } diff --git a/tests/NotificationTests/NotificationCenterTest.php b/tests/NotificationTests/NotificationCenterTest.php index 39d25660..1c0fd67d 100644 --- a/tests/NotificationTests/NotificationCenterTest.php +++ b/tests/NotificationTests/NotificationCenterTest.php @@ -577,7 +577,7 @@ public function testCleanAllNotifications() $notificationCenterA->cleanAllNotifications(); } - public function testFireNotificationsGivenLessThanExpectedNumberOfArguments() + public function testsendNotificationsGivenLessThanExpectedNumberOfArguments() { $clientObj = new FireNotificationTester; $this->notificationCenterObj->cleanAllNotifications(); @@ -599,10 +599,10 @@ public function testFireNotificationsGivenLessThanExpectedNumberOfArguments() ->method('log') ->with(Logger::ERROR, "Problem calling notify callback."); - $this->notificationCenterObj->fireNotifications(NotificationType::ACTIVATE, array("HelloWorld")); + $this->notificationCenterObj->sendNotifications(NotificationType::ACTIVATE, array("HelloWorld")); } - public function testFireNotificationsAndVerifyThatAllCallbacksWithoutArgsAreCalled() + public function testsendNotificationsAndVerifyThatAllCallbacksWithoutArgsAreCalled() { $clientMock = $this->getMockBuilder(FireNotificationTester::class) ->setMethods(array('decision_callback_no_args', 'decision_callback_no_args_2', 'track_callback_no_args')) @@ -636,10 +636,10 @@ public function testFireNotificationsAndVerifyThatAllCallbacksWithoutArgsAreCall $clientMock->expects($this->never()) ->method('track_callback_no_args'); - $this->notificationCenterObj->fireNotifications(NotificationType::ACTIVATE); + $this->notificationCenterObj->sendNotifications(NotificationType::ACTIVATE); } - public function testFireNotificationsAndVerifyThatAllCallbacksWithArgsAreCalled() + public function testsendNotificationsAndVerifyThatAllCallbacksWithArgsAreCalled() { $clientMock = $this->getMockBuilder(FireNotificationTester::class) ->setMethods(array('decision_callback_with_args', 'decision_callback_with_args_2', 'track_callback_no_args')) @@ -676,7 +676,7 @@ public function testFireNotificationsAndVerifyThatAllCallbacksWithArgsAreCalled( $clientMock->expects($this->never()) ->method('track_callback_no_args'); - $this->notificationCenterObj->fireNotifications( + $this->notificationCenterObj->sendNotifications( NotificationType::ACTIVATE, array(5, 5.5, 'string', array(5,6), function () { }) diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index cbde4d00..bd43f0c9 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -67,7 +67,7 @@ public function setUp() $this->notificationCenterMock = $this->getMockBuilder(NotificationCenter::class) ->setConstructorArgs(array($this->loggerMock, new NoOpErrorHandler)) - ->setMethods(array('fireNotifications')) + ->setMethods(array('sendNotifications')) ->getMock(); } @@ -641,9 +641,9 @@ public function testTrackInvalidOptimizelyObject() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); - // Verify that fireNotifications isn't called + // Verify that sendNotifications isn't called $this->notificationCenterMock->expects($this->never()) - ->method('fireNotifications'); + ->method('sendNotifications'); $optlyObject->track('some_event', 'some_user'); $this->expectOutputRegex('/Datafile has invalid format. Failing "track"./'); @@ -670,9 +670,9 @@ public function testTrackInvalidAttributes() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); - // Verify that fireNotifications isn't called + // Verify that sendNotifications isn't called $this->notificationCenterMock->expects($this->never()) - ->method('fireNotifications'); + ->method('sendNotifications'); // Call activate $this->assertNull($optlyObject->track('purchase', 'test_user', 42)); @@ -762,7 +762,7 @@ public function testTrackNoAttributesNoEventValue() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); - // Verify that fireNotifications is called with expected params + // Verify that sendNotifications is called with expected params $arrayParam = array( 'purchase', 'test_user', @@ -772,7 +772,7 @@ public function testTrackNoAttributesNoEventValue() ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::TRACK, $arrayParam @@ -879,7 +879,7 @@ public function testTrackNoAttributesNoEventValueAfterSetForcedVariation() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); - // Verify that fireNotifications is called with expected params + // Verify that sendNotifications is called with expected params $arrayParam = array( 'purchase', 'test_user', @@ -889,7 +889,7 @@ public function testTrackNoAttributesNoEventValueAfterSetForcedVariation() ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::TRACK, $arrayParam @@ -998,7 +998,7 @@ public function testTrackWithAttributesNoEventValue() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); - // Verify that fireNotifications is called with expected params + // Verify that sendNotifications is called with expected params $arrayParam = array( 'purchase', 'test_user', @@ -1008,7 +1008,7 @@ public function testTrackWithAttributesNoEventValue() ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::TRACK, $arrayParam @@ -1110,7 +1110,7 @@ public function testTrackNoAttributesWithDeprecatedEventValue() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); - // Verify that fireNotifications is called with expected params + // Verify that sendNotifications is called with expected params $arrayParam = array( 'purchase', 'test_user', @@ -1120,7 +1120,7 @@ public function testTrackNoAttributesWithDeprecatedEventValue() ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::TRACK, $arrayParam @@ -1217,7 +1217,7 @@ public function testTrackNoAttributesWithEventValue() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); - // Verify that fireNotifications is called with expected params + // Verify that sendNotifications is called with expected params $arrayParam = array( 'purchase', 'test_user', @@ -1227,7 +1227,7 @@ public function testTrackNoAttributesWithEventValue() ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::TRACK, $arrayParam @@ -1313,7 +1313,7 @@ public function testTrackNoAttributesWithInvalidEventValue() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); - // Verify that fireNotifications is called with expected params + // Verify that sendNotifications is called with expected params $arrayParam = array( 'purchase', 'test_user', @@ -1323,7 +1323,7 @@ public function testTrackNoAttributesWithInvalidEventValue() ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::TRACK, $arrayParam @@ -1435,7 +1435,7 @@ public function testTrackWithAttributesWithDeprecatedEventValue() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); - // Verify that fireNotifications is called with expected params + // Verify that sendNotifications is called with expected params $arrayParam = array( 'purchase', 'test_user', @@ -1445,7 +1445,7 @@ public function testTrackWithAttributesWithDeprecatedEventValue() ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::TRACK, $arrayParam @@ -1552,7 +1552,7 @@ public function testTrackWithAttributesWithEventValue() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optlyObject, $this->notificationCenterMock); - // Verify that fireNotifications is called with expected params + // Verify that sendNotifications is called with expected params $arrayParam = array( 'purchase', 'test_user', @@ -1562,7 +1562,7 @@ public function testTrackWithAttributesWithEventValue() ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::TRACK, $arrayParam @@ -1995,9 +1995,9 @@ public function testIsFeatureEnabledGivenFeatureFlagIsNotEnabledForUser() $notificationCenter->setAccessible(true); $notificationCenter->setValue($optimizelyMock, $this->notificationCenterMock); - // verify that fireNotifications isn't called + // verify that sendNotifications isn't called $this->notificationCenterMock->expects($this->never()) - ->method('fireNotifications'); + ->method('sendNotifications'); $this->loggerMock->expects($this->at(0)) ->method('log') @@ -2048,7 +2048,7 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExper $notificationCenter->setAccessible(true); $notificationCenter->setValue($optimizelyMock, $this->notificationCenterMock); - // verify that fireNotifications is called with expected params + // verify that sendNotifications is called with expected params $arrayParam = array( 'double_single_variable_feature', 'user_id', @@ -2058,7 +2058,7 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExper ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::FEATURE_EXPERIMENT, $arrayParam); $this->loggerMock->expects($this->at(0)) @@ -2101,16 +2101,16 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingEx $notificationCenter->setAccessible(true); $notificationCenter->setValue($optimizelyMock, $this->notificationCenterMock); - // verify that fireNotifications is called with expected params + // verify that sendNotifications is called with expected params $arrayParam = array( 'boolean_single_variable_feature', 'user_id', [], - $this->projectConfig->getAudience($experiment->getAudienceIds()[0]) + [$this->projectConfig->getAudience($experiment->getAudienceIds()[0])] ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::FEATURE_ROLLOUT, $arrayParam); $decisionServiceMock->expects($this->exactly(1)) @@ -2526,7 +2526,7 @@ public function testSendImpressionEventWithNoAttributes(){ ['param1' => 'val1', 'param2' => 'val2'], 'POST', []) ); - // verify that fireNotifications is called with expected params + // verify that sendNotifications is called with expected params $arrayParam = array( $this->projectConfig->getExperimentFromKey('group_experiment_1'), 'user_1', @@ -2538,7 +2538,7 @@ public function testSendImpressionEventWithNoAttributes(){ ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::ACTIVATE, $arrayParam @@ -2584,7 +2584,7 @@ public function testSendImpressionEventWithAttributes(){ ) ->willReturn(new LogEvent('logx.optimizely.com/decision', ['param1' => 'val1'], 'POST', [])); - // verify that fireNotifications is called with expected params + // verify that sendNotifications is called with expected params $arrayParam = array( $this->projectConfig->getExperimentFromKey('test_experiment'), 'test_user', @@ -2596,7 +2596,7 @@ public function testSendImpressionEventWithAttributes(){ ); $this->notificationCenterMock->expects($this->once()) - ->method('fireNotifications') + ->method('sendNotifications') ->with( NotificationType::ACTIVATE, $arrayParam From e8863d6e02962ed7c9e2cfb16c1a8a003fd16e7a Mon Sep 17 00:00:00 2001 From: Owais Date: Tue, 21 Nov 2017 13:22:26 +0500 Subject: [PATCH 4/7] :pen: Code coverage nit --- tests/ProjectConfigTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/ProjectConfigTest.php b/tests/ProjectConfigTest.php index 8a8e207c..9577c27d 100644 --- a/tests/ProjectConfigTest.php +++ b/tests/ProjectConfigTest.php @@ -626,4 +626,10 @@ public function testGetForcedVariationLogs() $this->config->getForcedVariation($experimentKey, $userId); } + // Adding for code coverage + // Should return null when none of the rollouts has an experiment with the given ID + public function testGetRolloutExperimentFromId(){ + $this->assertNull($this->config->getRolloutExperimentFromId('aabbcc')); + } + } From a18700d0f75be90fa951bc003ea71232daf2fc21 Mon Sep 17 00:00:00 2001 From: Owais Date: Wed, 22 Nov 2017 15:28:12 +0500 Subject: [PATCH 5/7] :pencil2: feature_experiment/rollout notifications removed --- .../Notification/NotificationType.php | 2 -- src/Optimizely/Optimizely.php | 29 ---------------- src/Optimizely/ProjectConfig.php | 24 -------------- .../NotificationCenterTest.php | 31 ----------------- tests/OptimizelyTest.php | 33 ------------------- tests/ProjectConfigTest.php | 6 ---- 6 files changed, 125 deletions(-) diff --git a/src/Optimizely/Notification/NotificationType.php b/src/Optimizely/Notification/NotificationType.php index b12fa677..996a2859 100644 --- a/src/Optimizely/Notification/NotificationType.php +++ b/src/Optimizely/Notification/NotificationType.php @@ -21,8 +21,6 @@ class NotificationType // format is EVENT: list of parameters to callback. const ACTIVATE = "ACTIVATE:experiment, user_id,attributes, variation, event"; const TRACK = "TRACK:event_key, user_id, attributes, event_tags, event"; - const FEATURE_EXPERIMENT = "FEATURE_EXPERIMENT:feature_key, user_id, attributes, experiment, variation"; - const FEATURE_ROLLOUT = "FEATURE_ROLLOUT:feature_key, user_id, attributes, audience"; public static function isNotificationTypeValid($notification_type) { diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index e7c46bf5..1a5dc9a7 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -491,37 +491,8 @@ public function isFeatureEnabled($featureFlagKey, $userId, $attributes = null) $this->sendImpressionEvent($experiment->getKey(), $variation->getKey(), $userId, $attributes); - $this->_notificationCenter->sendNotifications( - NotificationType::FEATURE_EXPERIMENT, - array( - $featureFlagKey, - $userId, - $attributes, - $experiment, - $variation - ) - ); - } else { $this->_logger->log(Logger::INFO, "The user '{$userId}' is not being experimented on Feature Flag '{$featureFlagKey}'."); - - $experiment = $this->_config->getRolloutExperimentFromId($experiment_id); - $audience = null; - if($experiment->getAudienceIds() && !empty($experiment->getAudienceIds())){ - $audienceId = $experiment->getAudienceIds()[0]; - $audience = $this->_config->getAudience($audienceId); - } - - - $this->_notificationCenter->sendNotifications( - NotificationType::FEATURE_ROLLOUT, - array( - $featureFlagKey, - $userId, - $attributes, - [$audience] - ) - ); } $this->_logger->log(Logger::INFO, "Feature Flag '{$featureFlagKey}' is enabled for user '{$userId}'."); diff --git a/src/Optimizely/ProjectConfig.php b/src/Optimizely/ProjectConfig.php index 01c761fb..4abb830e 100644 --- a/src/Optimizely/ProjectConfig.php +++ b/src/Optimizely/ProjectConfig.php @@ -472,30 +472,6 @@ public function getFeatureVariableFromKey($featureFlagKey, $variableKey) return null; } - /** - * Gets the experiment object from a rollout - * - * Note: This method is needed in the case where we want to fetch an experiment/rule inside - * a rollout. Other methods getExperimentById/Key can't be used because we do - * not store rollout rules/experiments in our experiment/variation ID maps. - * No need to throw an exception, since this method will not be directly called as a result of - * a public API method param and will receive pre-verified experimentId - * - * @param string $experimentId Experiment ID - * - * @return Experiment Experiment object corresponding to given experiment/rollout rule ID - */ - public function getRolloutExperimentFromId($experimentId){ - foreach($this->_rollouts as $rollout){ - $experiments = $rollout->getExperiments(); - foreach($experiments as $experiment){ - if($experiment->getId() == $experimentId) - return $experiment; - } - } - return null; - } - public function isVariationIdValid($experimentKey, $variationId) { return isset($this->_variationIdMap[$experimentKey]) && diff --git a/tests/NotificationTests/NotificationCenterTest.php b/tests/NotificationTests/NotificationCenterTest.php index 1c0fd67d..28a434d5 100644 --- a/tests/NotificationTests/NotificationCenterTest.php +++ b/tests/NotificationTests/NotificationCenterTest.php @@ -172,21 +172,6 @@ public function testAddNotificationForMultipleNotificationTypes() 1, sizeof($this->notificationCenterObj->getNotifications()[NotificationType::TRACK]) ); - - $this->loggerMock->expects($this->at(0)) - ->method('log') - ->with(Logger::INFO, sprintf("Callback added for notification type '%s'.", NotificationType::FEATURE_EXPERIMENT)); - $this->assertSame( - 3, - $this->notificationCenterObj->addNotificationListener(NotificationType::FEATURE_EXPERIMENT, function () { - }) - ); - - // verify that notifications length for NotificationType::FEATURE_EXPERIMENT has incremented by 1 - $this->assertSame( - 1, - sizeof($this->notificationCenterObj->getNotifications()[NotificationType::FEATURE_EXPERIMENT]) - ); } public function testAddNotificationForMultipleCallbacksForASingleNotificationType() @@ -515,10 +500,6 @@ public function testCleanAllNotifications() 0, sizeof($notificationCenterA->getNotifications()[NotificationType::TRACK]) ); - $this->assertSame( - 0, - sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_EXPERIMENT]) - ); // add a callback for multiple notification types $notificationCenterA->addNotificationListener(NotificationType::ACTIVATE, function () { @@ -533,10 +514,6 @@ public function testCleanAllNotifications() $notificationCenterA->addNotificationListener(NotificationType::TRACK, function () { }); - $notificationCenterA->addNotificationListener(NotificationType::FEATURE_EXPERIMENT, function () { - }); - - // verify that notifications length for each type reflects the just added callbacks $this->assertSame( 3, @@ -546,10 +523,6 @@ public function testCleanAllNotifications() 2, sizeof($notificationCenterA->getNotifications()[NotificationType::TRACK]) ); - $this->assertSame( - 1, - sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_EXPERIMENT]) - ); //////////////////////////////////////////////////////////////////////////////////////////////////// // === verify that cleanAllNotifications removes all notifications for each notification type === // @@ -566,10 +539,6 @@ public function testCleanAllNotifications() 0, sizeof($notificationCenterA->getNotifications()[NotificationType::TRACK]) ); - $this->assertSame( - 0, - sizeof($notificationCenterA->getNotifications()[NotificationType::FEATURE_EXPERIMENT]) - ); /////////////////////////////////////////////////////////////////////////////////////// //=== verify that cleanAllNotifications doesn't throw an error when called again === // diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index 75a1d83d..6bf91e65 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -2044,23 +2044,6 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsBeingExper ->method('sendImpressionEvent') ->with('test_experiment_double_feature', 'control', 'user_id', []); - $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); - $notificationCenter->setAccessible(true); - $notificationCenter->setValue($optimizelyMock, $this->notificationCenterMock); - - // verify that sendNotifications is called with expected params - $arrayParam = array( - 'double_single_variable_feature', - 'user_id', - [], - $experiment, - $variation - ); - - $this->notificationCenterMock->expects($this->once()) - ->method('sendNotifications') - ->with( NotificationType::FEATURE_EXPERIMENT, $arrayParam); - $this->loggerMock->expects($this->at(0)) ->method('log') ->with(Logger::INFO, "Feature Flag 'double_single_variable_feature' is enabled for user 'user_id'."); @@ -2097,22 +2080,6 @@ public function testIsFeatureEnabledGivenFeatureFlagIsEnabledAndUserIsNotBeingEx FeatureDecision::DECISION_SOURCE_ROLLOUT ); - $notificationCenter = new \ReflectionProperty(Optimizely::class, '_notificationCenter'); - $notificationCenter->setAccessible(true); - $notificationCenter->setValue($optimizelyMock, $this->notificationCenterMock); - - // verify that sendNotifications is called with expected params - $arrayParam = array( - 'boolean_single_variable_feature', - 'user_id', - [], - [$this->projectConfig->getAudience($experiment->getAudienceIds()[0])] - ); - - $this->notificationCenterMock->expects($this->once()) - ->method('sendNotifications') - ->with( NotificationType::FEATURE_ROLLOUT, $arrayParam); - $decisionServiceMock->expects($this->exactly(1)) ->method('getVariationForFeature') ->will($this->returnValue($expected_decision)); diff --git a/tests/ProjectConfigTest.php b/tests/ProjectConfigTest.php index 9577c27d..8a8e207c 100644 --- a/tests/ProjectConfigTest.php +++ b/tests/ProjectConfigTest.php @@ -626,10 +626,4 @@ public function testGetForcedVariationLogs() $this->config->getForcedVariation($experimentKey, $userId); } - // Adding for code coverage - // Should return null when none of the rollouts has an experiment with the given ID - public function testGetRolloutExperimentFromId(){ - $this->assertNull($this->config->getRolloutExperimentFromId('aabbcc')); - } - } From 870cb5ea5d0453767f209a3e8871b316115edc97 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Tue, 28 Nov 2017 00:57:27 +0500 Subject: [PATCH 6/7] nit: Space --- src/Optimizely/Notification/NotificationType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Optimizely/Notification/NotificationType.php b/src/Optimizely/Notification/NotificationType.php index b12fa677..f51e87f3 100644 --- a/src/Optimizely/Notification/NotificationType.php +++ b/src/Optimizely/Notification/NotificationType.php @@ -19,7 +19,7 @@ class NotificationType { // format is EVENT: list of parameters to callback. - const ACTIVATE = "ACTIVATE:experiment, user_id,attributes, variation, event"; + const ACTIVATE = "ACTIVATE:experiment, user_id, attributes, variation, event"; const TRACK = "TRACK:event_key, user_id, attributes, event_tags, event"; const FEATURE_EXPERIMENT = "FEATURE_EXPERIMENT:feature_key, user_id, attributes, experiment, variation"; const FEATURE_ROLLOUT = "FEATURE_ROLLOUT:feature_key, user_id, attributes, audience"; From a2e8beeeb67547593adbe5f0d9edfcb3698f4b92 Mon Sep 17 00:00:00 2001 From: Owais Akbani Date: Wed, 29 Nov 2017 18:21:18 +0500 Subject: [PATCH 7/7] Improve Code Coverage --- .../Notification/NotificationCenter.php | 7 - src/Optimizely/Optimizely.php | 1 + src/Optimizely/ProjectConfig.php | 11 -- .../NotificationCenterTest.php | 14 ++ tests/OptimizelyTest.php | 135 +++++++++++++++++- tests/ProjectConfigTest.php | 3 +- tests/TestData.php | 5 + 7 files changed, 155 insertions(+), 21 deletions(-) diff --git a/src/Optimizely/Notification/NotificationCenter.php b/src/Optimizely/Notification/NotificationCenter.php index eeb15d4f..8d7119d0 100644 --- a/src/Optimizely/Notification/NotificationCenter.php +++ b/src/Optimizely/Notification/NotificationCenter.php @@ -50,11 +50,6 @@ public function __construct(LoggerInterface $logger, ErrorHandlerInterface $erro $this->_errorHandler = $errorHandler; } - public function getNotificationId() - { - return $this->_notificationId; - } - public function getNotifications() { return $this->_notifications; @@ -179,8 +174,6 @@ public function sendNotifications($notification_type, array $args = []) call_user_func_array($callback, $args); } catch (ArgumentCountError $e) { $this->reportArgumentCountError(); - } catch (Throwable $e) { - $this->_logger->log(Logger::ERROR, "Problem calling notify callback."); } catch (Exception $e) { $this->_logger->log(Logger::ERROR, "Problem calling notify callback."); } diff --git a/src/Optimizely/Optimizely.php b/src/Optimizely/Optimizely.php index 13300efc..882dba66 100644 --- a/src/Optimizely/Optimizely.php +++ b/src/Optimizely/Optimizely.php @@ -177,6 +177,7 @@ private function validateUserInputs($attributes, $eventTags = null) { $this->_errorHandler->handleError( new InvalidEventTagException('Provided event tags are in an invalid format.') ); + return false; } } diff --git a/src/Optimizely/ProjectConfig.php b/src/Optimizely/ProjectConfig.php index 4abb830e..c2ec5dd7 100644 --- a/src/Optimizely/ProjectConfig.php +++ b/src/Optimizely/ProjectConfig.php @@ -514,19 +514,8 @@ public function getForcedVariation($experimentKey, $userId) } $variationId = $experimentToVariationMap[$experimentId]; - // check for null and empty string variation ID - if (strlen($variationId) == 0) { - $this->_logger->log(Logger::DEBUG, sprintf('No variation mapped to experiment "%s" in the forced variation map.', $experimentKey)); - return null; - } - $variation = $this->getVariationFromId($experimentKey, $variationId); $variationKey = $variation->getKey(); - // check if the variation exists in the datafile (a new variation is returned if it is not in the datafile) - if (strlen($variationKey) == 0) { - // this case is logged in getVariationFromId - return null; - } $this->_logger->log(Logger::DEBUG, sprintf('Variation "%s" is mapped to experiment "%s" and user "%s" in the forced variation map', $variationKey, $experimentKey, $userId)); diff --git a/tests/NotificationTests/NotificationCenterTest.php b/tests/NotificationTests/NotificationCenterTest.php index 28a434d5..a2dde1ce 100644 --- a/tests/NotificationTests/NotificationCenterTest.php +++ b/tests/NotificationTests/NotificationCenterTest.php @@ -606,6 +606,20 @@ public function testsendNotificationsAndVerifyThatAllCallbacksWithoutArgsAreCall ->method('track_callback_no_args'); $this->notificationCenterObj->sendNotifications(NotificationType::ACTIVATE); + + //////////////////////////////////////////////////////////////////////////////////////////// + // === Verify that none of the callbacks are called given an invalid NotificationType === // + //////////////////////////////////////////////////////////////////////////////////////////// + + $clientMock->expects($this->never()) + ->method('decision_callback_no_args'); + $clientMock->expects($this->never()) + ->method('decision_callback_no_args_2'); + + $clientMock->expects($this->never()) + ->method('track_callback_no_args'); + + $this->notificationCenterObj->sendNotifications("abacada"); } public function testsendNotificationsAndVerifyThatAllCallbacksWithArgsAreCalled() diff --git a/tests/OptimizelyTest.php b/tests/OptimizelyTest.php index df57ab45..865c15b4 100644 --- a/tests/OptimizelyTest.php +++ b/tests/OptimizelyTest.php @@ -23,6 +23,7 @@ use Optimizely\ErrorHandler\NoOpErrorHandler; use Optimizely\Event\LogEvent; use Optimizely\Exceptions\InvalidAttributeException; +use Optimizely\Exceptions\InvalidEventTagException; use Optimizely\Logger\NoOpLogger; use Optimizely\Notification\NotificationCenter; use Optimizely\Notification\NotificationType; @@ -674,10 +675,90 @@ public function testTrackInvalidAttributes() $this->notificationCenterMock->expects($this->never()) ->method('sendNotifications'); - // Call activate + // Call track $this->assertNull($optlyObject->track('purchase', 'test_user', 42)); } + public function testTrackInvalidEventTags() + { + $this->loggerMock->expects($this->once()) + ->method('log') + ->with(Logger::ERROR, 'Provided event tags are in an invalid format.'); + + $errorHandlerMock = $this->getMockBuilder(NoOpErrorHandler::class) + ->setMethods(array('handleError')) + ->getMock(); + $errorHandlerMock->expects($this->once()) + ->method('handleError') + ->with(new InvalidEventTagException('Provided event tags are in an invalid format.')); + + $optlyObject = new Optimizely( + $this->datafile, null, $this->loggerMock, $errorHandlerMock + ); + + $optlyObject->track('purchase', 'test_user', [], [1=>2]); + } + + public function testTrackUnknownEventKey() + { + $this->loggerMock->expects($this->at(0)) + ->method('log') + ->with(Logger::ERROR, 'Event key "unknown_key" is not in datafile.'); + + + $this->loggerMock->expects($this->at(1)) + ->method('log') + ->with(Logger::ERROR, 'Not tracking user "test_user" for event "unknown_key".'); + + $this->optimizelyObject->track('unknown_key', 'test_user'); + } + + public function testActivateGivenEventKeyWithNoExperiments() + { + $this->loggerMock->expects($this->once()) + ->method('log') + ->with(Logger::INFO, 'There are no valid experiments for event "unlinked_event" to track.'); + + $this->optimizelyObject->track('unlinked_event', 'test_user'); + } + + public function testTrackEventDispatchFailure(){ + + $eventDispatcherMock = $this->getMockBuilder(DefaultEventDispatcher::class) + ->setMethods(array('dispatchEvent')) + ->getMock(); + + $this->eventBuilderMock->expects($this->once()) + ->method('createConversionEvent') + ->with( + $this->projectConfig, + 'purchase', + ["7718750065" => "7725250007"], + 'test_user', + null, + null + ) + ->willReturn(new LogEvent('logx.optimizely.com/track', ['param1' => 'val1'], 'POST', [])); + + $eventDispatcher = new \ReflectionProperty(Optimizely::class, '_eventDispatcher'); + $eventDispatcher->setAccessible(true); + $eventDispatcher->setValue($this->optimizelyObject, $eventDispatcherMock); + + $eventBuilder = new \ReflectionProperty(Optimizely::class, '_eventBuilder'); + $eventBuilder->setAccessible(true); + $eventBuilder->setValue($this->optimizelyObject, $this->eventBuilderMock); + + $eventDispatcherMock->expects($this->once()) + ->method('dispatchEvent') + ->will($this->throwException(new Exception)); + + $this->loggerMock->expects($this->at(16)) + ->method('log') + ->with(Logger::ERROR, 'Unable to dispatch conversion event. Error '); + + $this->optimizelyObject->track('purchase', 'test_user'); + } + public function testTrackNoAttributesNoEventValue() { $this->eventBuilderMock->expects($this->once()) @@ -2477,6 +2558,34 @@ public function testGetFeatureVariableString() ); } + public function testGetFeatureVariableMethodsReturnNullWhenGetVariableValueForTypeReturnsNull(){ + $optimizelyMock = $this->getMockBuilder(Optimizely::class) + ->setConstructorArgs(array($this->datafile, null, $this->loggerMock)) + ->setMethods(array('getFeatureVariableValueForType')) + ->getMock(); + $optimizelyMock->expects($this->exactly(4)) + ->method('getFeatureVariableValueForType') + ->willReturn(null); + + $this->assertNull( + $optimizelyMock->getFeatureVariableBoolean( + 'boolean_single_variable_feature', 'boolean_variable', 'user_id', []) + ); + $this->assertNull( + $optimizelyMock->getFeatureVariableString( + 'string_single_variable_feature', 'string_variable', 'user_id', []) + ); + $this->assertNull( + $optimizelyMock->getFeatureVariableDouble( + 'double_single_variable_feature', 'double_variable', 'user_id', []) + ); + $this->assertNull( + $optimizelyMock->getFeatureVariableInteger( + 'integer_single_variable_feature', 'integer_variable', 'user_id', []) + ); + + } + public function testSendImpressionEventWithNoAttributes(){ $optlyObject = new OptimizelyTester($this->datafile, new ValidEventDispatcher(), $this->loggerMock); @@ -2531,7 +2640,29 @@ public function testSendImpressionEventWithNoAttributes(){ $optlyObject->sendImpressionEvent('group_experiment_1', 'group_exp_1_var_2', 'user_1', null); } - + public function testSendImpressionEventDispatchFailure() + { + $optlyObject = new OptimizelyTester($this->datafile, new ValidEventDispatcher(), $this->loggerMock); + + $eventDispatcherMock = $this->getMockBuilder(DefaultEventDispatcher::class) + ->setMethods(array('dispatchEvent')) + ->getMock(); + + $eventDispatcher = new \ReflectionProperty(Optimizely::class, '_eventDispatcher'); + $eventDispatcher->setAccessible(true); + $eventDispatcher->setValue($optlyObject, $eventDispatcherMock); + + $eventDispatcherMock->expects($this->once()) + ->method('dispatchEvent') + ->will($this->throwException(new Exception)); + + $this->loggerMock->expects($this->at(2)) + ->method('log') + ->with(Logger::ERROR, 'Unable to dispatch impression event. Error '); + + $optlyObject->sendImpressionEvent('test_experiment', 'control', 'test_user', []); + } + public function testSendImpressionEventWithAttributes(){ $optlyObject = new OptimizelyTester($this->datafile, new ValidEventDispatcher(), $this->loggerMock); diff --git a/tests/ProjectConfigTest.php b/tests/ProjectConfigTest.php index 8a8e207c..7de044e1 100644 --- a/tests/ProjectConfigTest.php +++ b/tests/ProjectConfigTest.php @@ -122,7 +122,8 @@ public function testInit() $eventKeyMap = new \ReflectionProperty(ProjectConfig::class, '_eventKeyMap'); $eventKeyMap->setAccessible(true); $this->assertEquals([ - 'purchase' => $this->config->getEvent('purchase') + 'purchase' => $this->config->getEvent('purchase'), + 'unlinked_event' => $this->config->getEvent('unlinked_event') ], $eventKeyMap->getValue($this->config)); // Check attribute key map diff --git a/tests/TestData.php b/tests/TestData.php index 5d5a2f87..74dc994a 100644 --- a/tests/TestData.php +++ b/tests/TestData.php @@ -458,6 +458,11 @@ ], "id": "7718020063", "key": "purchase" + }, + { + "experimentIds": [], + "id": "7718020064", + "key": "unlinked_event" } ], "anonymizeIP": false,