From f3eedaaf2a56a00cb5d87aa701744486a0c16283 Mon Sep 17 00:00:00 2001 From: Simey Lameze Date: Mon, 17 Feb 2025 10:45:45 +0800 Subject: [PATCH] MDL-75971 behat: add email handling capabilities to Behat --- admin/tool/policy/tests/behat/consent.feature | 56 ++-- .../tool/policy/tests/behat/optional.feature | 6 +- auth/email/tests/behat/signup.feature | 16 +- lib/behat/classes/behat_session_trait.php | 26 ++ lib/behat/lib.php | 6 + lib/classes/test/email_catcher.php | 49 +++ lib/classes/test/mailpit_email_catcher.php | 122 +++++++ lib/classes/test/mailpit_message.php | 227 +++++++++++++ lib/classes/test/mailpit_message_user.php | 90 +++++ lib/classes/test/message.php | 85 +++++ lib/classes/test/message_user.php | 41 +++ lib/moodlelib.php | 8 +- lib/tests/behat/behat_email.php | 309 ++++++++++++++++++ lib/tests/behat/email.feature | 21 ++ lib/upgrade.txt | 11 + 15 files changed, 1031 insertions(+), 42 deletions(-) create mode 100644 lib/classes/test/email_catcher.php create mode 100644 lib/classes/test/mailpit_email_catcher.php create mode 100644 lib/classes/test/mailpit_message.php create mode 100644 lib/classes/test/mailpit_message_user.php create mode 100644 lib/classes/test/message.php create mode 100644 lib/classes/test/message_user.php create mode 100644 lib/tests/behat/behat_email.php create mode 100644 lib/tests/behat/email.feature diff --git a/admin/tool/policy/tests/behat/consent.feature b/admin/tool/policy/tests/behat/consent.feature index 06da24e4c4a..ed10397361c 100644 --- a/admin/tool/policy/tests/behat/consent.feature +++ b/admin/tool/policy/tests/behat/consent.feature @@ -4,7 +4,7 @@ Feature: User must accept policy managed by this plugin when logging in and sign As a user I need to be able to accept site policy during sign up - Scenario: Accept policy on sign up, no site policy + Scenario: Accept policy on sign up manage by plugin, no site policy Given the following config values are set as admin: | registerauth | email | | passwordpolicy | 0 | @@ -16,13 +16,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | user1@example.com | + | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at user1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" @@ -50,13 +50,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | user1@example.com | + | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at user1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" @@ -95,13 +95,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | user1@example.com | + | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at user1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" @@ -154,13 +154,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | user1@example.com | + | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at user1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" @@ -207,13 +207,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | user1@example.com | + | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at user1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" @@ -519,13 +519,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | user1@example.com | + | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | When I press "Create my new account" Then I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at user1@example.com" And I follow "Log in" When I click on "Create new account" "link" # Confirm that the user can view and accept policies when attempting to create another account. @@ -641,13 +641,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | user1@example.com | + | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at user1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" @@ -710,13 +710,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | user1@example.com | + | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at user1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" @@ -852,13 +852,13 @@ Feature: User must accept policy managed by this plugin when logging in and sign And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | user1@example.com | + | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at user1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" diff --git a/admin/tool/policy/tests/behat/optional.feature b/admin/tool/policy/tests/behat/optional.feature index 374ad922dec..07b1d11b0c5 100644 --- a/admin/tool/policy/tests/behat/optional.feature +++ b/admin/tool/policy/tests/behat/optional.feature @@ -83,13 +83,13 @@ Feature: Optional policies And I set the following fields to these values: | Username | user3 | | Password | user3 | - | Email address | user3@address.invalid | - | Email (again) | user3@address.invalid | + | Email address | user3@example.com | + | Email (again) | user3@example.com | | First name | User3 | | Last name | L3 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user3@address.invalid" + And I should see "An email should have been sent to your address at user3@example.com" And I confirm email for "user3" And I should see "Thanks, User3 L3" And I should see "Your registration has been confirmed" diff --git a/auth/email/tests/behat/signup.feature b/auth/email/tests/behat/signup.feature index 297afe7f70f..db56867539d 100644 --- a/auth/email/tests/behat/signup.feature +++ b/auth/email/tests/behat/signup.feature @@ -15,13 +15,13 @@ Feature: User must accept policy when logging in and signing up And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | student1@example.com | + | Email (again) | student1@example.com | | First name | User1 | | Last name | L1 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at student1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" @@ -45,14 +45,14 @@ Feature: User must accept policy when logging in and signing up And I set the following fields to these values: | Username | user1 | | Password | user1 | - | Email address | user1@address.invalid | - | Email (again) | user1@address.invalid | + | Email address | user1@example.com | + | Email (again) | user1@example.com | | First name | User1 | | Last name | L1 | | I understand and agree | 1 | And I press "Create my new account" And I should see "Confirm your account" - And I should see "An email should have been sent to your address at user1@address.invalid" + And I should see "An email should have been sent to your address at user1@example.com" And I confirm email for "user1" And I should see "Thanks, User1 L1" And I should see "Your registration has been confirmed" @@ -67,8 +67,8 @@ Feature: User must accept policy when logging in and signing up Scenario Outline: Email validation during email registration Given the following config values are set as admin: | allowaccountssameemail | | - | registerauth | email | - | passwordpolicy | 0 | + | registerauth | email | + | passwordpolicy | 0 | And the following "users" exist: | username | firstname | lastname | email | | s1 | John | Doe | s1@example.com | diff --git a/lib/behat/classes/behat_session_trait.php b/lib/behat/classes/behat_session_trait.php index 365b1ec59cc..030914abdc2 100644 --- a/lib/behat/classes/behat_session_trait.php +++ b/lib/behat/classes/behat_session_trait.php @@ -1694,6 +1694,32 @@ EOF; return !empty($matches); } + /** + * Get the user object from an identifier. + * + * The user username and email fields are checked. + * + * @param string $identifier The user's username or email. + * @return stdClass|null The user id or null if not found. + */ + protected function get_user_by_identifier(string $identifier): ?stdClass { + global $DB; + + $sql = <<get_record_sql($sql, [ + 'username' => $identifier, + 'email' => $identifier, + ]); + + return $result ?: null; + } + /** * Get the user id from an identifier. * diff --git a/lib/behat/lib.php b/lib/behat/lib.php index bbe95e66099..d525e6fb5d3 100644 --- a/lib/behat/lib.php +++ b/lib/behat/lib.php @@ -233,6 +233,12 @@ function behat_clean_init_config() { unset($CFG->{$key}); } } + + // Allow email catcher settings. + if (defined('TEST_EMAILCATCHER_MAIL_SERVER')) { + $CFG->noemailever = false; + $CFG->smtphosts = TEST_EMAILCATCHER_MAIL_SERVER; + } } /** diff --git a/lib/classes/test/email_catcher.php b/lib/classes/test/email_catcher.php new file mode 100644 index 00000000000..d2abf6e6458 --- /dev/null +++ b/lib/classes/test/email_catcher.php @@ -0,0 +1,49 @@ +. + +namespace core\test; + +/** + * Generic email catcher interface. + * + * @package core + * @category test + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright Simey Lameze + */ +interface email_catcher { + + /** + * Get a list of messages from the mailpit server. + * + * @param bool $showdetails Optional. Whether to include detailed information in the messages. Default is false. + * @return iterable + */ + public function get_messages(bool $showdetails = false): iterable; + + /** + * Delete all messages from the mailpit server. + */ + public function delete_all(); + + /** + * Search for a message in the mailpit server. + * + * @param string $query The search query. + * @return iterable + */ + public function search(string $query): iterable; +} diff --git a/lib/classes/test/mailpit_email_catcher.php b/lib/classes/test/mailpit_email_catcher.php new file mode 100644 index 00000000000..005ff8ade3a --- /dev/null +++ b/lib/classes/test/mailpit_email_catcher.php @@ -0,0 +1,122 @@ +. + +/** + * Mailpit mail handling implementation. + * + * @package core + * @category test + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright Simey Lameze + */ +namespace core\test; + +use core\http_client; +use stdClass; + +/** + * Mailpit email handling class. + * + * @package core + * @category test + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mailpit_email_catcher implements email_catcher { + + /** @var http_client The http client object. */ + protected http_client $httpclient; + + /** + * Constructor. + * + * @param string $baseuri The base uri for the mailpit server. + */ + public function __construct(string $baseuri) { + $this->httpclient = new http_client(['base_uri' => $baseuri]); + } + + /** + * Reset the mailpit server after a test. + */ + public function reset_after_test(): void { + $this->httpclient->delete_all(); + } + + /** + * Delete all messages from the mailpit server. + */ + public function delete_all() { + $this->httpclient->delete('api/v1/messages'); + } + + /** + * Get a list of messages from the mailpit server. + * + * @param bool $showdetails Optional. Whether to include detailed information in the messages. Default is false. + * @return iterable + */ + public function get_messages(bool $showdetails = false): iterable { + $uri = 'api/v1/messages'; + $options = [ + 'query' => [ + 'start' => 0, + ], + ]; + + do { + $response = $this->httpclient->get( + uri: $uri, + options: $options, + ); + + $data = json_decode($response->getBody()); + foreach ($data->messages as $messagedata) { + yield mailpit_message::create_from_api_response($this, $messagedata, $showdetails); + } + + $options['query']['start'] = $data->start + $data->count; + } while ($data->total > ($options['query']['start'])); + } + + /** + * Get the message summary for a specific message. + * + * @param string $id The message id. + * @return stdClass + */ + public function get_message_data(string $id): stdClass { + $response = $this->httpclient->get("api/v1/message/{$id}"); + + return json_decode($response->getBody()); + } + + /** + * Search for a message in the mailpit server. + * + * @param string $query The search query. + * @return mixed + */ + public function search(string $query): iterable { + $uri = "api/v1/search?query={$query}"; + + $response = $this->httpclient->get($uri); + $data = json_decode($response->getBody()); + + foreach ($data->messages as $messagedata) { + yield mailpit_message::create_from_api_response($this, $messagedata); + } + } +} diff --git a/lib/classes/test/mailpit_message.php b/lib/classes/test/mailpit_message.php new file mode 100644 index 00000000000..98a71db5ea0 --- /dev/null +++ b/lib/classes/test/mailpit_message.php @@ -0,0 +1,227 @@ +. + +namespace core\test; + +use stdClass; + +/** + * Mailpit message handling implementation. + * + * @package core + * @category test + * @copyright Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class mailpit_message implements message { + + /** @var bool Whether the message has been loaded. */ + protected bool $messageloaded = false; + + /** + * Constructor. + * + * @param email_catcher $client The email catcher client. + * @param string $id The message ID. + * @param stdClass $sender The sender. + * @param string $subject The subject. + * @param array $recipients The recipients. + * @param array $cc The cc recipients. + * @param array $bcc The bcc recipients. + * @param int $attachmentcount The number of attachments. + * @param string|null $text The text body. + * @param string|null $html The HTML body. + * @param array $attachments The attachments. + * @param array $inline The inline attachments. + */ + protected function __construct( + /** @var email_catcher $client The email catcher client used for message operations.*/ + private readonly email_catcher $client, + /** @var string $id The unique identifier for the message. */ + private readonly string $id, + /** @var stdClass $sender The sender of the message, represented as an object with email details. */ + private readonly stdClass $sender, + /** @var string $subject The subject line of the message. */ + private readonly string $subject, + /** @var array $recipients List of primary recipients for the message. */ + private readonly array $recipients, + /** @var array $cc List of carbon copy recipients (optional, defaults to an empty array). */ + private readonly array $cc = [], + /** @var array $bcc List of blind carbon copy recipients (optional, defaults to an empty array). */ + private readonly array $bcc = [], + /** @var int $attachmentcount The number of attachments in the message (default is 0). */ + private readonly int $attachmentcount = 0, + /** @var ?string $text The plain text body of the message (nullable, might be loaded later). */ + private ?string $text = null, + /** @var ?string $html The HTML body of the message (nullable, might be loaded later). */ + private ?string $html = null, + /** @var array $attachments An array of attachment details (defaults to empty array). */ + private array $attachments = [], + /** @var array $inline An array of inline elements like images or styles (defaults to empty array). */ + private array $inline = [], + ) { + } + + /** + * Load the message content. + */ + protected function load_message_content(): void { + if (!$this->messageloaded) { + $message = $this->client->get_message_data($this->id); + $this->text = $message->Text; + $this->html = $message->HTML; + $this->attachments = $message->Attachments; + $this->inline = $message->Inline; + $this->messageloaded = true; + } + } + + /** + * Create a message from an api response. + * + * @param email_catcher $client The email catcher client. + * @param stdClass $message The api response. + * @param bool $showdetails Optional. Whether to include detailed information in the messages. Default is false. + * @return mailpit_message The created mailpit message instance. + */ + public static function create_from_api_response( + email_catcher $client, + stdClass $message, + bool $showdetails = false, + ): self { + $message = new self( + client: $client, + id: $message->ID, + sender: $message->From, + subject: $message->Subject, + recipients: $message->To, + cc: $message->Cc, + attachmentcount: $message->Attachments, + ); + + if ($showdetails) { + $message->load_message_content(); + } + + return $message; + } + + /** + * Get the text representation of the body, if one was provided. + * + * @return null|string The text body. + */ + public function get_body_text(): ?string { + return $this->text; + } + + /** + * Get the HTML representation of the body, if one was provided. + * + * @return null|string The HTML body. + */ + public function get_body_html(): ?string { + return $this->html; + } + + /** + * Get the message ID. + * + * @return string The ID. + */ + public function get_id(): string { + return $this->id; + } + + /** + * Get the message recipients. + * + * @return iterable The recipients. + */ + public function get_recipients(): iterable { + foreach ($this->recipients as $user) { + yield mailpit_message_user::from_recipient($user); + } + } + + /** + * Get the first recipient of the message. + * + * @return string The email address of the first recipient. + */ + public function get_first_recipient(): string { + $recipients = $this->get_recipients(); + foreach ($recipients as $recipient) { + return $recipient->get_address(); + } + return ''; + } + + /** + * Whether the message has the specified recipient. + * + * @param string $email The email address. + * @return bool Whether the message has the recipient. + */ + public function has_recipient(string $email): bool { + foreach ($this->get_recipients() as $recipient) { + if ($recipient->get_address() === $email) { + return true; + } + } + return false; + } + + /** + * Get the message cc recipients. + * + * @return iterable The cc recipients. + */ + public function get_cc(): iterable { + foreach ($this->cc as $user) { + yield mailpit_message_user::from_recipient($user); + } + } + + /** + * Get the message bcc recipients. + * + * @return iterable The bcc recipients. + */ + public function get_bcc(): iterable { + foreach ($this->bcc as $user) { + yield mailpit_message_user::from_recipient($user); + } + } + + /** + * Get the message subject. + * + * @return string The subject. + */ + public function get_subject(): string { + return $this->subject; + } + + /** + * Get the message sender. + * + * @return message_user The sender. + */ + public function get_sender(): message_user { + return mailpit_message_user::from_sender($this->sender); + } +} diff --git a/lib/classes/test/mailpit_message_user.php b/lib/classes/test/mailpit_message_user.php new file mode 100644 index 00000000000..b98ec517df8 --- /dev/null +++ b/lib/classes/test/mailpit_message_user.php @@ -0,0 +1,90 @@ +. + +namespace core\test; + +/** + * Message user interface for Mailpit. + * + * @package core + * @category test + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright Andrew Lyons + */ +class mailpit_message_user implements message_user { + + /** + * Create a new message user object from a sender. + * + * @param \stdClass $user The user object. + * @return self + */ + public static function from_sender( + \stdClass $user, + ): self { + return new self( + name: $user->Name, + address: $user->Address, + ); + } + + /** + * Create a new message user object from a recipient. + * + * @param \stdClass $user The user object. + * @return self + */ + public static function from_recipient( + \stdClass $user, + ): self { + return new self( + name: $user->Name, + address: $user->Address, + ); + } + + /** + * Constructor. + * + * @param string $name The name of the user. + * @param string $address The email address of the user. + */ + protected function __construct( + /** @var string The name of the user. */ + protected string $name, + /** @var string The email address of the user. */ + protected string $address + ) { + } + + /** + * Get the display name of the user. + * + * @return string + */ + public function get_name(): string { + return $this->name; + } + + /** + * Get the email address of the user. + * + * @return string + */ + public function get_address(): string { + return $this->address; + } +} diff --git a/lib/classes/test/message.php b/lib/classes/test/message.php new file mode 100644 index 00000000000..cf0b8d5a00b --- /dev/null +++ b/lib/classes/test/message.php @@ -0,0 +1,85 @@ +. + +namespace core\test; + +/** + * Generic message interface. + * + * @package core + * @category test + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright Simey Lameze + */ +interface message { + + /** + * Get the message subject. + * + * @return string + */ + public function get_subject(): string; + + /** + * Get the text representation of the body, if one was provided. + * + * @return null|string + */ + public function get_body_text(): ?string; + + /** + * Get the HTML representation of the body, if one was provided. + * + * @return null|string + */ + public function get_body_html(): ?string; + + /** + * Get the message sender. + * + * @return message_user + */ + public function get_sender(): message_user; + + /** + * Get the message recipients. + * + * @return iterable + */ + public function get_recipients(): iterable; + + /** + * Whether the message has the specified recipient. + * + * @param string $email The email address. + * @return bool + */ + public function has_recipient(string $email): bool; + + /** + * Get the message cc recipients. + * + * @return iterable + */ + public function get_cc(): iterable; + + /** + * Get the message cc recipients. + * + * @return iterable + */ + public function get_bcc(): iterable; +} diff --git a/lib/classes/test/message_user.php b/lib/classes/test/message_user.php new file mode 100644 index 00000000000..1c265d3ac3a --- /dev/null +++ b/lib/classes/test/message_user.php @@ -0,0 +1,41 @@ +. + +namespace core\test; + +/** + * Generic message user interface. + * + * @package core + * @category test + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @copyright Andrew Lyons + */ +interface message_user { + /** + * Get the display name of the user. + * + * @return string + */ + public function get_name(): string; + + /** + * Get the email address of the user. + * + * @return string + */ + public function get_address(): string; +} diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 73c4ce0d6da..b734820bffc 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -5595,14 +5595,16 @@ function email_to_user($user, $from, $subject, $messagetext, $messagehtml = '', return false; } - if (defined('BEHAT_SITE_RUNNING')) { - // Fake email sending in behat. + if (defined('BEHAT_SITE_RUNNING') && !defined('TEST_EMAILCATCHER_MAIL_SERVER') && + !defined('TEST_EMAILCATCHER_API_SERVER')) { + + // Behat tests are running and we are not using email catcher so fake email sending. return true; } if (!empty($CFG->noemailever)) { // Hidden setting for development sites, set in config.php if needed. - debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_NORMAL); + debugging('Not sending email due to $CFG->noemailever config setting', DEBUG_DEVELOPER); return true; } diff --git a/lib/tests/behat/behat_email.php b/lib/tests/behat/behat_email.php new file mode 100644 index 00000000000..aa79af469f9 --- /dev/null +++ b/lib/tests/behat/behat_email.php @@ -0,0 +1,309 @@ +. + +use core\test\message; +use Behat\Gherkin\Node\TableNode; +use Behat\Mink\Exception\ExpectationException; +use Moodle\BehatExtension\Exception\SkippedException; + +require_once(__DIR__ . '/../../behat/behat_base.php'); + +/** + * Steps definitions to assist with email testing. + * + * @package core + * @category test + * @copyright Simey Lameze + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_email extends behat_base { + + /** + * Check if email catcher is configured. + * + * @return bool + */ + public static function is_email_catcher_configured(): bool { + return defined('TEST_EMAILCATCHER_MAIL_SERVER') && defined('TEST_EMAILCATCHER_API_SERVER'); + } + + /** + * Behat step to check if email catcher is configured. + * + * @Given /^an email catcher server is configured$/ + */ + public function email_catcher_is_configured(): void { + if (!self::is_email_catcher_configured()) { + throw new SkippedException( + 'The TEST_EMAILCATCHER_MAIL_SERVER and TEST_EMAILCATCHER_API_SERVER constants must be ' . + 'defined in config.php to use the mailcatcher steps.' + ); + } + } + + /** + * Get the email catcher object or thrown a SkippedException if TEST_MAILPIT_SERVER is not defined. + * + * @return \core\test\email_catcher|null + */ + private function get_catcher(): ?\core\test\email_catcher { + if (self::is_email_catcher_configured()) { + return new \core\test\mailpit_email_catcher(TEST_EMAILCATCHER_API_SERVER); + } + + return null; + } + + /** + * Clean up the email inbox after each scenario. + * + * @AfterScenario @behat_email + */ + public function reset_after_test(): void { + $this->get_catcher()?->delete_all(); + } + + /** + * Get the e-mail address of a user from the step input. + * + * This could be an e-mail address, or a username. + * + * @param string $input The input from the step. + * @return string + */ + private function get_email_address_from_input(string $input): string { + if (strpos($input, '@') !== false) { + return $input; + } + + $user = $this->get_user_by_identifier($input); + if (!$user) { + throw new ExpectationException("No user found with identifier {$input}", $this->getSession()->getDriver()); + } + + return $user->email; + } + + /** + * Get any message matching the supplied user and subject. + * + * @param string $user The user to check for. + * @param string $subject The subject to check for. + * @return iterable + */ + private function get_messages_matching_address_and_subject( + string $user, + string $subject, + ): iterable { + $address = $this->get_email_address_from_input($user); + return new \CallbackFilterIterator( + iterator: $this->get_catcher()->get_messages(showdetails: true), + callback: function (message $message) use ($address, $subject): bool { + if (!$message->has_recipient($address)) { + return false; + } + + if (strpos($message->get_subject(), $subject) === false) { + return false; + } + + return true; + }, + ); + } + + /** + * Verifies the content of an email sent to a specific user and subject. + * + * @Given the email to :user with subject containing :subject should contain :content + * + * @param string $user The user to check for. + * @param string $subject The subject to check for. + * @param string $content The content to check for. + */ + public function verify_email_content(string $user, string $subject, string $content): void { + $messages = $this->get_messages_matching_address_and_subject($user, $subject); + + $count = 0; + foreach ($messages as $message) { + $count++; + $this->validate_data('content', $message, $content); + } + + if ($count === 0) { + throw new ExpectationException( + "No messages found with subject containing {$subject}", + $this->getSession()->getDriver(), + ); + } + } + + /** + * Custom Behat test to verify the number of emails for a user. + * + * @Then user :address should have :count emails + * + * @param string $address The user to check for. + * @param int $expected The number of emails to check for. + */ + public function verify_email_count(string $address, int $expected): void { + $address = $this->get_email_address_from_input($address); + $messages = new \CallbackFilterIterator( + iterator: $this->get_catcher()->get_messages(), + callback: fn($message) => $message->has_recipient($address), + ); + + $count = iterator_count($messages); + if ($count !== $expected) { + throw new ExpectationException( + sprintf( + 'Expected %d messages, but found %d', + $expected, + $count, + ), + $this->getSession(), + ); + } + } + + /** + * Custom Behat test to empty the email inbox. + * + * @When I empty the email inbox + */ + public function empty_email_inbox() { + $this->get_catcher()->delete_all(); + } + + /** + * Behat step to send emails. + * + * @Given the following emails have been sent: + * + * @param TableNode $table The table of emails to send. + */ + public function the_following_emails_have_been_sent(TableNode $table): void { + if (!$rows = $table->getRows()) { + return; + } + + // Allowed fields. + $allowedfields = ['to', 'subject', 'message']; + + // Create a map of header to index. + $headers = array_flip($rows[0]); + // Remove header row. + unset($rows[0]); + + // Validate supplied headers. + foreach ($headers as $header => $index) { + if (!in_array($header, $allowedfields)) { + throw new ExpectationException("Invalid header {$header} found in table", $this->getSession()->getDriver()); + } + } + + foreach ($rows as $row) { + // Check if the required headers are set in the $headers map. + $to = isset($headers['to']) ? $row[$headers['to']] : 'userto@example.com'; + $subject = isset($headers['subject']) ? $row[$headers['subject']] : 'Default test subject'; + $message = isset($headers['message']) ? $row[$headers['message']] : 'Default test message'; + + // Use no-reply user as dummy user to send emails from. + $noreplyuser = \core_user::get_user(\core_user::NOREPLY_USER); + + // Create a dummy user to send emails to. + $emailuserto = new stdClass(); + $emailuserto->id = -99; + $emailuserto->email = $to; + $emailuserto->firstname = 'Test'; + $emailuserto->lastname = 'User'; + + // Send test email. + email_to_user($emailuserto, $noreplyuser, $subject, $message); + } + } + + /** + * Validate the emails expected and actual values. + * + * @param string $field The field to validate. + * @param message $message The expected value. + * @param string $expected The actual value. + */ + private function validate_data( + string $field, + message $message, + string $expected, + ): void { + switch ($field) { + case 'user': + $actual = $message->get_recipients(); + foreach ($actual as $recipient) { + if ($recipient->get_address() === $expected) { + return; + } + } + throw new ExpectationException( + sprintf( + 'Expected %s %s, but found %s', + $expected, + $field, + $actual, + ), + $this->getSession(), + ); + case 'subject': + $actual = $message->get_subject(); + if (str_contains($expected, $actual)) { + return; + } + throw new ExpectationException( + sprintf( + 'Expected %s %s, but found %s', + $expected, + $field, + $actual, + ), + $this->getSession(), + ); + case 'content': + if (str_contains($expected, $message->get_body_text())) { + return; + } + if (str_contains($expected, $message->get_body_html())) { + return; + } + throw new ExpectationException( + sprintf( + 'Expected %s to contain %s, but it does not. Actual text was:\n%s\nActual HTML content was:\n%s\n', + $field, + $expected, + $message->get_body_text(), + $message->get_body_html(), + ), + $this->getSession(), + ); + default: + throw new ExpectationException( + sprintf( + 'Unknown field to validate: %s', + $field, + ), + $this->getSession(), + ); + } + } +} diff --git a/lib/tests/behat/email.feature b/lib/tests/behat/email.feature new file mode 100644 index 00000000000..1fc4066a494 --- /dev/null +++ b/lib/tests/behat/email.feature @@ -0,0 +1,21 @@ +@behat_test @behat_email +Feature: Testing Email Functionality + + Background: + Given an email catcher server is configured + + Scenario: Verifying email content to user + When the following emails have been sent: + | to | subject | message | + | student1@example.com | A testing subject | This is a test message | + Then the email to "student1@example.com" with subject containing "A testing subject" should contain "This is a test message" + + Scenario: Test emptying the email inbox + When the following emails have been sent: + | to | subject | + | student1@example.com | Apple | + | student1@example.com | Banana | + | student1@example.com | Chocolate | + Then user "student1@example.com" should have 3 emails + And I empty the email inbox + And user "student1@example.com" should have 0 emails diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 7eb4f38d3ca..5242faf529f 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -1,6 +1,17 @@ This files describes API changes in core libraries and APIs, information provided here is intended especially for developers. +=== 4.4.8 === + +* Behat now supports email content verification using Mailpit. +You can check the contents of an email using the step `Then the email to "user@example.com" with subject containing "subject" should contain "content".` +To use this feature: +1. Ensure that Mailpit is running +2. Define the following constants in your `config.php`: + - `TEST_EMAILCATCHER_MAIL_SERVER` - The Mailpit server address (e.g. `0.0.0.0:1025`) + - `TEST_EMAILCATCHER_API_SERVER` - The Mailpit API server (qe.g. `http://localhost:8025`) +3. Ensure that the email catcher is set up using the step `Given an email catcher server is configured`. + === 4.4.6 === * A new core\ip_utils::normalize_internet_address() method is created to