diff --git a/lib/Config/Database.php b/lib/Config/Database.php index 152885ee91..d5c70d1d34 100644 --- a/lib/Config/Database.php +++ b/lib/Config/Database.php @@ -86,6 +86,7 @@ class Database { $statistics_forms = Env::$db_prefix . 'statistics_forms'; $mapping_to_external_entities = Env::$db_prefix . 'mapping_to_external_entities'; $log = Env::$db_prefix . 'log'; + $stats_notifications = Env::$db_prefix . 'stats_notifications'; define('MP_SETTINGS_TABLE', $settings); define('MP_SEGMENTS_TABLE', $segments); @@ -112,6 +113,7 @@ class Database { define('MP_STATISTICS_FORMS_TABLE', $statistics_forms); define('MP_MAPPING_TO_EXTERNAL_ENTITIES_TABLE', $mapping_to_external_entities); define('MP_LOG_TABLE', $log); + define('MP_STATS_NOTIFICATIONS_TABLE', $stats_notifications); } } } diff --git a/lib/Config/Migrator.php b/lib/Config/Migrator.php index fd02fcbbd8..574f8b34ee 100644 --- a/lib/Config/Migrator.php +++ b/lib/Config/Migrator.php @@ -23,6 +23,7 @@ class Migrator { 'settings', 'custom_fields', 'scheduled_tasks', + 'stats_notifications', 'scheduled_task_subscribers', 'sending_queues', 'subscribers', @@ -131,12 +132,26 @@ class Migrator { return $this->sqlify(__FUNCTION__, $attributes); } - function scheduledTaskSubscribers() { + function statsNotifications() { $attributes = array( + 'id int(11) unsigned NOT NULL AUTO_INCREMENT,', + 'newsletter_id int(11) unsigned NOT NULL,', + 'task_id int(11) unsigned NOT NULL,', + 'created_at TIMESTAMP NULL,', + 'updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,', + 'PRIMARY KEY (id),', + 'UNIQUE KEY newsletter_id_task_id (newsletter_id, task_id),', + 'KEY task_id (task_id)', + ); + return $this->sqlify(__FUNCTION__, $attributes); + } + + function scheduledTaskSubscribers() { + $attributes = array ( 'task_id int(11) unsigned NOT NULL,', 'subscriber_id int(11) unsigned NOT NULL,', 'processed int(1) NOT NULL,', - 'failed int(1) NOT NULL DEFAULT 0,', + 'failed SMALLINT(1) NOT NULL DEFAULT 0,', 'error text NULL,', 'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,', 'PRIMARY KEY (task_id, subscriber_id),', diff --git a/lib/Config/Populator.php b/lib/Config/Populator.php index 9150ad639e..43db70790a 100644 --- a/lib/Config/Populator.php +++ b/lib/Config/Populator.php @@ -194,6 +194,15 @@ class Populator { ]); } + $stats_notifications = Setting::getValue('stats_notifications'); + if(empty($stats_notifications)) { + $sender = Setting::getValue('sender', []); + Setting::setValue('stats_notifications', [ + 'enabled' => true, + 'address' => isset($sender['address'])? $sender['address'] : null, + ]); + } + // reset mailer log MailerLog::resetMailerLog(); } diff --git a/lib/Cron/Daemon.php b/lib/Cron/Daemon.php index fc0c86daa4..a2ee283efe 100644 --- a/lib/Cron/Daemon.php +++ b/lib/Cron/Daemon.php @@ -1,20 +1,19 @@ timer = microtime(true); + $this->workers_factory = $workers_factory; } function run($settings_daemon_data) { @@ -22,6 +21,7 @@ class Daemon { CronHelper::saveDaemon($settings_daemon_data); try { $this->executeMigrationWorker(); + $this->executeStatsNotificationsWorker(); $this->executeScheduleWorker(); $this->executeQueueWorker(); $this->executeSendingServiceKeyCheckWorker(); @@ -35,32 +35,37 @@ class Daemon { } function executeScheduleWorker() { - $scheduler = new SchedulerWorker($this->timer); + $scheduler = $this->workers_factory->createScheduleWorker($this->timer); return $scheduler->process(); } function executeQueueWorker() { - $queue = new SendingQueueWorker(new SendingErrorHandler(), $this->timer); + $queue = $this->workers_factory->createQueueWorker($this->timer); return $queue->process(); } + function executeStatsNotificationsWorker() { + $worker = $this->workers_factory->createStatsNotificationsWorker($this->timer); + return $worker->process(); + } + function executeSendingServiceKeyCheckWorker() { - $worker = new SendingServiceKeyCheckWorker($this->timer); + $worker = $this->workers_factory->createSendingServiceKeyCheckWorker($this->timer); return $worker->process(); } function executePremiumKeyCheckWorker() { - $worker = new PremiumKeyCheckWorker($this->timer); + $worker = $this->workers_factory->createPremiumKeyCheckWorker($this->timer); return $worker->process(); } function executeBounceWorker() { - $bounce = new BounceWorker($this->timer); + $bounce = $this->workers_factory->createBounceWorker($this->timer); return $bounce->process(); } function executeMigrationWorker() { - $migration = new MigrationWorker($this->timer); + $migration = $this->workers_factory->createMigrationWorker($this->timer); return $migration->process(); } diff --git a/lib/Cron/Triggers/WordPress.php b/lib/Cron/Triggers/WordPress.php index 9bfc417db8..5f97591df2 100644 --- a/lib/Cron/Triggers/WordPress.php +++ b/lib/Cron/Triggers/WordPress.php @@ -8,6 +8,7 @@ use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker; use MailPoet\Cron\Workers\Bounce as BounceWorker; use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck as PremiumKeyCheckWorker; use MailPoet\Cron\Workers\KeyCheck\SendingServiceKeyCheck as SendingServiceKeyCheckWorker; +use MailPoet\Cron\Workers\StatsNotifications\Worker; use MailPoet\Mailer\MailerLog; use MailPoet\Models\Setting; use MailPoet\Services\Bridge; @@ -44,6 +45,8 @@ class WordPress { $premium_key_specified = Bridge::isPremiumKeySpecified(); $premium_keycheck_due_tasks = PremiumKeyCheckWorker::getDueTasks(); $premium_keycheck_future_tasks = PremiumKeyCheckWorker::getFutureTasks(); + // stats notifications + $stats_notifications_tasks = (bool)Worker::getDueTasks(); // check requirements for each worker $sending_queue_active = (($scheduled_queues || $running_queues) && !$sending_limit_reached && !$sending_is_paused); $bounce_sync_active = ($mp_sending_enabled && ($bounce_due_tasks || !$bounce_future_tasks)); @@ -57,6 +60,7 @@ class WordPress { || $bounce_sync_active || $sending_service_key_check_active || $premium_key_check_active + || $stats_notifications_tasks ); } diff --git a/lib/Cron/Workers/SendingQueue/SendingQueue.php b/lib/Cron/Workers/SendingQueue/SendingQueue.php index dc49b1db0a..970c5b654b 100644 --- a/lib/Cron/Workers/SendingQueue/SendingQueue.php +++ b/lib/Cron/Workers/SendingQueue/SendingQueue.php @@ -5,6 +5,7 @@ use MailPoet\Cron\CronHelper; use MailPoet\Cron\Workers\SendingQueue\Tasks\Links; use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask; use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask; +use MailPoet\Cron\Workers\StatsNotifications\Scheduler as StatsNotificationsScheduler; use MailPoet\Logging\Logger; use MailPoet\Mailer\MailerError; use MailPoet\Mailer\MailerLog; @@ -26,11 +27,15 @@ class SendingQueue { const BATCH_SIZE = 20; const TASK_BATCH_SIZE = 5; + /** @var StatsNotificationsScheduler */ + public $stats_notifications_scheduler; + /** @var SendingErrorHandler */ private $error_handler; - function __construct(SendingErrorHandler $error_handler, $timer = false, $mailer_task = false, $newsletter_task = false) { + function __construct(SendingErrorHandler $error_handler, StatsNotificationsScheduler $stats_notifications_scheduler, $timer = false, $mailer_task = false, $newsletter_task = false) { $this->error_handler = $error_handler; + $this->stats_notifications_scheduler = $stats_notifications_scheduler; $this->mailer_task = ($mailer_task) ? $mailer_task : new MailerTask(); $this->newsletter_task = ($newsletter_task) ? $newsletter_task : new NewsletterTask(); $this->timer = ($timer) ? $timer : microtime(true); @@ -99,6 +104,7 @@ class SendingQueue { ); if($queue->status === ScheduledTaskModel::STATUS_COMPLETED) { $this->newsletter_task->markNewsletterAsSent($newsletter, $queue); + $this->stats_notifications_scheduler->schedule($newsletter); } $this->enforceSendingAndExecutionLimits(); } diff --git a/lib/Cron/Workers/StatsNotifications/Scheduler.php b/lib/Cron/Workers/StatsNotifications/Scheduler.php new file mode 100644 index 0000000000..ba094f17f4 --- /dev/null +++ b/lib/Cron/Workers/StatsNotifications/Scheduler.php @@ -0,0 +1,84 @@ +shouldSchedule($newsletter)) { + return false; + } + + $task = ScheduledTask::create(); + $task->type = Worker::TASK_TYPE; + $task->status = ScheduledTask::STATUS_SCHEDULED; + $task->scheduled_at = $this->getNextRunDate(); + $task->save(); + + $stats_notifications = StatsNotification::create(); + $stats_notifications->newsletter_id = $newsletter->id; + $stats_notifications->task_id = $task->id; + $stats_notifications->save(); + } + + private function shouldSchedule(Newsletter $newsletter) { + if($this->isDisabled()) { + return false; + } + if($this->isTaskScheduled($newsletter->id)) { + return false; + } + if(($newsletter->type !== Newsletter::TYPE_NOTIFICATION) && ($newsletter->type !== Newsletter::TYPE_STANDARD)) { + return false; + } + return true; + } + + private function isDisabled() { + $settings = Setting::getValue(Worker::SETTINGS_KEY); + if(!is_array($settings)) { + return true; + } + if(!isset($settings['enabled'])) { + return true; + } + if(!isset($settings['address'])) { + return true; + } + if(empty(trim($settings['address']))) { + return true; + } + if(!(bool)Setting::getValue('tracking.enabled')) { + return true; + } + return !(bool)$settings['enabled']; + } + + private function isTaskScheduled($newsletter_id) { + $existing = ScheduledTask::table_alias('tasks') + ->join(StatsNotification::$_table, 'tasks.id = notification.task_id', 'notification') + ->where('tasks.type', Worker::TASK_TYPE) + ->where('notification.newsletter_id', $newsletter_id) + ->findMany(); + return (bool)$existing; + } + + private function getNextRunDate() { + $date = new Carbon(); + $date->addHours(self::HOURS_TO_SEND_AFTER_NEWSLETTER); + return $date; + } + +} diff --git a/lib/Cron/Workers/StatsNotifications/Worker.php b/lib/Cron/Workers/StatsNotifications/Worker.php new file mode 100644 index 0000000000..3a216dfff7 --- /dev/null +++ b/lib/Cron/Workers/StatsNotifications/Worker.php @@ -0,0 +1,138 @@ +timer = $timer ?: microtime(true); + $this->renderer = $renderer; + $this->mailer = $mailer; + } + + /** @throws \Exception */ + function process() { + $settings = Setting::getValue(self::SETTINGS_KEY); + $this->mailer->sender = $this->mailer->getSenderNameAndAddress($this->constructSenderEmail()); + foreach(self::getDueTasks() as $task) { + try { + $this->mailer->send($this->constructNewsletter($task), $settings['address']); + } catch(\Exception $e) { + if(WP_DEBUG) { + throw $e; + } + } finally { + $this->markTaskAsFinished($task); + } + CronHelper::enforceExecutionLimit($this->timer); + } + } + + private function constructSenderEmail() { + $url_parts = parse_url(home_url()); + $site_name = strtolower($url_parts['host']); + if(strpos($site_name, 'www.') === 0) { + $site_name = substr($site_name, 4); + } + return [ + 'address' => self::SENDER_EMAIL_PREFIX . $site_name, + 'name' => self::SENDER_EMAIL_PREFIX . $site_name, + ]; + } + + public static function getDueTasks() { + $date = new Carbon(); + return ScheduledTask::orderByAsc('priority') + ->orderByAsc('updated_at') + ->whereNull('deleted_at') + ->where('status', ScheduledTask::STATUS_SCHEDULED) + ->whereLte('scheduled_at', $date) + ->where('type', self::TASK_TYPE) + ->limit(Sending::RESULT_BATCH_SIZE) + ->findMany(); + } + + private function constructNewsletter(ScheduledTask $task) { + $newsletter = $this->getNewsletter($task); + $link = NewsletterLink::findTopLinkForNewsletter($newsletter); + $context = $this->prepareContext($newsletter, $link); + return [ + 'subject' => sprintf(_x('Stats for email %s', 'title of an automatic email containing statistics (newsletter open rate, click rate, etc)', 'mailpoet'), $newsletter->subject), + 'body' => [ + 'html' => $this->renderer->render('emails/statsNotification.html', $context), + 'text' => $this->renderer->render('emails/statsNotification.txt', $context), + ], + ]; + } + + private function getNewsletter(ScheduledTask $task) { + $statsNotificationModel = $task->statsNotification()->findOne(); + $newsletter = $statsNotificationModel->newsletter()->findOne(); + if(!$newsletter) { + throw new \Exception('Newsletter not found'); + } + return $newsletter + ->withSendingQueue() + ->withTotalSent() + ->withStatistics(); + } + + private function prepareContext(Newsletter $newsletter, NewsletterLink $link = null) { + $clicked = ($newsletter->statistics['clicked'] * 100) / $newsletter->total_sent; + $opened = ($newsletter->statistics['opened'] * 100) / $newsletter->total_sent; + $unsubscribed = ($newsletter->statistics['unsubscribed'] * 100) / $newsletter->total_sent; + $context = [ + 'subject' => $newsletter->subject, + 'preheader' => sprintf(_x( + '%1$s%% opens, %2$s%% clicks, %3$s%% unsubscribes in a nutshell.', 'newsletter open rate, click rate and unsubscribe rate', 'mailpoet'), + number_format($opened, 2), + number_format($clicked, 2), + number_format($unsubscribed, 2) + ), + 'topLinkClicks' => 0, + 'linkSettings' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-settings#basics'), + 'linkStats' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-newsletters#/stats/' . $newsletter->id()), + 'premiumPage' => get_site_url(null, '/wp-admin/admin.php?page=mailpoet-premium'), + 'premiumPluginActive' => is_plugin_active('mailpoet-premium/mailpoet-premium.php'), + 'clicked' => $clicked, + 'opened' => $opened, + ]; + if($link) { + $context['topLinkClicks'] = (int)$link->clicksCount; + $context['topLink'] = $link->url; + } + return $context; + } + + private function markTaskAsFinished(ScheduledTask $task) { + $task->status = ScheduledTask::STATUS_COMPLETED; + $task->processed_at = new Carbon; + $task->scheduled_at = null; + $task->save(); + } + +} diff --git a/lib/Cron/Workers/WorkersFactory.php b/lib/Cron/Workers/WorkersFactory.php new file mode 100644 index 0000000000..88107246a7 --- /dev/null +++ b/lib/Cron/Workers/WorkersFactory.php @@ -0,0 +1,74 @@ +sending_error_handler = $sending_error_handler; + $this->scheduler = $scheduler; + $this->mailer = $mailer; + $this->renderer = $renderer; + } + + /** @return SchedulerWorker */ + function createScheduleWorker($timer) { + return new SchedulerWorker($timer); + } + + /** @return SendingQueueWorker */ + function createQueueWorker($timer) { + return new SendingQueueWorker($this->sending_error_handler, $this->scheduler, $timer); + } + + function createStatsNotificationsWorker($timer) { + return new StatsNotificationsWorker($this->mailer, $this->renderer, $timer); + } + + /** @return SendingServiceKeyCheckWorker */ + function createSendingServiceKeyCheckWorker($timer) { + return new SendingServiceKeyCheckWorker($timer); + } + + /** @return PremiumKeyCheckWorker */ + function createPremiumKeyCheckWorker($timer) { + return new PremiumKeyCheckWorker($timer); + } + + /** @return BounceWorker */ + function createBounceWorker($timer) { + return new BounceWorker($timer); + } + + /** @return MigrationWorker */ + function createMigrationWorker($timer) { + return new MigrationWorker($timer); + } + +} diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index f865a2a4c8..681afb4b66 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -51,9 +51,13 @@ class ContainerConfigurator implements IContainerConfigurator { // Config $container->autowire(\MailPoet\Config\AccessControl::class)->setPublic(true); $container->autowire(\MailPoet\Config\Hooks::class)->setPublic(true); + $container->register(\MailPoet\Config\Renderer::class)->setFactory([__CLASS__, 'createRenderer']); // Cron $container->autowire(\MailPoet\Cron\Daemon::class)->setPublic(true); $container->autowire(\MailPoet\Cron\DaemonHttpRunner::class)->setPublic(true); + $container->autowire(\MailPoet\Cron\Workers\WorkersFactory::class)->setPublic(true); + $container->autowire(\MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler::class)->setPublic(true); + $container->autowire(\MailPoet\Cron\Workers\StatsNotifications\Scheduler::class); // Listing $container->autowire(\MailPoet\Listing\BulkActionController::class)->setPublic(true); $container->autowire(\MailPoet\Listing\Handler::class)->setPublic(true); @@ -62,6 +66,8 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\Router\Endpoints\Subscription::class)->setPublic(true); $container->autowire(\MailPoet\Router\Endpoints\Track::class)->setPublic(true); $container->autowire(\MailPoet\Router\Endpoints\ViewInBrowser::class)->setPublic(true); + // Mailer + $container->autowire(\MailPoet\Mailer\Mailer::class); // Subscribers $container->autowire(\MailPoet\Subscribers\NewSubscriberNotificationMailer::class)->setPublic(true); $container->autowire(\MailPoet\Subscribers\ConfirmationEmailMailer::class)->setPublic(true); @@ -94,4 +100,10 @@ class ContainerConfigurator implements IContainerConfigurator { } return $container->get(IContainerConfigurator::PREMIUM_CONTAINER_SERVICE_SLUG)->get($id); } + + static function createRenderer() { + $caching = !WP_DEBUG; + $debugging = WP_DEBUG; + return new \MailPoet\Config\Renderer($caching, $debugging); + } } diff --git a/lib/Models/NewsletterLink.php b/lib/Models/NewsletterLink.php index 661858455a..b29f2f2586 100644 --- a/lib/Models/NewsletterLink.php +++ b/lib/Models/NewsletterLink.php @@ -5,4 +5,23 @@ if(!defined('ABSPATH')) exit; class NewsletterLink extends Model { public static $_table = MP_NEWSLETTER_LINKS_TABLE; + + static function findTopLinkForNewsletter(Newsletter $newsletter) { + $link = self::selectExpr('links.*') + ->selectExpr('count(*)', 'clicksCount') + ->tableAlias('links') + ->innerJoin(StatisticsClicks::$_table, + array('clicks.link_id', '=', 'links.id'), + 'clicks') + ->where('newsletter_id', $newsletter->id()) + ->groupBy('links.id') + ->orderByDesc('clicksCount') + ->limit(1) + ->findOne(); + if(!$link) { + return null; + } + return $link; + } + } diff --git a/lib/Models/ScheduledTask.php b/lib/Models/ScheduledTask.php index 9cdb71e0c6..a37c8c5440 100644 --- a/lib/Models/ScheduledTask.php +++ b/lib/Models/ScheduledTask.php @@ -37,6 +37,15 @@ class ScheduledTask extends Model { ); } + /** @return StatsNotification */ + function statsNotification() { + return $this->hasOne( + StatsNotification::class, + 'task_id', + 'id' + ); + } + function pause() { $this->set('status', self::STATUS_PAUSED); $this->save(); diff --git a/lib/Models/StatsNotification.php b/lib/Models/StatsNotification.php new file mode 100644 index 0000000000..b02cb2720a --- /dev/null +++ b/lib/Models/StatsNotification.php @@ -0,0 +1,42 @@ +hasOne( + Newsletter::class, + 'id', + 'newsletter_id' + ); + } + + /** @return StatsNotification */ + static function createOrUpdate($data = array()) { + $model = null; + + if(isset($data['id']) && (int)$data['id'] > 0) { + $model = static::findOne((int)$data['id']); + } + + if(!$model && isset($data['task_id']) && $data['newsletter_id']) { + $model = self::where('newsletter_id', $data['newsletter_id']) + ->where('task_id', $data['task_id']) + ->findOne(); + } + + if(!$model) { + $model = static::create(); + $model->hydrate($data); + } else { + unset($data['id']); + $model->set($data); + } + + return $model->save(); + } + +} diff --git a/lib/Subscribers/NewSubscriberNotificationMailer.php b/lib/Subscribers/NewSubscriberNotificationMailer.php index 71034eb9d0..f96870c5da 100644 --- a/lib/Subscribers/NewSubscriberNotificationMailer.php +++ b/lib/Subscribers/NewSubscriberNotificationMailer.php @@ -6,6 +6,7 @@ use MailPoet\Config\Renderer; use MailPoet\Models\Segment; use MailPoet\Models\Setting; use MailPoet\Models\Subscriber; +use MailPoet\WP\Functions; class NewSubscriberNotificationMailer { @@ -18,11 +19,15 @@ class NewSubscriberNotificationMailer { /** @var \MailPoet\Mailer\Mailer */ private $mailer; + /** @var Functions */ + private $wordpress_functions; + /** * @param \MailPoet\Mailer\Mailer|null $mailer * @param Renderer|null $renderer + * @param Functions|null $wordpress_functions */ - function __construct($mailer = null, $renderer = null) { + function __construct($mailer = null, $renderer = null, $wordpress_functions = null) { if($renderer) { $this->renderer = $renderer; } else { @@ -30,6 +35,11 @@ class NewSubscriberNotificationMailer { $debugging = WP_DEBUG; $this->renderer = new Renderer($caching, $debugging); } + if($wordpress_functions) { + $this->wordpress_functions = $wordpress_functions; + } else { + $this->wordpress_functions = new Functions(); + } if($mailer) { $this->mailer = $mailer; } else { @@ -75,7 +85,7 @@ class NewSubscriberNotificationMailer { } private function constructSenderEmail() { - $url_parts = parse_url(home_url()); + $url_parts = parse_url($this->wordpress_functions->homeUrl()); $site_name = strtolower($url_parts['host']); if(substr($site_name, 0, 4) === 'www.') { $site_name = substr($site_name, 4); diff --git a/lib/WP/Functions.php b/lib/WP/Functions.php index a98c359f44..9a39ff27e1 100644 --- a/lib/WP/Functions.php +++ b/lib/WP/Functions.php @@ -28,6 +28,10 @@ class Functions { return call_user_func_array('current_time', func_get_args()); } + function homeUrl() { + return call_user_func_array('home_url', func_get_args()); + } + function getImageInfo($id) { /* * In some cases wp_get_attachment_image_src ignore the second parameter diff --git a/tests/integration/Cron/CronHelperTest.php b/tests/integration/Cron/CronHelperTest.php index e3283b16ea..5b834b68de 100644 --- a/tests/integration/Cron/CronHelperTest.php +++ b/tests/integration/Cron/CronHelperTest.php @@ -17,6 +17,10 @@ class CronHelperTest extends \MailPoetTest { Setting::setValue('cron_trigger', array( 'method' => 'none' )); + Setting::setValue('sender', array( + 'name' => 'John Doe', + 'address' => 'john.doe@example.com' + )); } function testItDefinesConstants() { diff --git a/tests/integration/Cron/DaemonHttpRunnerTest.php b/tests/integration/Cron/DaemonHttpRunnerTest.php index 3e37415e02..5a80a7546c 100644 --- a/tests/integration/Cron/DaemonHttpRunnerTest.php +++ b/tests/integration/Cron/DaemonHttpRunnerTest.php @@ -6,6 +6,8 @@ use Codeception\Stub\Expected; use MailPoet\Cron\CronHelper; use MailPoet\Cron\Daemon; use MailPoet\Cron\DaemonHttpRunner; +use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler; +use MailPoet\Cron\Workers\WorkersFactory; use MailPoet\Models\Setting; class DaemonHttpRunnerTest extends \MailPoetTest { @@ -20,27 +22,25 @@ class DaemonHttpRunnerTest extends \MailPoetTest { } function testItDoesNotRunWithoutRequestData() { - $daemon = Stub::construct( - new DaemonHttpRunner(new Daemon()), - array(), - array( + $daemon = Stub::make( + DaemonHttpRunner::class, + [ 'abortWithError' => function($message) { return $message; } - ) + ] ); expect($daemon->run(false))->equals('Invalid or missing request data.'); } function testItDoesNotRunWhenThereIsInvalidOrMissingToken() { - $daemon = Stub::construct( - new DaemonHttpRunner(new Daemon()), - array(), - array( + $daemon = Stub::make( + DaemonHttpRunner::class, + [ 'abortWithError' => function($message) { return $message; } - ) + ] ); $daemon->settings_daemon_data = array( 'token' => 123 @@ -52,14 +52,19 @@ class DaemonHttpRunnerTest extends \MailPoetTest { $data = array( 'token' => 123 ); - $daemon = Stub::make(new Daemon(), array( - 'executeScheduleWorker' => function() { - throw new \Exception('Message'); - }, - 'executeQueueWorker' => function() { - throw new \Exception(); - }, - ), $this); + $daemon = Stub::make( + Daemon::class, + [ + 'executeScheduleWorker' => function() { + throw new \Exception('Message'); + }, + 'executeQueueWorker' => function() { + throw new \Exception(); + }, + 'executeMigrationWorker' => null, + 'executeStatsNotificationsWorker' => null, + ] + ); $daemon_http_runner = Stub::make(new DaemonHttpRunner($daemon), array( 'pauseExecution' => null, 'callSelf' => null @@ -72,16 +77,14 @@ class DaemonHttpRunnerTest extends \MailPoetTest { } function testItCanPauseExecution() { - $daemon = Stub::make(new Daemon(), array( - 'executeScheduleWorker' => null, - 'executeQueueWorker' => null, - ), $this); - $daemon_http_runner = Stub::make(new DaemonHttpRunner($daemon), array( + $daemon = Stub::makeEmpty(Daemon::class); + $daemon_http_runner = Stub::make(DaemonHttpRunner::class, array( 'pauseExecution' => Expected::exactly(1, function($pause_delay) { expect($pause_delay)->lessThan(CronHelper::DAEMON_EXECUTION_LIMIT); expect($pause_delay)->greaterThan(CronHelper::DAEMON_EXECUTION_LIMIT - 1); }), - 'callSelf' => null + 'callSelf' => null, + 'terminateRequest' => null, ), $this); $data = array( 'token' => 123 @@ -93,7 +96,7 @@ class DaemonHttpRunnerTest extends \MailPoetTest { function testItTerminatesExecutionWhenDaemonIsDeleted() { - $daemon = Stub::make(new DaemonHttpRunner(new Daemon()), array( + $daemon = Stub::make(DaemonHttpRunner::class, array( 'executeScheduleWorker' => function() { Setting::deleteValue(CronHelper::DAEMON_SETTING); }, @@ -105,12 +108,12 @@ class DaemonHttpRunnerTest extends \MailPoetTest { 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct(new Daemon()); + $daemon->__construct(Stub::makeEmpty(Daemon::class)); $daemon->run($data); } function testItTerminatesExecutionWhenDaemonTokenChangesAndKeepsChangedToken() { - $daemon = Stub::make(new DaemonHttpRunner(new Daemon()), array( + $daemon = Stub::make(DaemonHttpRunner::class, array( 'executeScheduleWorker' => function() { Setting::setValue( CronHelper::DAEMON_SETTING, @@ -125,14 +128,14 @@ class DaemonHttpRunnerTest extends \MailPoetTest { 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct(new Daemon()); + $daemon->__construct(Stub::makeEmpty(Daemon::class)); $daemon->run($data); $data_after_run = Setting::getValue(CronHelper::DAEMON_SETTING); expect($data_after_run['token'], 567); } function testItTerminatesExecutionWhenDaemonIsDeactivated() { - $daemon = Stub::make(new DaemonHttpRunner(new Daemon()), [ + $daemon = Stub::make(DaemonHttpRunner::class, [ 'executeScheduleWorker' => null, 'executeQueueWorker' => null, 'pauseExecution' => null, @@ -143,34 +146,40 @@ class DaemonHttpRunnerTest extends \MailPoetTest { 'status' => CronHelper::DAEMON_STATUS_INACTIVE, ]; Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct(new Daemon()); + $daemon->__construct(Stub::makeEmpty(Daemon::class)); $daemon->run($data); } function testItUpdatesDaemonTokenDuringExecution() { - $daemon_http_runner = Stub::make(new DaemonHttpRunner(new Daemon()), array( + $daemon_http_runner = Stub::make(DaemonHttpRunner::class, array( 'executeScheduleWorker' => null, 'executeQueueWorker' => null, 'pauseExecution' => null, - 'callSelf' => null + 'callSelf' => null, + 'terminateRequest' => null, ), $this); $data = array( 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon_http_runner->__construct(new Daemon()); + $daemon_http_runner->__construct(Stub::makeEmptyExcept(Daemon::class, 'run')); $daemon_http_runner->run($data); $updated_daemon = Setting::getValue(CronHelper::DAEMON_SETTING); expect($updated_daemon['token'])->equals($daemon_http_runner->token); } function testItUpdatesTimestampsDuringExecution() { - $daemon = Stub::make(new Daemon(), array( - 'executeScheduleWorker' => function() { - sleep(2); - }, - 'executeQueueWorker' => null, - ), $this); + $daemon = Stub::make(Daemon::class, [ + 'executeScheduleWorker' => function() { + sleep(2); + }, + 'executeQueueWorker' => function() { + throw new \Exception(); + }, + 'executeMigrationWorker' => null, + 'executeStatsNotificationsWorker' => null, + ] + ); $daemon_http_runner = Stub::make(new DaemonHttpRunner($daemon), array( 'pauseExecution' => null, 'callSelf' => null @@ -192,25 +201,26 @@ class DaemonHttpRunnerTest extends \MailPoetTest { function testItCanRun() { ignore_user_abort(0); expect(ignore_user_abort())->equals(0); - $daemon = Stub::make(new DaemonHttpRunner(new Daemon()), array( + $daemon = Stub::make(DaemonHttpRunner::class, array( 'pauseExecution' => null, // workers should be executed 'executeScheduleWorker' => Expected::exactly(1), 'executeQueueWorker' => Expected::exactly(1), // daemon should call itself 'callSelf' => Expected::exactly(1), + 'terminateRequest' => null, ), $this); $data = array( 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct(new Daemon()); + $daemon->__construct(Stub::makeEmptyExcept(Daemon::class, 'run')); $daemon->run($data); expect(ignore_user_abort())->equals(1); } function testItRespondsToPingRequest() { - $daemon = Stub::make(new DaemonHttpRunner(new Daemon()), array( + $daemon = Stub::make(DaemonHttpRunner::class, array( 'terminateRequest' => Expected::exactly(1, function($message) { expect($message)->equals('pong'); }) diff --git a/tests/integration/Cron/DaemonTest.php b/tests/integration/Cron/DaemonTest.php index 1053478f03..0e0cee380c 100644 --- a/tests/integration/Cron/DaemonTest.php +++ b/tests/integration/Cron/DaemonTest.php @@ -6,39 +6,44 @@ use Codeception\Stub\Expected; use MailPoet\Cron\CronHelper; use MailPoet\Cron\DaemonHttpRunner; use MailPoet\Cron\Daemon; +use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler; +use MailPoet\Cron\Workers\WorkersFactory; use MailPoet\Models\Setting; class DaemonTest extends \MailPoetTest { function testItCanExecuteWorkers() { - $daemon = Stub::make(new Daemon(), array( + $daemon = Stub::make(Daemon::class, array( 'executeScheduleWorker' => Expected::exactly(1), 'executeQueueWorker' => Expected::exactly(1), - 'pauseExecution' => null, - 'callSelf' => null + 'executeMigrationWorker' => null, + 'executeStatsNotificationsWorker' => null, + 'executeSendingServiceKeyCheckWorker' => null, + 'executePremiumKeyCheckWorker' => null, + 'executeBounceWorker' => null, ), $this); $data = array( 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct($data); $daemon->run([]); } function testItCanRun() { - $daemon = Stub::make(new Daemon(), array( - 'pauseExecution' => null, + $daemon = Stub::make(Daemon::class, array( // workers should be executed 'executeScheduleWorker' => Expected::exactly(1), 'executeQueueWorker' => Expected::exactly(1), - // daemon should call itself - 'callSelf' => Expected::exactly(1), + 'executeMigrationWorker' => Expected::exactly(1), + 'executeStatsNotificationsWorker' => Expected::exactly(1), + 'executeSendingServiceKeyCheckWorker' => Expected::exactly(1), + 'executePremiumKeyCheckWorker' => Expected::exactly(1), + 'executeBounceWorker' => Expected::exactly(1) ), $this); $data = array( 'token' => 123 ); Setting::setValue(CronHelper::DAEMON_SETTING, $data); - $daemon->__construct(); $daemon->run($data); } diff --git a/tests/integration/Cron/Workers/SendingQueue/SendingQueueTest.php b/tests/integration/Cron/Workers/SendingQueue/SendingQueueTest.php index 80cc27d515..3756782b91 100644 --- a/tests/integration/Cron/Workers/SendingQueue/SendingQueueTest.php +++ b/tests/integration/Cron/Workers/SendingQueue/SendingQueueTest.php @@ -12,6 +12,7 @@ use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler; use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker; use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask; use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask; +use MailPoet\Cron\Workers\StatsNotifications\Scheduler as StatsNotificationsScheduler; use MailPoet\Mailer\MailerLog; use MailPoet\Models\Newsletter; use MailPoet\Models\NewsletterLink; @@ -36,6 +37,9 @@ class SendingQueueTest extends \MailPoetTest { /** @var SendingErrorHandler */ private $sending_error_handler; + /** @var Scheduler */ + private $stats_notifications_worker; + function _before() { $wp_users = get_users(); wp_set_current_user($wp_users[0]->ID); @@ -76,7 +80,8 @@ class SendingQueueTest extends \MailPoetTest { $this->newsletter_link->hash = 'abcde'; $this->newsletter_link->save(); $this->sending_error_handler = new SendingErrorHandler(); - $this->sending_queue_worker = new SendingQueueWorker($this->sending_error_handler); + $this->stats_notifications_worker = new StatsNotificationsScheduler(); + $this->sending_queue_worker = new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker); } private function getDirectUnsubscribeURL() { @@ -106,20 +111,20 @@ class SendingQueueTest extends \MailPoetTest { // constructor accepts timer argument $timer = microtime(true) - 5; - $sending_queue_worker = new SendingQueueWorker($this->sending_error_handler, $timer); + $sending_queue_worker = new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker, $timer); expect($sending_queue_worker->timer)->equals($timer); } function testItEnforcesExecutionLimitsBeforeQueueProcessing() { $sending_queue_worker = Stub::make( - new SendingQueueWorker($this->sending_error_handler), + new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker), array( 'processQueue' => Expected::never(), 'enforceSendingAndExecutionLimits' => Expected::exactly(1, function() { throw new \Exception(); }) ), $this); - $sending_queue_worker->__construct($this->sending_error_handler); + $sending_queue_worker->__construct($this->sending_error_handler, $this->stats_notifications_worker); try { $sending_queue_worker->process(); self::fail('Execution limits function was not called.'); @@ -130,12 +135,13 @@ class SendingQueueTest extends \MailPoetTest { function testItEnforcesExecutionLimitsAfterSendingWhenQueueStatusIsNotSetToComplete() { $sending_queue_worker = Stub::make( - new SendingQueueWorker($this->sending_error_handler), + new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker), array( 'enforceSendingAndExecutionLimits' => Expected::exactly(1) ), $this); $sending_queue_worker->__construct( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -164,12 +170,13 @@ class SendingQueueTest extends \MailPoetTest { $queue = $this->queue; $queue->status = SendingQueue::STATUS_COMPLETED; $sending_queue_worker = Stub::make( - new SendingQueueWorker($this->sending_error_handler), + new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker), array( 'enforceSendingAndExecutionLimits' => Expected::never() ), $this); $sending_queue_worker->__construct( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -193,7 +200,7 @@ class SendingQueueTest extends \MailPoetTest { function testItEnforcesExecutionLimitsAfterQueueProcessing() { $sending_queue_worker = Stub::make( - new SendingQueueWorker($this->sending_error_handler), + new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker), array( 'processQueue' => function() { // this function returns a queue object @@ -201,7 +208,7 @@ class SendingQueueTest extends \MailPoetTest { }, 'enforceSendingAndExecutionLimits' => Expected::exactly(2) ), $this); - $sending_queue_worker->__construct($this->sending_error_handler); + $sending_queue_worker->__construct($this->sending_error_handler, $this->stats_notifications_worker); $sending_queue_worker->process(); } @@ -225,6 +232,7 @@ class SendingQueueTest extends \MailPoetTest { $directUnsubscribeURL = $this->getDirectUnsubscribeURL(); $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -246,6 +254,7 @@ class SendingQueueTest extends \MailPoetTest { $trackedUnsubscribeURL = $this->getTrackedUnsubscribeURL(); $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -265,6 +274,7 @@ class SendingQueueTest extends \MailPoetTest { function testItCanProcessSubscribersOneByOne() { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -309,6 +319,7 @@ class SendingQueueTest extends \MailPoetTest { function testItCanProcessSubscribersInBulk() { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -356,6 +367,7 @@ class SendingQueueTest extends \MailPoetTest { function testItProcessesStandardNewsletters() { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -410,6 +422,7 @@ class SendingQueueTest extends \MailPoetTest { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::makeEmpty(new MailerTask(), array(), $this) ); @@ -425,6 +438,7 @@ class SendingQueueTest extends \MailPoetTest { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -591,9 +605,10 @@ class SendingQueueTest extends \MailPoetTest { 'updateProcessedSubscribers' => false )); $sending_task->id = 100; - $sending_queue_worker = Stub::make(new SendingQueueWorker($this->sending_error_handler)); + $sending_queue_worker = Stub::make(new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker)); $sending_queue_worker->__construct( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -627,6 +642,7 @@ class SendingQueueTest extends \MailPoetTest { function testItDoesNotUpdateNewsletterHashDuringSending() { $sending_queue_worker = new SendingQueueWorker( $this->sending_error_handler, + $this->stats_notifications_worker, $timer = false, Stub::make( new MailerTask(), @@ -650,7 +666,7 @@ class SendingQueueTest extends \MailPoetTest { return $custom_batch_size_value; }; Hooks::addFilter('mailpoet_cron_worker_sending_queue_batch_size', $filter); - $sending_queue_worker = new SendingQueueWorker($this->sending_error_handler); + $sending_queue_worker = new SendingQueueWorker($this->sending_error_handler, $this->stats_notifications_worker); expect($sending_queue_worker->batch_size)->equals($custom_batch_size_value); Hooks::removeFilter('mailpoet_cron_worker_sending_queue_batch_size', $filter); } diff --git a/tests/integration/Cron/Workers/StatsNotifications/SchedulerTest.php b/tests/integration/Cron/Workers/StatsNotifications/SchedulerTest.php new file mode 100644 index 0000000000..a4fc1af259 --- /dev/null +++ b/tests/integration/Cron/Workers/StatsNotifications/SchedulerTest.php @@ -0,0 +1,116 @@ +stats_notifications = new Scheduler(); + Setting::setValue(Worker::SETTINGS_KEY, [ + 'enabled' => true, + 'address' => 'email@example.com' + ]); + Setting::setValue('tracking.enabled', true); + } + + function testShouldSchedule() { + $newsletter_id = 5; + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isInstanceOf(StatsNotification::class); + $task = ScheduledTask::where('id', $notification->task_id)->findOne(); + expect($task)->isInstanceOf(ScheduledTask::class); + } + + function testShouldNotScheduleIfTrackingIsDisabled() { + Setting::setValue('tracking.enabled', false); + $newsletter_id = 13; + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + + function testShouldNotScheduleIfDisabled() { + $newsletter_id = 6; + Setting::setValue(Worker::SETTINGS_KEY, [ + 'enabled' => false, + 'address' => 'email@example.com' + ]); + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + + function testShouldNotScheduleIfSettingsMissing() { + $newsletter_id = 7; + Setting::setValue(Worker::SETTINGS_KEY, []); + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + + function testShouldNotScheduleIfEmailIsMissing() { + $newsletter_id = 8; + Setting::setValue(Worker::SETTINGS_KEY, [ + 'enabled' => true, + ]); + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + + function testShouldNotScheduleIfEmailIsEmpty() { + $newsletter_id = 9; + Setting::setValue(Worker::SETTINGS_KEY, [ + 'enabled' => true, + 'address' => ' ' + ]); + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + + function testShouldNotScheduleIfAlreadyScheduled() { + $newsletter_id = 10; + $existing_task = ScheduledTask::createOrUpdate([ + 'type' => Worker::TASK_TYPE, + 'status' => ScheduledTask::STATUS_SCHEDULED, + 'scheduled_at' => '2017-01-02 12:13:14', + ]); + $existing_notification = StatsNotification::createOrUpdate([ + 'newsletter_id' => $newsletter_id, + 'task_id' => $existing_task->id, + ]); + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, 'type' => Newsletter::TYPE_STANDARD]); + $this->stats_notifications->schedule($newsletter); + $notifications = StatsNotification::where('newsletter_id', $newsletter_id)->findMany(); + expect($notifications)->count(1); + $tasks = ScheduledTask::where('id', $notifications[0]->task_id)->findMany(); + expect($tasks)->count(1); + expect($existing_notification->id)->equals($notifications[0]->id); + expect($existing_task->id)->equals($tasks[0]->id); + } + + function testShouldNotScheduleIfInvalidType() { + $newsletter_id = 11; + $newsletter = Newsletter::createOrUpdate(['id' => $newsletter_id, Newsletter::TYPE_WELCOME]); + $this->stats_notifications->schedule($newsletter); + $notification = StatsNotification::where('newsletter_id', $newsletter_id)->findOne(); + expect($notification)->isEmpty(); + } + +} diff --git a/tests/integration/Cron/Workers/StatsNotifications/WorkerTest.php b/tests/integration/Cron/Workers/StatsNotifications/WorkerTest.php new file mode 100644 index 0000000000..7adfb0f3ff --- /dev/null +++ b/tests/integration/Cron/Workers/StatsNotifications/WorkerTest.php @@ -0,0 +1,224 @@ +mailer = $this->createMock(Mailer::class); + $this->renderer = $this->createMock(Renderer::class); + $this->stats_notifications = new Worker($this->mailer, $this->renderer); + Setting::setValue(Worker::SETTINGS_KEY, [ + 'enabled' => true, + 'address' => 'email@example.com' + ]); + $newsletter = Newsletter::createOrUpdate([ + 'subject' => 'Email Subject1', + 'type' => Newsletter::TYPE_STANDARD, + ]); + $sending_task = ScheduledTask::createOrUpdate([ + 'type' => 'sending', + 'status' => ScheduledTask::STATUS_COMPLETED, + ]); + $stats_notifications_task = ScheduledTask::createOrUpdate([ + 'type' => Worker::TASK_TYPE, + 'status' => ScheduledTask::STATUS_SCHEDULED, + 'scheduled_at' => '2017-01-02 12:13:14', + 'processed_at' => null, + ]); + StatsNotification::createOrUpdate([ + 'newsletter_id' => $newsletter->id(), + 'task_id' => $stats_notifications_task->id(), + ]); + $queue = SendingQueue::createOrUpdate([ + 'newsletter_rendered_subject' => 'Email Subject', + 'task_id' => $sending_task->id(), + 'newsletter_id' => $newsletter->id(), + 'count_processed' => 5, + ]); + $link = NewsletterLink::createOrUpdate([ + 'url' => 'Link url', + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'hash' => 'xyz', + ]); + StatisticsClicks::createOrUpdate([ + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'subscriber_id' => '5', + 'link_id' => $link->id(), + 'count' => 5, + 'created_at' => '2018-01-02 15:16:17', + ]); + $link2 = NewsletterLink::createOrUpdate([ + 'url' => 'Link url2', + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'hash' => 'xyzd', + ]); + StatisticsClicks::createOrUpdate([ + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'subscriber_id' => '6', + 'link_id' => $link2->id(), + 'count' => 5, + 'created_at' => '2018-01-02 15:16:17', + ]); + StatisticsClicks::createOrUpdate([ + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'subscriber_id' => '7', + 'link_id' => $link2->id(), + 'count' => 5, + 'created_at' => '2018-01-02 15:16:17', + ]); + StatisticsOpens::createOrUpdate([ + 'subscriber_id' => '10', + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'created_at' => '2017-01-02 12:23:45', + ]); + StatisticsOpens::createOrUpdate([ + 'subscriber_id' => '11', + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'created_at' => '2017-01-02 21:23:45', + ]); + StatisticsUnsubscribes::createOrUpdate([ + 'subscriber_id' => '12', + 'newsletter_id' => $newsletter->id(), + 'queue_id' => $queue->id(), + 'created_at' => '2017-01-02 21:23:45', + ]); + } + + function testRendersTemplate() { + $this->renderer->expects($this->exactly(2)) + ->method('render'); + $this->renderer->expects($this->at(0)) + ->method('render') + ->with($this->equalTo('emails/statsNotification.html')); + + $this->renderer->expects($this->at(1)) + ->method('render') + ->with($this->equalTo('emails/statsNotification.txt')); + + $this->stats_notifications->process(); + } + + function testAddsSubjectToContext() { + $this->renderer->expects($this->exactly(2)) // html + text template + ->method('render') + ->with( + $this->anything(), + $this->callback(function($context){ + return $context['subject'] === 'Email Subject1'; + })); + + $this->stats_notifications->process(); + } + + function testAddsPreHeaderToContext() { + $this->renderer->expects($this->exactly(2)) // html + text template + ->method('render') + ->with( + $this->anything(), + $this->callback(function($context){ + return $context['preheader'] === '40.00% opens, 60.00% clicks, 20.00% unsubscribes in a nutshell.'; + })); + + $this->stats_notifications->process(); + } + + function testAddsWPUrlsToContext() { + $this->renderer->expects($this->exactly(2)) // html + text template + ->method('render') + ->with( + $this->anything(), + $this->callback(function($context){ + return strpos($context['linkSettings'], 'mailpoet-settings') + && strpos($context['linkStats'], 'mailpoet-newsletters#/stats'); + })); + + $this->stats_notifications->process(); + } + + function testAddsLinksToContext() { + $this->renderer->expects($this->exactly(2)) // html + text template + ->method('render') + ->with( + $this->anything(), + $this->callback(function($context){ + return ($context['topLink'] === 'Link url2') + && ($context['topLinkClicks'] === 2); + })); + + $this->stats_notifications->process(); + } + + function testSends() { + $this->mailer->expects($this->once()) + ->method('send'); + + $this->stats_notifications->process(); + } + + function testItWorksForNewsletterWithNoStats() { + $newsletter = Newsletter::createOrUpdate([ + 'subject' => 'Email Subject2', + 'type' => Newsletter::TYPE_STANDARD, + ]); + $sending_task = ScheduledTask::createOrUpdate([ + 'type' => 'sending', + 'status' => ScheduledTask::STATUS_COMPLETED, + ]); + $stats_notifications_task = ScheduledTask::createOrUpdate([ + 'type' => Worker::TASK_TYPE, + 'status' => ScheduledTask::STATUS_SCHEDULED, + 'scheduled_at' => '2016-01-02 12:13:14', + 'processed_at' => null, + ]); + StatsNotification::createOrUpdate([ + 'newsletter_id' => $newsletter->id(), + 'task_id' => $stats_notifications_task->id(), + ]); + SendingQueue::createOrUpdate([ + 'newsletter_rendered_subject' => 'Email Subject2', + 'task_id' => $sending_task->id(), + 'newsletter_id' => $newsletter->id(), + 'count_processed' => 15, + ]); + + $this->mailer->expects($this->exactly(2)) + ->method('send'); + + $this->stats_notifications->process(); + } + +} diff --git a/tests/integration/Subscribers/NewSubscriberNotificationMailerTest.php b/tests/integration/Subscribers/NewSubscriberNotificationMailerTest.php index d3da5fa3b3..9d2cb369b7 100644 --- a/tests/integration/Subscribers/NewSubscriberNotificationMailerTest.php +++ b/tests/integration/Subscribers/NewSubscriberNotificationMailerTest.php @@ -8,6 +8,7 @@ use MailPoet\Mailer\Mailer; use MailPoet\Models\Segment; use MailPoet\Models\Setting; use MailPoet\Models\Subscriber; +use MailPoet\WP\Functions; class NewSubscriberNotificationMailerTest extends \MailPoetTest { @@ -85,7 +86,6 @@ class NewSubscriberNotificationMailerTest extends \MailPoetTest { function testItRemovesWwwFromSenderAddress() { Setting::setValue(NewSubscriberNotificationMailer::SETTINGS_KEY, ['enabled' => true,'address' => 'a@b.c']); - update_option( 'home', 'http://www.example.com/xyz' ); $mailer = Stub::makeEmpty(Mailer::class, [ 'getSenderNameAndAddress' => @@ -96,7 +96,14 @@ class NewSubscriberNotificationMailerTest extends \MailPoetTest { }), ], $this); - $service = new NewSubscriberNotificationMailer($mailer); + $functions = Stub::makeEmpty(Functions::class, [ + 'homeUrl' => + Expected::once(function() { + return 'http://www.example.com/xyz'; + }), + ], $this); + + $service = new NewSubscriberNotificationMailer($mailer, null, $functions); $service->send($this->subscriber, $this->segments); } } diff --git a/views/emails/statsNotification.html b/views/emails/statsNotification.html new file mode 100644 index 0000000000..a39438c01f --- /dev/null +++ b/views/emails/statsNotification.html @@ -0,0 +1,456 @@ + +
+ + + + +
|
+
+ <%= __('Enter the email address that should receive your newsletter’s stats 24 hours after it has been sent.') %> +
+
+
+
+
+
+
+