diff --git a/automatic_updates.module b/automatic_updates.module index c602db75cf9a40a5d13e96fed820317ea4703ac2..2c5d74c39b419d9aded7c86b6608b736e5390e87 100644 --- a/automatic_updates.module +++ b/automatic_updates.module @@ -42,14 +42,29 @@ function automatic_updates_mail(string $key, array &$message, array $params): vo $options = [ 'langcode' => $message['langcode'], ]; + switch ($key) { + case 'cron_successful': + $message['subject'] = t("Drupal core was successfully updated", [], $options); + $message['body'][] = t('Congratulations!', [], $options); + $message['body'][] = t('Drupal core was automatically updated from @previous_version to @updated_version.', [ + '@previous_version' => $params['previous_version'], + '@updated_version' => $params['updated_version'], + ], $options); + break; - if ($key === 'cron_successful') { - $message['subject'] = t("Drupal core was successfully updated", [], $options); - $message['body'][] = t('Congratulations!', [], $options); - $message['body'][] = t('Drupal core was automatically updated from @previous_version to @updated_version.', [ - '@previous_version' => $params['previous_version'], - '@updated_version' => $params['updated_version'], - ], $options); + case 'cron_failed': + $message['subject'] = t("Drupal core update failed", [], $options); + $message['body'][] = t('Drupal core failed to update automatically from @previous_version to @target_version. The following error was logged:', [ + '@previous_version' => $params['previous_version'], + '@target_version' => $params['target_version'], + ], $options); + $message['body'][] = $params['error_message']; + break; + } + + // If this email was related to an unattended update, explicitly state that + // this isn't supported yet. + if (str_starts_with($key, 'cron_')) { $message['body'][] = t('This e-mail was sent by the Automatic Updates module. Unattended updates are not yet fully supported.', [], $options); $message['body'][] = t('If you are using this feature in production, it is strongly recommended for you to visit your site and ensure that everything still looks good.', [], $options); } diff --git a/src/CronUpdater.php b/src/CronUpdater.php index f79d7561b206fda98916ab5156daaa6323e3b786..dc8711275f7997b5302be4acec08ca5e3dfa287a 100644 --- a/src/CronUpdater.php +++ b/src/CronUpdater.php @@ -182,6 +182,15 @@ class CronUpdater extends Updater { $this->apply(); } catch (\Throwable $e) { + // Send notifications about the failed update. + $mail_params = [ + 'previous_version' => $installed_version, + 'target_version' => $target_version, + 'error_message' => $e->getMessage(), + ]; + foreach ($this->getEmailRecipients() as $email => $langcode) { + $this->mailManager->mail('automatic_updates', 'cron_failed', $email, $langcode, $mail_params); + } $this->logger->error($e->getMessage()); // If an error occurred during the pre-create event, the stage will be @@ -276,10 +285,8 @@ class CronUpdater extends Updater { 'previous_version' => $installed_version, 'updated_version' => $target_version, ]; - $recipients = $this->configFactory->get('update.settings') - ->get('notification.emails'); - foreach ($recipients as $recipient) { - $this->mailManager->mail('automatic_updates', 'cron_successful', $recipient, $this->getEmailLangcode($recipient), $mail_params); + foreach ($this->getEmailRecipients() as $recipient => $langcode) { + $this->mailManager->mail('automatic_updates', 'cron_successful', $recipient, $langcode, $mail_params); } } catch (\Throwable $e) { @@ -317,6 +324,23 @@ class CronUpdater extends Updater { return $this->languageManager->getDefaultLanguage()->getId(); } + /** + * Returns an array of people to email with success or failure notifications. + * + * @return string[] + * An array whose keys are the email addresses to send notifications to, and + * values are the langcodes that they should be emailed in. + */ + protected function getEmailRecipients(): array { + $recipients = $this->configFactory->get('update.settings') + ->get('notification.emails'); + $emails = []; + foreach ($recipients as $recipient) { + $emails[$recipient] = $this->getEmailLangcode($recipient); + } + return $emails; + } + /** * Gets the cron update mode. * diff --git a/tests/modules/automatic_updates_test/automatic_updates_test.module b/tests/modules/automatic_updates_test/automatic_updates_test.module index bd140ddb95a993b7e4547b45fad71232f5e65f5a..2aec23f2382601623f993738994d6c552625ffa7 100644 --- a/tests/modules/automatic_updates_test/automatic_updates_test.module +++ b/tests/modules/automatic_updates_test/automatic_updates_test.module @@ -20,11 +20,9 @@ function automatic_updates_test_mail_alter(array &$message): void { $message['subject'], ]); foreach ($lines as $line) { - if (!($line instanceof TranslatableMarkup)) { - // All lines need to be translated. - return; + if ($line instanceof TranslatableMarkup) { + $line_langcodes[] = $line->getOption('langcode'); } - $line_langcodes[] = $line->getOption('langcode'); } $message['line_langcodes'] = array_unique($line_langcodes); } diff --git a/tests/src/Kernel/CronUpdaterTest.php b/tests/src/Kernel/CronUpdaterTest.php index c0e4c0c8c67e0a12210a214610756fb33261c483..9fe45de88a2032545cb9ab43b720db0a82c5e581 100644 --- a/tests/src/Kernel/CronUpdaterTest.php +++ b/tests/src/Kernel/CronUpdaterTest.php @@ -52,6 +52,16 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase { */ private $logger; + /** + * The people who should be emailed about successful or failed updates. + * + * The keys are the email addresses, and the values are the langcode they + * should be emailed in. + * + * @var string[] + */ + private $emailRecipients = []; + /** * {@inheritdoc} */ @@ -64,6 +74,26 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase { ->addLogger($this->logger); $this->installEntitySchema('user'); $this->installSchema('user', ['users_data']); + + // Prepare the recipient list to email when an update succeeds or fails. + // First, create a user whose preferred language is different from the + // default language, so we can be sure they're emailed in their preferred + // language; we also ensure that an email which doesn't correspond to a user + // account is emailed in the default language. + $default_language = $this->container->get('language_manager') + ->getDefaultLanguage() + ->getId(); + $this->assertNotSame('fr', $default_language); + + $account = $this->createUser([], NULL, FALSE, [ + 'preferred_langcode' => 'fr', + ]); + $this->emailRecipients['emissary@deep.space'] = $default_language; + $this->emailRecipients[$account->getEmail()] = $account->getPreferredLangcode(); + + $this->config('update.settings') + ->set('notification.emails', array_keys($this->emailRecipients)) + ->save(); } /** @@ -349,51 +379,99 @@ class CronUpdaterTest extends AutomaticUpdatesKernelTestBase { } /** - * Tests that user 1 is emailed when an unattended update succeeds. + * Tests that email is sent when an unattended update succeeds. */ public function testEmailOnSuccess(): void { - $default_language = $this->container->get('language_manager') - ->getDefaultLanguage() - ->getId(); - $this->assertNotSame('fr', $default_language); + $this->container->get('cron')->run(); - $account = $this->createUser([], NULL, FALSE, [ - 'preferred_langcode' => 'fr', + // Ensure we sent a success message to all recipients. + $sent_messages = $this->getMails([ + 'subject' => "Drupal core was successfully updated", ]); + $this->assertNotEmpty($sent_messages); + $this->assertSame(count($this->emailRecipients), count($sent_messages)); - $recipients = [ - 'emissary@deep.space', - $account->getEmail(), - ]; - $languages = [ - $default_language, - $account->getPreferredLangcode(), + foreach ($sent_messages as $message) { + $email = $message['to']; + $this->assertSame($message['langcode'], $this->emailRecipients[$email]); + $this->assertCorrectMessageSent($email, $message, $message['langcode'], "Congratulations!\n\nDrupal core was automatically updated from 9.8.0 to 9.8.1.\n"); + } + } + + /** + * Data provider for ::testEmailOnFailure(). + * + * @return string[][] + * The test cases. + */ + public function providerEmailOnFailure(): array { + return [ + 'pre-create' => [ + PreCreateEvent::class, + ], + 'pre-require' => [ + PreRequireEvent::class, + ], + 'pre-apply' => [ + PreApplyEvent::class, + ], ]; + } - $this->config('update.settings') - ->set('notification.emails', $recipients) - ->save(); + /** + * Tests that email is sent when an unattended update fails. + * + * @param string $event_class + * The event class that should trigger the failure. + * + * @dataProvider providerEmailOnFailure + */ + public function testEmailOnFailure(string $event_class): void { + $results = [ + ValidationResult::createError(['Error while updating!']), + ]; + TestSubscriber1::setTestResult($results, $event_class); + $exception = new StageValidationException($results); $this->container->get('cron')->run(); - // Ensure we sent a success message to all recipients. + // Ensure we sent a failure message to all recipients. $sent_messages = $this->getMails([ - 'subject' => "Drupal core was successfully updated", + 'subject' => "Drupal core update failed", ]); - $this->assertSame(count($recipients), count($sent_messages)); + $this->assertNotEmpty($sent_messages); + $this->assertSame(count($this->emailRecipients), count($sent_messages)); + + foreach ($sent_messages as $message) { + $email = $message['to']; + $this->assertSame($message['langcode'], $this->emailRecipients[$email]); + $this->assertCorrectMessageSent($email, $message, $message['langcode'], "Drupal core failed to update automatically from 9.8.0 to 9.8.1. The following\nerror was logged:\n\n" . $exception->getMessage()); + } + } + + /** + * Asserts correct message sent to correct recipient. + * + * @param string $expected_recipient + * The email address that should have received the message. + * @param array $sent_message + * The sent message, as processed by hook_mail(). + * @param string $expected_language_code + * The language code that the recipient should have been emailed in. + * @param string $expected_body_text + * The expected message that the email body should contain. + */ + private function assertCorrectMessageSent(string $expected_recipient, array $sent_message, string $expected_language_code, string $expected_body_text): void { // Ensure the messages had the correct body text, and were sent to the right // people. - foreach ($sent_messages as $i => $message) { - $this->assertSame($message['to'], $recipients[$i]); - $this->assertStringStartsWith("Congratulations!\n\nDrupal core was automatically updated from 9.8.0 to 9.8.1.\n", $message['body']); - // The message, and every line in it, should have been sent in the - // expected language. - $langcode = $message['langcode']; - $this->assertSame($langcode, $languages[$i]); - // @see automatic_updates_test_mail_alter() - $this->assertArrayHasKey('line_langcodes', $message); - $this->assertSame([$langcode], $message['line_langcodes']); - } + $this->assertSame($sent_message['to'], $expected_recipient); + $this->assertStringStartsWith($expected_body_text, $sent_message['body']); + // The message, and every line in it, should have been sent in the + // expected language. + $this->assertSame($expected_language_code, $sent_message['langcode']); + // @see automatic_updates_test_mail_alter() + $this->assertArrayHasKey('line_langcodes', $sent_message); + $this->assertSame([$expected_language_code], $sent_message['line_langcodes']); } }