Commit 7d2f1492 authored by Kunal Sachdev's avatar Kunal Sachdev Committed by Adam G-H
Browse files

Issue #3299612 by kunal.sachdev, yash.rode, phenaproxima: Send an email when...

Issue #3299612 by kunal.sachdev, yash.rode, phenaproxima: Send an email when an unattended update fails
parent f9893dc0
Loading
Loading
Loading
Loading
+22 −7
Original line number Diff line number Diff line
@@ -42,14 +42,29 @@ function automatic_updates_mail(string $key, array &$message, array $params): vo
  $options = [
    'langcode' => $message['langcode'],
  ];

  if ($key === 'cron_successful') {
  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;

    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);
  }
+28 −4
Original line number Diff line number Diff line
@@ -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.
   *
+2 −4
Original line number Diff line number Diff line
@@ -20,12 +20,10 @@ 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');
      }
    }
    $message['line_langcodes'] = array_unique($line_langcodes);
  }
}
+109 −31
Original line number Diff line number Diff line
@@ -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']);
    $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.
      $langcode = $message['langcode'];
      $this->assertSame($langcode, $languages[$i]);
    $this->assertSame($expected_language_code, $sent_message['langcode']);
    // @see automatic_updates_test_mail_alter()
      $this->assertArrayHasKey('line_langcodes', $message);
      $this->assertSame([$langcode], $message['line_langcodes']);
    }
    $this->assertArrayHasKey('line_langcodes', $sent_message);
    $this->assertSame([$expected_language_code], $sent_message['line_langcodes']);
  }

}