From 45cfddf83f5f1a58055a5b024d9cad8424d26040 Mon Sep 17 00:00:00 2001 From: lucashedding <lucashedding@1463982.no-reply.drupal.org> Date: Mon, 9 Dec 2019 07:52:31 -0600 Subject: [PATCH] Issue #3093955 by heddn, ressa: Send email alert to administrator after automatic update --- automatic_updates.module | 13 +- automatic_updates.services.yml | 9 ++ src/Controller/InPlaceUpdateController.php | 4 +- src/Event/PostUpdateEvent.php | 62 +++++++ src/Event/UpdateEvents.php | 19 +++ src/EventSubscriber/PostUpdateSubscriber.php | 123 ++++++++++++++ src/Services/InPlaceUpdate.php | 64 ++++---- src/Services/Notify.php | 4 +- src/Services/UpdateInterface.php | 14 +- src/UpdateMetadata.php | 153 ++++++++++++++++++ .../automatic-updates-post-update.html.twig | 33 ++++ .../Controller/InPlaceUpdateController.php | 4 +- tests/src/Functional/NotifyTest.php | 29 +++- 13 files changed, 482 insertions(+), 49 deletions(-) create mode 100644 src/Event/PostUpdateEvent.php create mode 100644 src/Event/UpdateEvents.php create mode 100644 src/EventSubscriber/PostUpdateSubscriber.php create mode 100644 src/UpdateMetadata.php create mode 100644 templates/automatic-updates-post-update.html.twig diff --git a/automatic_updates.module b/automatic_updates.module index 8235dfabce..1d96f034ff 100644 --- a/automatic_updates.module +++ b/automatic_updates.module @@ -6,6 +6,7 @@ */ use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface; +use Drupal\automatic_updates\UpdateMetadata; use Drupal\Core\Url; use Drupal\update\UpdateManagerInterface; use Symfony\Component\Process\PhpExecutableFinder; @@ -122,15 +123,17 @@ function automatic_updates_cron() { if ($not_recommended_version && $projects['drupal']['existing_version'] !== $recommended_release['version']) { if ($config->get('enable_cron_security_updates')) { if ($security_update) { + $metadata = new UpdateMetadata('drupal', 'core', \Drupal::VERSION, $recommended_release['version']); /** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */ $updater = \Drupal::service('automatic_updates.update'); - $updater->update('drupal', 'core', \Drupal::VERSION, $recommended_release['version']); + $updater->update($metadata); } } else { + $metadata = new UpdateMetadata('drupal', 'core', \Drupal::VERSION, $recommended_release['version']); /** @var \Drupal\automatic_updates\Services\UpdateInterface $updater */ $updater = \Drupal::service('automatic_updates.update'); - $updater->update('drupal', 'core', \Drupal::VERSION, $recommended_release['version']); + $updater->update($metadata); } } } @@ -148,6 +151,12 @@ function automatic_updates_theme(array $existing, $type, $theme, $path) { 'messages' => [], ], ], + 'automatic_updates_post_update' => [ + 'variables' => [ + 'success' => NULL, + 'metadata' => NULL, + ], + ], ]; } diff --git a/automatic_updates.services.yml b/automatic_updates.services.yml index 53dab8ea0a..23621cf5cb 100644 --- a/automatic_updates.services.yml +++ b/automatic_updates.services.yml @@ -46,6 +46,15 @@ services: plugin.manager.database_update_handler: class: Drupal\automatic_updates\DatabaseUpdateHandlerPluginManager parent: default_plugin_manager + automatic_updates.post_update_subscriber: + class: Drupal\automatic_updates\EventSubscriber\PostUpdateSubscriber + arguments: + - '@config.factory' + - '@plugin.manager.mail' + - '@language_manager' + - '@entity_type.manager' + tags: + - { name: event_subscriber } automatic_updates.readiness_checker: class: Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManager diff --git a/src/Controller/InPlaceUpdateController.php b/src/Controller/InPlaceUpdateController.php index 151e525cd4..e4332576de 100644 --- a/src/Controller/InPlaceUpdateController.php +++ b/src/Controller/InPlaceUpdateController.php @@ -3,6 +3,7 @@ namespace Drupal\automatic_updates\Controller; use Drupal\automatic_updates\Services\UpdateInterface; +use Drupal\automatic_updates\UpdateMetadata; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Messenger\MessengerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -42,7 +43,8 @@ class InPlaceUpdateController extends ControllerBase { * Builds the response. */ public function update($project, $type, $from, $to) { - $updated = $this->updater->update($project, $type, $from, $to); + $metadata = new UpdateMetadata($project, $type, $from, $to); + $updated = $this->updater->update($metadata); $message_type = MessengerInterface::TYPE_STATUS; $message = $this->t('Update successful'); if (!$updated) { diff --git a/src/Event/PostUpdateEvent.php b/src/Event/PostUpdateEvent.php new file mode 100644 index 0000000000..789b01c449 --- /dev/null +++ b/src/Event/PostUpdateEvent.php @@ -0,0 +1,62 @@ +<?php + +namespace Drupal\automatic_updates\Event; + +use Drupal\automatic_updates\UpdateMetadata; +use Symfony\Component\EventDispatcher\Event; + +/** + * Defines the post update event. + * + * @see \Drupal\automatic_updates\Event\UpdateEvents + */ +class PostUpdateEvent extends Event { + + /** + * The update metadata. + * + * @var \Drupal\automatic_updates\UpdateMetadata + */ + protected $updateMetadata; + + /** + * The update success status. + * + * @var bool + */ + protected $success; + + /** + * Constructs a new PostUpdateEvent. + * + * @param \Drupal\automatic_updates\UpdateMetadata $metadata + * The update metadata. + * @param bool $success + * TRUE if update succeeded, FALSE otherwise. + */ + public function __construct(UpdateMetadata $metadata, $success) { + $this->updateMetadata = $metadata; + $this->success = $success; + } + + /** + * Get the update metadata. + * + * @return \Drupal\automatic_updates\UpdateMetadata + * The update metadata. + */ + public function getUpdateMetadata() { + return $this->updateMetadata; + } + + /** + * Gets the update success status. + * + * @return bool + * TRUE if update succeeded, FALSE otherwise. + */ + public function success() { + return $this->success; + } + +} diff --git a/src/Event/UpdateEvents.php b/src/Event/UpdateEvents.php new file mode 100644 index 0000000000..842b6d6ddf --- /dev/null +++ b/src/Event/UpdateEvents.php @@ -0,0 +1,19 @@ +<?php + +namespace Drupal\automatic_updates\Event; + +/** + * Defines events for the automatic_updates module. + */ +final class UpdateEvents { + + /** + * Name of the event fired after updating a site. + * + * @Event + * + * @see \Drupal\automatic_updates\Event\PostUpdateEvent + */ + const POST_UPDATE = 'automatic_updates.post_update'; + +} diff --git a/src/EventSubscriber/PostUpdateSubscriber.php b/src/EventSubscriber/PostUpdateSubscriber.php new file mode 100644 index 0000000000..f701f6ef5d --- /dev/null +++ b/src/EventSubscriber/PostUpdateSubscriber.php @@ -0,0 +1,123 @@ +<?php + +namespace Drupal\automatic_updates\EventSubscriber; + +use Drupal\automatic_updates\Event\PostUpdateEvent; +use Drupal\automatic_updates\Event\UpdateEvents; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\Core\Mail\MailManagerInterface; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Post update event subscriber. + */ +class PostUpdateSubscriber implements EventSubscriberInterface { + use StringTranslationTrait; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * Mail manager. + * + * @var \Drupal\Core\Mail\MailManagerInterface + */ + protected $mailManager; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * Entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * PostUpdateSubscriber constructor. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Mail\MailManagerInterface $mail_manager + * The mail manager. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * Entity type manager. + */ + public function __construct(ConfigFactoryInterface $config_factory, MailManagerInterface $mail_manager, LanguageManagerInterface $language_manager, EntityTypeManagerInterface $entity_type_manager) { + $this->configFactory = $config_factory; + $this->mailManager = $mail_manager; + $this->languageManager = $language_manager; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + UpdateEvents::POST_UPDATE => ['onPostUpdate'], + ]; + } + + /** + * Send notification on post update with success/failure. + * + * @param \Drupal\automatic_updates\Event\PostUpdateEvent $event + * The post update event. + */ + public function onPostUpdate(PostUpdateEvent $event) { + $notify_list = $this->configFactory->get('update.settings')->get('notification.emails'); + if (!empty($notify_list)) { + $params['subject'] = $this->t('Automatic update of "@project" succeeded', ['@project' => $event->getUpdateMetadata()->getProjectName()]); + if (!$event->success()) { + $params['subject'] = $this->t('Automatic update of "@project" failed', ['@project' => $event->getUpdateMetadata()->getProjectName()]); + } + $params['body'] = [ + '#theme' => 'automatic_updates_post_update', + '#success' => $event->success(), + '#metadata' => $event->getUpdateMetadata(), + ]; + $default_langcode = $this->languageManager->getDefaultLanguage()->getId(); + $params['langcode'] = $default_langcode; + foreach ($notify_list as $to) { + $this->doSend($to, $params); + } + } + } + + /** + * Composes and send the email message. + * + * @param string $to + * The email address where the message will be sent. + * @param array $params + * Parameters to build the email. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function doSend($to, array $params) { + $users = $this->entityTypeManager->getStorage('user') + ->loadByProperties(['mail' => $to]); + foreach ($users as $user) { + $to_user = reset($users); + $params['langcode'] = $to_user->getPreferredLangcode(); + $this->mailManager->mail('automatic_updates', 'post_update', $to, $params['langcode'], $params); + } + } + +} diff --git a/src/Services/InPlaceUpdate.php b/src/Services/InPlaceUpdate.php index c226d6fd60..21ab7a14a6 100644 --- a/src/Services/InPlaceUpdate.php +++ b/src/Services/InPlaceUpdate.php @@ -2,8 +2,11 @@ namespace Drupal\automatic_updates\Services; +use Drupal\automatic_updates\Event\PostUpdateEvent; +use Drupal\automatic_updates\Event\UpdateEvents; use Drupal\automatic_updates\ProjectInfoTrait; use Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface; +use Drupal\automatic_updates\UpdateMetadata; use Drupal\Component\FileSystem\FileSystem; use Drupal\Core\Archiver\ArchiverInterface; use Drupal\Core\Archiver\ArchiverManager; @@ -128,7 +131,7 @@ class InPlaceUpdate implements UpdateInterface { /** * {@inheritdoc} */ - public function update($project_name, $project_type, $from_version, $to_version) { + public function update(UpdateMetadata $metadata) { // Bail immediately on updates if error category checks fail. /** @var \Drupal\automatic_updates\ReadinessChecker\ReadinessCheckerManagerInterface $readiness_check_manager */ $checker = \Drupal::service('automatic_updates.readiness_checker'); @@ -136,14 +139,14 @@ class InPlaceUpdate implements UpdateInterface { return FALSE; } $success = FALSE; - if ($project_name === 'drupal') { + if ($metadata->getProjectName() === 'drupal') { $project_root = $this->rootPath; } else { - $project_root = drupal_get_path($project_type, $project_name); + $project_root = drupal_get_path($metadata->getProjectType(), $metadata->getProjectName()); } - if ($archive = $this->getArchive($project_name, $from_version, $to_version)) { - $modified = $this->checkModifiedFiles($project_name, $project_type, $archive); + if ($archive = $this->getArchive($metadata)) { + $modified = $this->checkModifiedFiles($metadata, $archive); if (!$modified && $this->backup($archive, $project_root)) { $this->logger->info('In place update has started.'); $success = $this->processUpdate($archive, $project_root); @@ -168,30 +171,33 @@ class InPlaceUpdate implements UpdateInterface { } } } + /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher */ + $event_dispatcher = \Drupal::service('event_dispatcher'); + $event = new PostUpdateEvent($metadata, $success); + $event_dispatcher->dispatch(UpdateEvents::POST_UPDATE, $event); + return $success; } /** * Get an archive with the quasi-patch contents. * - * @param string $project_name - * The project name. - * @param string $from_version - * The current project version. - * @param string $to_version - * The desired next project version. + * @param \Drupal\automatic_updates\UpdateMetadata $metadata + * The update metadata. * * @return \Drupal\Core\Archiver\ArchiverInterface|null * The archive or NULL if download fails. + * + * @throws \SodiumException */ - protected function getArchive($project_name, $from_version, $to_version) { - $quasi_patch = $this->getQuasiPatchFileName($project_name, $from_version, $to_version); - $url = $this->buildUrl($project_name, $quasi_patch); + protected function getArchive(UpdateMetadata $metadata) { + $quasi_patch = $this->getQuasiPatchFileName($metadata); + $url = $this->buildUrl($metadata->getProjectName(), $quasi_patch); $temp_directory = FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR; $destination = $this->fileSystem->getDestinationFilename($temp_directory . $quasi_patch, FileSystemInterface::EXISTS_REPLACE); $this->doGetResource($url, $destination); $csig_file = $quasi_patch . '.csig'; - $csig_url = $this->buildUrl($project_name, $csig_file); + $csig_url = $this->buildUrl($metadata->getProjectName(), $csig_file); $csig_destination = $this->fileSystem->getDestinationFilename(FileSystem::getOsTemporaryDirectory() . DIRECTORY_SEPARATOR . $csig_file, FileSystemInterface::EXISTS_REPLACE); $this->doGetResource($csig_url, $csig_destination); $csig = file_get_contents($csig_destination); @@ -202,25 +208,23 @@ class InPlaceUpdate implements UpdateInterface { /** * Check if files are modified before applying updates. * - * @param string $project_name - * The project name. - * @param string $project_type - * The project type. + * @param \Drupal\automatic_updates\UpdateMetadata $metadata + * The update metadata. * @param \Drupal\Core\Archiver\ArchiverInterface $archive * The archive. * * @return bool * Return TRUE if modified files exist, FALSE otherwise. */ - protected function checkModifiedFiles($project_name, $project_type, ArchiverInterface $archive) { - if ($project_type === 'core') { - $project_type = 'module'; + protected function checkModifiedFiles(UpdateMetadata $metadata, ArchiverInterface $archive) { + if ($metadata->getProjectType() === 'core') { + $metadata->setProjectType('module'); } - $extensions = $this->getInfos($project_type); + $extensions = $this->getInfos($metadata->getProjectType()); /** @var \Drupal\automatic_updates\Services\ModifiedFilesInterface $modified_files */ $modified_files = \Drupal::service('automatic_updates.modified_files'); try { - $files = iterator_to_array($modified_files->getModifiedFiles([$extensions[$project_name]])); + $files = iterator_to_array($modified_files->getModifiedFiles([$extensions[$metadata->getProjectName()]])); } catch (RequestException $exception) { // While not strictly true that there are modified files, we can't be sure @@ -525,18 +529,14 @@ class InPlaceUpdate implements UpdateInterface { /** * Get the quasi-patch file name. * - * @param string $project_name - * The project name. - * @param string $from_version - * The current project version. - * @param string $to_version - * The desired next project version. + * @param \Drupal\automatic_updates\UpdateMetadata $metadata + * The update metadata. * * @return string * The quasi-patch file name. */ - protected function getQuasiPatchFileName($project_name, $from_version, $to_version) { - return "$project_name-$from_version-to-$to_version.zip"; + protected function getQuasiPatchFileName(UpdateMetadata $metadata) { + return "{$metadata->getProjectName()}-{$metadata->getFromVersion()}-to-{$metadata->getToVersion()}.zip"; } /** diff --git a/src/Services/Notify.php b/src/Services/Notify.php index f5dcbe553d..87f312fbd7 100644 --- a/src/Services/Notify.php +++ b/src/Services/Notify.php @@ -157,11 +157,11 @@ class Notify implements NotifyInterface { protected function doSend($to, array $params) { $users = $this->entityTypeManager->getStorage('user') ->loadByProperties(['mail' => $to]); - if ($users) { + foreach ($users as $user) { $to_user = reset($users); $params['langcode'] = $to_user->getPreferredLangcode(); + $this->mailManager->mail('automatic_updates', 'notify', $to, $params['langcode'], $params); } - $this->mailManager->mail('automatic_updates', 'notify', $to, $params['langcode'], $params); } } diff --git a/src/Services/UpdateInterface.php b/src/Services/UpdateInterface.php index ee685a4578..71d26b9bba 100644 --- a/src/Services/UpdateInterface.php +++ b/src/Services/UpdateInterface.php @@ -2,6 +2,8 @@ namespace Drupal\automatic_updates\Services; +use Drupal\automatic_updates\UpdateMetadata; + /** * Interface UpdateInterface. */ @@ -10,18 +12,12 @@ interface UpdateInterface { /** * Update a project to the next release. * - * @param string $project_name - * The project name. - * @param string $project_type - * The project type. - * @param string $from_version - * The current project version. - * @param string $to_version - * The desired next project version. + * @param \Drupal\automatic_updates\UpdateMetadata $metadata + * The update metadata. * * @return bool * TRUE if project was successfully updated, FALSE otherwise. */ - public function update($project_name, $project_type, $from_version, $to_version); + public function update(UpdateMetadata $metadata); } diff --git a/src/UpdateMetadata.php b/src/UpdateMetadata.php new file mode 100644 index 0000000000..f54972cfe8 --- /dev/null +++ b/src/UpdateMetadata.php @@ -0,0 +1,153 @@ +<?php + +namespace Drupal\automatic_updates; + +/** + * Transfer object to encapsulate the details for an update. + */ +final class UpdateMetadata { + + /** + * The project name. + * + * @var string + */ + protected $projectName; + + /** + * The project type. + * + * @var string + */ + protected $projectType; + + /** + * The current project version. + * + * @var string + */ + protected $fromVersion; + + /** + * The desired next project version. + * + * @var string + */ + protected $toVersion; + + /** + * UpdateMetadata constructor. + * + * @param string $project_name + * The project name. + * @param string $project_type + * The project type. + * @param string $from_version + * The current project version. + * @param string $to_version + * The desired next project version. + */ + public function __construct($project_name, $project_type, $from_version, $to_version) { + $this->projectName = $project_name; + $this->projectType = $project_type; + $this->fromVersion = $from_version; + $this->toVersion = $to_version; + } + + /** + * Get project name. + * + * @return string + * The project nam. + */ + public function getProjectName() { + return $this->projectName; + } + + /** + * Set the project name. + * + * @param string $projectName + * The project name. + * + * @return \Drupal\automatic_updates\UpdateMetadata + * The update metadata. + */ + public function setProjectName($projectName) { + $this->projectName = $projectName; + return $this; + } + + /** + * Get the project type. + * + * @return string + * The project type. + */ + public function getProjectType() { + return $this->projectType; + } + + /** + * Set the project type. + * + * @param string $projectType + * The project type. + * + * @return \Drupal\automatic_updates\UpdateMetadata + * The update metadata. + */ + public function setProjectType($projectType) { + $this->projectType = $projectType; + return $this; + } + + /** + * Get the current project version. + * + * @return string + * The current project version. + */ + public function getFromVersion() { + return $this->fromVersion; + } + + /** + * Set the current project version. + * + * @param string $fromVersion + * The current project version. + * + * @return \Drupal\automatic_updates\UpdateMetadata + * The update metadata. + */ + public function setFromVersion($fromVersion) { + $this->fromVersion = $fromVersion; + return $this; + } + + /** + * Get the desired next project version. + * + * @return string + * The desired next project version. + */ + public function getToVersion() { + return $this->toVersion; + } + + /** + * Set the desired next project version. + * + * @param string $toVersion + * The desired next project version. + * + * @return \Drupal\automatic_updates\UpdateMetadata + * The update metadata. + */ + public function setToVersion($toVersion) { + $this->toVersion = $toVersion; + return $this; + } + +} diff --git a/templates/automatic-updates-post-update.html.twig b/templates/automatic-updates-post-update.html.twig new file mode 100644 index 0000000000..c1f1d9171c --- /dev/null +++ b/templates/automatic-updates-post-update.html.twig @@ -0,0 +1,33 @@ +{# +/** + * @file + * Template for the post update email notification. + * + * Available variables: + * - success: The update success status + * - metadata: The update metadata + * + * @ingroup themeable + */ +#} +<p> + {% if success %} + {{ 'The project "@project" was updated from "@from_version" to "@to_version" with success.'|t({ + '@project': metadata.getProjectName, + '@from_version': metadata.getFromVersion, + '@to_version': metadata.getToVersion, + }) }} + {% else %} + {{ 'The project "@project" was updated from "@from_version" to "@to_version" with failures.'|t({ + '@project': metadata.getProjectName, + '@from_version': metadata.getFromVersion, + '@to_version': metadata.getToVersion, + }) }} + {% endif %} +</p> +<p> + {% set status_report = path('system.status') %} + {% trans %} + See the <a href="{{ status_report }}">site status report page</a> and any logs for more information. + {% endtrans %} +</p> diff --git a/tests/modules/test_automatic_updates/src/Controller/InPlaceUpdateController.php b/tests/modules/test_automatic_updates/src/Controller/InPlaceUpdateController.php index 2d645eccf0..4bf564784a 100644 --- a/tests/modules/test_automatic_updates/src/Controller/InPlaceUpdateController.php +++ b/tests/modules/test_automatic_updates/src/Controller/InPlaceUpdateController.php @@ -3,6 +3,7 @@ namespace Drupal\test_automatic_updates\Controller; use Drupal\automatic_updates\Services\UpdateInterface; +use Drupal\automatic_updates\UpdateMetadata; use Drupal\Core\Controller\ControllerBase; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -41,7 +42,8 @@ class InPlaceUpdateController extends ControllerBase { * Builds the response. */ public function update($project, $type, $from, $to) { - $updated = $this->updater->update($project, $type, $from, $to); + $metadata = new UpdateMetadata($project, $type, $from, $to); + $updated = $this->updater->update($metadata); return [ '#markup' => $updated ? $this->t('Update successful') : $this->t('Update Failed'), ]; diff --git a/tests/src/Functional/NotifyTest.php b/tests/src/Functional/NotifyTest.php index a0bdbe0e5c..42492a7dfb 100644 --- a/tests/src/Functional/NotifyTest.php +++ b/tests/src/Functional/NotifyTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\automatic_updates\Functional; +use Drupal\automatic_updates\Event\PostUpdateEvent; +use Drupal\automatic_updates\UpdateMetadata; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Test\AssertMailTrait; use Drupal\Core\Url; @@ -60,9 +62,9 @@ class NotifyTest extends BrowserTestBase { } /** - * Tests sending email notifications. + * Tests sending PSA email notifications. */ - public function testSendMail() { + public function testPsaMail() { // Test PSAs on admin pages. $this->drupalGet(Url::fromRoute('system.admin')); $this->assertSession()->pageTextContains('Critical Release - SA-2019-02-19'); @@ -84,4 +86,27 @@ class NotifyTest extends BrowserTestBase { $this->assertCount(0, $this->getMails()); } + /** + * Tests sending post update email notifications. + */ + public function testPostUpdateMail() { + // Success email. + $metadata = new UpdateMetadata('drupal', 'core', '8.7.0', '8.8.0'); + $post_update = new PostUpdateEvent($metadata, TRUE); + $notify = $this->container->get('automatic_updates.post_update_subscriber'); + $notify->onPostUpdate($post_update); + $this->assertCount(1, $this->getMails()); + $this->assertMailString('subject', 'Automatic update of "drupal" succeeded', 1); + $this->assertMailString('body', 'The project "drupal" was updated from "8.7.0" to "8.8.0" with success.', 1); + + // Failure email. + $this->container->get('state')->set('system.test_mail_collector', []); + $post_update = new PostUpdateEvent($metadata, FALSE); + $notify = $this->container->get('automatic_updates.post_update_subscriber'); + $notify->onPostUpdate($post_update); + $this->assertCount(1, $this->getMails()); + $this->assertMailString('subject', 'Automatic update of "drupal" failed', 1); + $this->assertMailString('body', 'The project "drupal" was updated from "8.7.0" to "8.8.0" with failures.', 1); + } + } -- GitLab