diff --git a/core/lib/Drupal/Core/Test/AssertMailTrait.php b/core/lib/Drupal/Core/Test/AssertMailTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..f6b5279ca91f1290cb1c0418c05d651888171d56 --- /dev/null +++ b/core/lib/Drupal/Core/Test/AssertMailTrait.php @@ -0,0 +1,161 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Test\AssertMailTrait. + */ + +namespace Drupal\Core\Test; + +/** + * Provides methods for testing emails sent during test runs. + */ +trait AssertMailTrait { + + /** + * Gets an array containing all emails sent during this test case. + * + * @param array $filter + * An array containing key/value pairs used to filter the emails that are + * returned. + * + * @return array + * An array containing email messages captured during the current test. + */ + protected function getMails(array $filter = []) { + $captured_emails = $this->container->get('state')->get('system.test_mail_collector', []); + $filtered_emails = []; + + foreach ($captured_emails as $message) { + foreach ($filter as $key => $value) { + if (!isset($message[$key]) || $message[$key] != $value) { + continue 2; + } + } + $filtered_emails[] = $message; + } + + return $filtered_emails; + } + + /** + * Asserts that the most recently sent email message has the given value. + * + * The field in $name must have the content described in $value. + * + * @param string $name + * Name of field or message property to assert. Examples: subject, body, + * id, ... + * @param string $value + * Value of the field to assert. + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + * @param string $group + * (optional) The group this message is in, which is displayed in a column + * in test output. Use 'Debug' to indicate this is debugging output. Do not + * translate this string. Defaults to 'Email'; most tests do not override + * this default. + * + * @return bool + * TRUE on pass, FALSE on fail. + */ + protected function assertMail($name, $value = '', $message = '', $group = 'Email') { + $captured_emails = $this->container->get('state')->get('system.test_mail_collector') ?: []; + $email = end($captured_emails); + return $this->assertTrue($email && isset($email[$name]) && $email[$name] == $value, $message, $group); + } + + /** + * Asserts that the most recently sent email message has the string in it. + * + * @param string $field_name + * Name of field or message property to assert: subject, body, id, ... + * @param string $string + * String to search for. + * @param int $email_depth + * Number of emails to search for string, starting with most recent. + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + * @param string $group + * (optional) The group this message is in, which is displayed in a column + * in test output. Use 'Debug' to indicate this is debugging output. Do not + * translate this string. Defaults to 'Other'; most tests do not override + * this default. + * + * @return bool + * TRUE on pass, FALSE on fail. + */ + protected function assertMailString($field_name, $string, $email_depth, $message = '', $group = 'Other') { + $mails = $this->getMails(); + $string_found = FALSE; + // Cast MarkupInterface objects to string. + $string = (string) $string; + for ($i = count($mails) - 1; $i >= count($mails) - $email_depth && $i >= 0; $i--) { + $mail = $mails[$i]; + // Normalize whitespace, as we don't know what the mail system might have + // done. Any run of whitespace becomes a single space. + $normalized_mail = preg_replace('/\s+/', ' ', $mail[$field_name]); + $normalized_string = preg_replace('/\s+/', ' ', $string); + $string_found = (FALSE !== strpos($normalized_mail, $normalized_string)); + if ($string_found) { + break; + } + } + if (!$message) { + $message = format_string('Expected text found in @field of email message: "@expected".', ['@field' => $field_name, '@expected' => $string]); + } + return $this->assertTrue($string_found, $message, $group); + } + + /** + * Asserts that the most recently sent email message has the pattern in it. + * + * @param string $field_name + * Name of field or message property to assert: subject, body, id, ... + * @param string $regex + * Pattern to search for. + * @param string $message + * (optional) A message to display with the assertion. Do not translate + * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed + * variables in the message text, not t(). If left blank, a default message + * will be displayed. + * @param string $group + * (optional) The group this message is in, which is displayed in a column + * in test output. Use 'Debug' to indicate this is debugging output. Do not + * translate this string. Defaults to 'Other'; most tests do not override + * this default. + * + * @return bool + * TRUE on pass, FALSE on fail. + */ + protected function assertMailPattern($field_name, $regex, $message = '', $group = 'Other') { + $mails = $this->getMails(); + $mail = end($mails); + $regex_found = preg_match("/$regex/", $mail[$field_name]); + if (!$message) { + $message = format_string('Expected text found in @field of email message: "@expected".', ['@field' => $field_name, '@expected' => $regex]); + } + return $this->assertTrue($regex_found, $message, $group); + } + + /** + * Outputs to verbose the most recent $count emails sent. + * + * @param int $count + * Optional number of emails to output. + */ + protected function verboseEmail($count = 1) { + $mails = $this->getMails(); + for ($i = count($mails) - 1; $i >= count($mails) - $count && $i >= 0; $i--) { + $mail = $mails[$i]; + $this->verbose('Email:<pre>' . print_r($mail, TRUE) . '</pre>'); + } + } + +} diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index a987d13b41ed018a742d3c226ce1fd91f87f7992..681e75c1f1075bbf21b0e317a15f36f559e2030c 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -271,6 +271,12 @@ protected function setUp() { // The temporary stream wrapper is able to operate both with and without // configuration. $this->registerStreamWrapper('temporary', 'Drupal\Core\StreamWrapper\TemporaryStream'); + + // Manually configure the test mail collector implementation to prevent + // tests from sending out emails and collect them in state instead. + // While this should be enforced via settings.php prior to installation, + // some tests expect to be able to test mail system implementations. + $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector'; } /** diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php index e4e9622f64d1200a37cd0215b2a09d553b80d674..e9b63768202953adf89ea00a3c9fbd9b78f5d68f 100644 --- a/core/modules/simpletest/src/WebTestBase.php +++ b/core/modules/simpletest/src/WebTestBase.php @@ -28,6 +28,7 @@ use Drupal\Core\Session\UserSession; use Drupal\Core\Site\Settings; use Drupal\Core\StreamWrapper\PublicStream; +use Drupal\Core\Test\AssertMailTrait; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -47,6 +48,9 @@ abstract class WebTestBase extends TestBase { use ContentTypeCreationTrait { createContentType as drupalCreateContentType; } + use AssertMailTrait { + getMails as drupalGetMails; + } use NodeCreationTrait { getNodeByTitle as drupalGetNodeByTitle; createNode as drupalCreateNode; @@ -2518,32 +2522,6 @@ protected function assertHeader($header, $value, $message = '', $group = 'Browse return $this->assertTrue($header_value == $value, $message ? $message : 'HTTP response header ' . $header . ' with value ' . $value . ' found, actual value: ' . $header_value, $group); } - /** - * Gets an array containing all emails sent during this test case. - * - * @param $filter - * An array containing key/value pairs used to filter the emails that are - * returned. - * - * @return - * An array containing email messages captured during the current test. - */ - protected function drupalGetMails($filter = array()) { - $captured_emails = \Drupal::state()->get('system.test_mail_collector') ?: array(); - $filtered_emails = array(); - - foreach ($captured_emails as $message) { - foreach ($filter as $key => $value) { - if (!isset($message[$key]) || $message[$key] != $value) { - continue 2; - } - } - $filtered_emails[] = $message; - } - - return $filtered_emails; - } - /** * Passes if the internal browser's URL matches the given path. * @@ -2644,126 +2622,6 @@ protected function assertNoResponse($code, $message = '', $group = 'Browser') { return $this->assertFalse($match, $message ? $message : SafeMarkup::format('HTTP response not expected @code, actual @curl_code', array('@code' => $code, '@curl_code' => $curl_code)), $group); } - /** - * Asserts that the most recently sent email message has the given value. - * - * The field in $name must have the content described in $value. - * - * @param $name - * Name of field or message property to assert. Examples: subject, body, - * id, ... - * @param $value - * Value of the field to assert. - * @param $message - * (optional) A message to display with the assertion. Do not translate - * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed - * variables in the message text, not t(). If left blank, a default message - * will be displayed. - * @param $group - * (optional) The group this message is in, which is displayed in a column - * in test output. Use 'Debug' to indicate this is debugging output. Do not - * translate this string. Defaults to 'Email'; most tests do not override - * this default. - * - * @return - * TRUE on pass, FALSE on fail. - */ - protected function assertMail($name, $value = '', $message = '', $group = 'Email') { - $captured_emails = \Drupal::state()->get('system.test_mail_collector') ?: array(); - $email = end($captured_emails); - return $this->assertTrue($email && isset($email[$name]) && $email[$name] == $value, $message, $group); - } - - /** - * Asserts that the most recently sent email message has the string in it. - * - * @param $field_name - * Name of field or message property to assert: subject, body, id, ... - * @param $string - * String to search for. - * @param $email_depth - * Number of emails to search for string, starting with most recent. - * @param $message - * (optional) A message to display with the assertion. Do not translate - * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed - * variables in the message text, not t(). If left blank, a default message - * will be displayed. - * @param $group - * (optional) The group this message is in, which is displayed in a column - * in test output. Use 'Debug' to indicate this is debugging output. Do not - * translate this string. Defaults to 'Other'; most tests do not override - * this default. - * - * @return - * TRUE on pass, FALSE on fail. - */ - protected function assertMailString($field_name, $string, $email_depth, $message = '', $group = 'Other') { - $mails = $this->drupalGetMails(); - $string_found = FALSE; - // Cast MarkupInterface objects to string. - $string = (string) $string; - for ($i = count($mails) -1; $i >= count($mails) - $email_depth && $i >= 0; $i--) { - $mail = $mails[$i]; - // Normalize whitespace, as we don't know what the mail system might have - // done. Any run of whitespace becomes a single space. - $normalized_mail = preg_replace('/\s+/', ' ', $mail[$field_name]); - $normalized_string = preg_replace('/\s+/', ' ', $string); - $string_found = (FALSE !== strpos($normalized_mail, $normalized_string)); - if ($string_found) { - break; - } - } - if (!$message) { - $message = format_string('Expected text found in @field of email message: "@expected".', array('@field' => $field_name, '@expected' => $string)); - } - return $this->assertTrue($string_found, $message, $group); - } - - /** - * Asserts that the most recently sent email message has the pattern in it. - * - * @param $field_name - * Name of field or message property to assert: subject, body, id, ... - * @param $regex - * Pattern to search for. - * @param $message - * (optional) A message to display with the assertion. Do not translate - * messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed - * variables in the message text, not t(). If left blank, a default message - * will be displayed. - * @param $group - * (optional) The group this message is in, which is displayed in a column - * in test output. Use 'Debug' to indicate this is debugging output. Do not - * translate this string. Defaults to 'Other'; most tests do not override - * this default. - * - * @return - * TRUE on pass, FALSE on fail. - */ - protected function assertMailPattern($field_name, $regex, $message = '', $group = 'Other') { - $mails = $this->drupalGetMails(); - $mail = end($mails); - $regex_found = preg_match("/$regex/", $mail[$field_name]); - if (!$message) { - $message = format_string('Expected text found in @field of email message: "@expected".', array('@field' => $field_name, '@expected' => $regex)); - } - return $this->assertTrue($regex_found, $message, $group); - } - - /** - * Outputs to verbose the most recent $count emails sent. - * - * @param $count - * Optional number of emails to output. - */ - protected function verboseEmail($count = 1) { - $mails = $this->drupalGetMails(); - for ($i = count($mails) -1; $i >= count($mails) - $count && $i >= 0; $i--) { - $mail = $mails[$i]; - $this->verbose('Email:<pre>' . print_r($mail, TRUE) . '</pre>'); - } - } - /** * Creates a mock request and sets it on the generator. * diff --git a/core/tests/Drupal/KernelTests/Core/Test/AssertMailTraitTest.php b/core/tests/Drupal/KernelTests/Core/Test/AssertMailTraitTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7af7718d62bb7bd1778e2aa84cf1950b6c19e4df --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Test/AssertMailTraitTest.php @@ -0,0 +1,96 @@ +<?php + +/** + * @file + * Contains \Drupal\KernelTests\Core\Test\AssertMailTraitTest. + */ + +namespace Drupal\KernelTests\Core\Test; + +use Drupal\Core\Test\AssertMailTrait; +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests \Drupal\Core\Test\AssertMailTrait works. + * + * @group Test + * + * @coversDefaultClass \Drupal\Core\Test\AssertMailTrait + */ +class AssertMailTraitTest extends KernelTestBase { + use AssertMailTrait; + + /** + * Tests that the maintenance theme initializes the theme and its base themes. + */ + public function testAssertMailTrait() { + /* @var \Drupal\Core\Mail\MailManagerInterface $mail_service */ + $mail_service = \Drupal::service('plugin.manager.mail'); + + // Create an email. + $subject = $this->randomString(64); + $body = $this->randomString(128); + $message = [ + 'id' => 'drupal_mail_test', + 'headers' => ['Content-type' => 'text/html'], + 'subject' => $subject, + 'to' => 'foobar@example.com', + 'body' => $body, + ]; + + // Before we send the email, \Drupal\Core\Test\AssertMailTrait::getMails() + // should return an empty array. + $captured_emails = $this->getMails(); + $this->assertCount(0, $captured_emails, 'The captured emails queue is empty.'); + + // Send the email. + $mail_service->getInstance(['module' => 'simpletest', 'key' => 'drupal_mail_test'])->mail($message); + + // Ensure that there is one email in the captured emails array. + $captured_emails = $this->getMails(); + $this->assertEquals(count($captured_emails), 1, 'One email was captured.'); + + // Assert that the email was sent by iterating over the message properties + // and ensuring that they are captured intact. + foreach ($message as $field => $value) { + $this->assertMail($field, $value, "The email was sent and the value for property $field is intact."); + } + + // Send additional emails so more than one email is captured. + for ($index = 0; $index < 5; $index++) { + $message = [ + 'id' => 'drupal_mail_test_' . $index, + 'headers' => ['Content-type' => 'text/html'], + 'subject' => $this->randomString(64), + 'to' => $this->randomMachineName(32) . '@example.com', + 'body' => $this->randomString(512), + ]; + $mail_service->getInstance(['module' => 'drupal_mail_test', 'key' => $index])->mail($message); + } + + // There should now be 6 emails captured. + $captured_emails = $this->getMails(); + $this->assertCount(6, $captured_emails, 'All emails were captured.'); + + // Test different ways of getting filtered emails via + // \Drupal\Core\Test\AssertMailTrait::getMails(). + $captured_emails = $this->getMails(['id' => 'drupal_mail_test']); + $this->assertCount(1, $captured_emails, 'Only one email is returned when filtering by id.'); + $captured_emails = $this->getMails(['id' => 'drupal_mail_test', 'subject' => $subject]); + $this->assertCount(1, $captured_emails, 'Only one email is returned when filtering by id and subject.'); + $captured_emails = $this->getMails([ + 'id' => 'drupal_mail_test', + 'subject' => $subject, + 'from' => 'this_was_not_used@example.com', + ]); + $this->assertCount(0, $captured_emails, 'No emails are returned when querying with an unused from address.'); + + // Send the last email again, so we can confirm that + // \Drupal\Core\Test\AssertMailTrait::getMails() filters correctly returns + // all emails with a given property/value. + $mail_service->getInstance(['module' => 'drupal_mail_test', 'key' => $index])->mail($message); + $captured_emails = $this->getMails(['id' => 'drupal_mail_test_4']); + $this->assertCount(2, $captured_emails, 'All emails with the same id are returned when filtering by id.'); + } + +} diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php index d20f341b38005f2519f97345052c05681f38e662..25ca3b7abe78fc2ab111225ff0c7f76cb0f3a400 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -370,6 +370,12 @@ private function bootKernel() { 'class' => '\Drupal\Component\PhpStorage\FileStorage', ]; new Settings($settings); + + // Manually configure the test mail collector implementation to prevent + // tests from sending out emails and collect them in state instead. + // While this should be enforced via settings.php prior to installation, + // some tests expect to be able to test mail system implementations. + $GLOBALS['config']['system.mail']['interface']['default'] = 'test_mail_collector'; } /**