diff --git a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php index 6788eaf4..00d8aa22 100644 --- a/src/Codeception/Module/Symfony/MailerAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MailerAssertionsTrait.php @@ -10,7 +10,7 @@ use Symfony\Component\Mailer\Event\MessageEvents; use Symfony\Component\Mailer\EventListener\MessageLoggerListener; use Symfony\Component\Mailer\Test\Constraint as MailerConstraint; -use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\RawMessage; use function array_key_last; @@ -88,7 +88,8 @@ public function dontSeeEmailIsSent(): void } /** - * Returns the last sent email. + * Returns the last sent message or `null` if no message was sent. + * The return type is `RawMessage`, which covers both `Email` and `Message` objects. * The function is based on `\Symfony\Component\Mailer\EventListener\MessageLoggerListener`, which means: * If your app performs an HTTP redirect after sending the email, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first. * See also: [grabSentEmails()](https://codeception.com/docs/modules/Symfony#grabSentEmails) @@ -96,16 +97,12 @@ public function dontSeeEmailIsSent(): void * ```php * grabLastSentEmail(); - * $address = $email->getTo()[0]; - * $I->assertSame('john_doe@example.com', $address->getAddress()); + * $I->assertEmailHasHeader('To', $email); * ``` */ - public function grabLastSentEmail(): ?Email + public function grabLastSentEmail(): ?RawMessage { - /** @var Email[] $emails */ - $emails = $this->getMessageMailerEvents()->getMessages(); - - return $emails ? $emails[array_key_last($emails)] : null; + return $this->grabLastSentRawMessage(); } /** @@ -119,7 +116,7 @@ public function grabLastSentEmail(): ?Email * $emails = $I->grabSentEmails(); * ``` * - * @return \Symfony\Component\Mime\RawMessage[] + * @return RawMessage[] */ public function grabSentEmails(): array { @@ -161,6 +158,13 @@ public function getMailerEvent(int $index = 0, ?string $transport = null): ?Mess return $this->getMessageMailerEvents()->getEvents($transport)[$index] ?? null; } + protected function grabLastSentRawMessage(): ?RawMessage + { + $messages = $this->getMessageMailerEvents()->getMessages(); + + return $messages ? $messages[array_key_last($messages)] : null; + } + protected function getMessageMailerEvents(): MessageEvents { $logger = $this->grabCachedService( diff --git a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php index 220c25b7..fbda1fe8 100644 --- a/src/Codeception/Module/Symfony/MimeAssertionsTrait.php +++ b/src/Codeception/Module/Symfony/MimeAssertionsTrait.php @@ -7,6 +7,8 @@ use PHPUnit\Framework\Assert; use PHPUnit\Framework\Constraint\LogicalNot; use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; use Symfony\Component\Mime\Test\Constraint as MimeConstraint; use function sprintf; @@ -14,24 +16,23 @@ trait MimeAssertionsTrait { /** - * Verify that an email contains addresses with a [header](https://datatracker.ietf.org/doc/html/rfc4021) + * Verify that a message contains addresses with a [header](https://datatracker.ietf.org/doc/html/rfc4021) * `$headerName` and its expected value `$expectedValue`. - * If the Email object is not specified, the last email sent is used instead. + * If no Message is specified, the last sent message is used instead. * * ```php * assertEmailAddressContains('To', 'jane_doe@example.com'); * ``` */ - public function assertEmailAddressContains(string $headerName, string $expectedValue, ?Email $email = null): void + public function assertEmailAddressContains(string $headerName, string $expectedValue, ?Message $email = null): void { - $email = $this->verifyEmailObject($email, __FUNCTION__); - $this->assertThat($email, new MimeConstraint\EmailAddressContains($headerName, $expectedValue)); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailAddressContains($headerName, $expectedValue)); } /** - * Verify that an email has sent the specified number `$count` of attachments. - * If the Email object is not specified, the last email sent is used instead. + * Verify that an email has the specified number `$count` of attachments. + * If no Email is specified, the last sent email is used instead. * * ```php * verifyEmailObject($email, __FUNCTION__); - $this->assertThat($email, new MimeConstraint\EmailAttachmentCount($count)); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailAttachmentCount($count)); } /** - * Verify that an email has a [header](https://datatracker.ietf.org/doc/html/rfc4021) `$headerName`. - * If the Email object is not specified, the last email sent is used instead. + * Verify that a message has a [header](https://datatracker.ietf.org/doc/html/rfc4021) `$headerName`. + * If no Message is specified, the last sent message is used instead. * * ```php * assertEmailHasHeader('Bcc'); * ``` */ - public function assertEmailHasHeader(string $headerName, ?Email $email = null): void + public function assertEmailHasHeader(string $headerName, ?Message $email = null): void { - $email = $this->verifyEmailObject($email, __FUNCTION__); - $this->assertThat($email, new MimeConstraint\EmailHasHeader($headerName)); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailHasHeader($headerName)); } /** * Verify that the [header](https://datatracker.ietf.org/doc/html/rfc4021) - * `$headerName` of an email is not the expected one `$expectedValue`. - * If the Email object is not specified, the last email sent is used instead. + * `$headerName` of a message is not the expected one `$expectedValue`. + * If no Message is specified, the last sent message is used instead. * * ```php * assertEmailHeaderNotSame('To', 'john_doe@gmail.com'); * ``` */ - public function assertEmailHeaderNotSame(string $headerName, string $expectedValue, ?Email $email = null): void + public function assertEmailHeaderNotSame(string $headerName, string $expectedValue, ?Message $email = null): void { - $email = $this->verifyEmailObject($email, __FUNCTION__); - $this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHeaderSame($headerName, $expectedValue))); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailHeaderSame($headerName, $expectedValue))); } /** * Verify that the [header](https://datatracker.ietf.org/doc/html/rfc4021) - * `$headerName` of an email is the same as expected `$expectedValue`. - * If the Email object is not specified, the last email sent is used instead. + * `$headerName` of a message is the same as expected `$expectedValue`. + * If no Message is specified, the last sent message is used instead. * * ```php * assertEmailHeaderSame('To', 'jane_doe@gmail.com'); * ``` */ - public function assertEmailHeaderSame(string $headerName, string $expectedValue, ?Email $email = null): void + public function assertEmailHeaderSame(string $headerName, string $expectedValue, ?Message $email = null): void { - $email = $this->verifyEmailObject($email, __FUNCTION__); - $this->assertThat($email, new MimeConstraint\EmailHeaderSame($headerName, $expectedValue)); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailHeaderSame($headerName, $expectedValue)); } /** * Verify that the HTML body of an email contains `$text`. - * If the Email object is not specified, the last email sent is used instead. + * If no Email is specified, the last sent email is used instead. * * ```php * verifyEmailObject($email, __FUNCTION__); - $this->assertThat($email, new MimeConstraint\EmailHtmlBodyContains($text)); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailHtmlBodyContains($text)); } /** * Verify that the HTML body of an email does not contain a text `$text`. - * If the Email object is not specified, the last email sent is used instead. + * If no Email is specified, the last sent email is used instead. * * ```php * verifyEmailObject($email, __FUNCTION__); - $this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHtmlBodyContains($text))); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailHtmlBodyContains($text))); } /** - * Verify that an email does not have a [header](https://datatracker.ietf.org/doc/html/rfc4021) `$headerName`. - * If the Email object is not specified, the last email sent is used instead. + * Verify that a message does not have a [header](https://datatracker.ietf.org/doc/html/rfc4021) `$headerName`. + * If no Message is specified, the last sent message is used instead. * * ```php * assertEmailNotHasHeader('Bcc'); * ``` */ - public function assertEmailNotHasHeader(string $headerName, ?Email $email = null): void + public function assertEmailNotHasHeader(string $headerName, ?Message $email = null): void { - $email = $this->verifyEmailObject($email, __FUNCTION__); - $this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHasHeader($headerName))); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailHasHeader($headerName))); } /** * Verify the text body of an email contains a `$text`. - * If the Email object is not specified, the last email sent is used instead. + * If no Email is specified, the last sent email is used instead. * * ```php * verifyEmailObject($email, __FUNCTION__); - $this->assertThat($email, new MimeConstraint\EmailTextBodyContains($text)); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailTextBodyContains($text)); } /** * Verify that the text body of an email does not contain a `$text`. - * If the Email object is not specified, the last email sent is used instead. + * If no Email is specified, the last sent email is used instead. * * ```php * verifyEmailObject($email, __FUNCTION__); - $this->assertThat($email, new LogicalNot(new MimeConstraint\EmailTextBodyContains($text))); + $this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailTextBodyContains($text))); } /** - * Returns the last email sent if $email is null. If no email has been sent it fails. + * Resolves a Message for assertion or fails the test. + * + * Uses the provided `$message` or retrieves the last sent message. + * Fails if no message is found, or if it is a plain RawMessage lacking the headers and structure required by Mime constraints. */ - private function verifyEmailObject(?Email $email, string $function): Email + private function getMessageOrFail(?Message $message, string $caller): Message { - $email = $email ?: $this->grabLastSentEmail(); - $errorMsgTemplate = "There is no email to verify. An Email object was not specified when invoking '%s' and the application has not sent one."; - return $email ?? Assert::fail( - sprintf($errorMsgTemplate, $function) - ); + $message ??= $this->grabLastSentRawMessage(); + + if (!$message instanceof Message) { + Assert::fail(sprintf("No message to verify for '%s'. None was provided or sent by the application.", $caller)); + } + + return $message; } } diff --git a/tests/MailerAssertionsTest.php b/tests/MailerAssertionsTest.php index 4f8609fb..ed9c61e4 100644 --- a/tests/MailerAssertionsTest.php +++ b/tests/MailerAssertionsTest.php @@ -9,6 +9,7 @@ use Symfony\Component\Mailer\Event\MessageEvent; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Message; use Tests\Support\CodeceptTestCase; final class MailerAssertionsTest extends CodeceptTestCase @@ -27,6 +28,12 @@ public function testAssertEmailCount(): void $this->assertEmailCount(1); } + public function testAssertEmailCountWithMessage(): void + { + $this->client->request('GET', '/send-message'); + $this->assertEmailCount(1); + } + public function testAssertEmailIsNotQueued(): void { $this->client->request('GET', '/send-email'); @@ -58,18 +65,40 @@ public function testGetMailerEvent(): void $this->assertInstanceOf(MessageEvent::class, $this->getMailerEvent()); } - public function testGrabLastSentEmail(): void + public function testGrabLastSentEmailReturnsEmailInstance(): void { $this->client->request('GET', '/send-email'); $email = $this->grabLastSentEmail(); $this->assertInstanceOf(Email::class, $email); - $this->assertSame('jane_doe@example.com', $email->getTo()[0]->getAddress()); } - public function testGrabSentEmails(): void + public function testGrabLastSentEmailReturnsMessageInstance(): void + { + $this->client->request('GET', '/send-message'); + $message = $this->grabLastSentEmail(); + $this->assertInstanceOf(Message::class, $message); + $this->assertNotInstanceOf(Email::class, $message); + } + + public function testGrabLastSentEmailReturnsNullWhenNoMessagesSent(): void + { + $this->assertNull($this->grabLastSentEmail()); + } + + public function testGrabSentEmailsWithEmailType(): void { $this->client->request('GET', '/send-email'); - $this->assertCount(1, $this->grabSentEmails()); + $emails = $this->grabSentEmails(); + $this->assertCount(1, $emails); + $this->assertInstanceOf(Email::class, $emails[0]); + } + + public function testGrabSentEmailsWithMessageType(): void + { + $this->client->request('GET', '/send-message'); + $messages = $this->grabSentEmails(); + $this->assertCount(1, $messages); + $this->assertInstanceOf(Message::class, $messages[0]); } public function testSeeEmailIsSent(): void @@ -80,10 +109,8 @@ public function testSeeEmailIsSent(): void public function testEdgeCases(): void { - // No emails sent $this->assertNull($this->grabLastSentEmail()); - // Out of range index $this->client->request('GET', '/send-email'); $this->assertNull($this->getMailerEvent(999)); } diff --git a/tests/MimeAssertionsTest.php b/tests/MimeAssertionsTest.php index 840a12d0..f73d1295 100644 --- a/tests/MimeAssertionsTest.php +++ b/tests/MimeAssertionsTest.php @@ -6,7 +6,11 @@ use Codeception\Module\Symfony\MailerAssertionsTrait; use Codeception\Module\Symfony\MimeAssertionsTrait; +use PHPUnit\Framework\AssertionFailedError; use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\Part\TextPart; use Tests\Support\CodeceptTestCase; final class MimeAssertionsTest extends CodeceptTestCase @@ -79,4 +83,44 @@ public function testAssertionsWorkWithProvidedEmail(): void $this->assertEmailTextBodyContains('Custom body text', $email); $this->assertEmailNotHasHeader('Cc', $email); } + + public function testHeaderAssertionsWorkWithSentMessage(): void + { + $this->getService('mailer.message_logger_listener')->reset(); + $this->client->request('GET', '/send-message'); + + $this->assertEmailHasHeader('To'); + $this->assertEmailHasHeader('From'); + $this->assertEmailHasHeader('Subject'); + $this->assertEmailAddressContains('To', 'jane_doe@example.com'); + $this->assertEmailHeaderSame('Subject', 'Test message'); + $this->assertEmailHeaderNotSame('Subject', 'Wrong subject'); + $this->assertEmailNotHasHeader('Bcc'); + } + + public function testHeaderAssertionsWithProvidedMessage(): void + { + $headers = new Headers(); + $headers->addMailboxListHeader('From', ['sender@example.com']); + $headers->addMailboxListHeader('To', ['recipient@example.com']); + $headers->addTextHeader('Subject', 'Test subject'); + + $message = new Message($headers, new TextPart('body content')); + + $this->assertEmailHasHeader('To', $message); + $this->assertEmailAddressContains('To', 'recipient@example.com', $message); + $this->assertEmailHeaderSame('Subject', 'Test subject', $message); + $this->assertEmailHeaderNotSame('Subject', 'Wrong subject', $message); + $this->assertEmailNotHasHeader('Bcc', $message); + } + + public function testFailsWhenNoMessageSent(): void + { + $this->getService('mailer.message_logger_listener')->reset(); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('No message to verify for'); + + $this->assertEmailHasHeader('To'); + } } diff --git a/tests/_app/Controller/AppController.php b/tests/_app/Controller/AppController.php index 5d467ebe..2a980b7c 100644 --- a/tests/_app/Controller/AppController.php +++ b/tests/_app/Controller/AppController.php @@ -21,6 +21,7 @@ use Symfony\Component\Validator\Constraints\NotBlank; use Symfony\Contracts\HttpClient\HttpClientInterface; use Tests\App\Event\TestEvent; +use Tests\App\Mailer\MessageMailer; use Tests\App\Mailer\RegistrationMailer; use Twig\Environment; @@ -175,6 +176,13 @@ public function sendEmail(RegistrationMailer $mailer): Response return new Response('Email sent'); } + public function sendMessage(MessageMailer $mailer): Response + { + $mailer->send('jane_doe@example.com'); + + return new Response('Message sent'); + } + public function testPage(Environment $twig): Response { return new Response($twig->render('test_page.html.twig')); diff --git a/tests/_app/Mailer/MessageMailer.php b/tests/_app/Mailer/MessageMailer.php new file mode 100644 index 00000000..2588d5bc --- /dev/null +++ b/tests/_app/Mailer/MessageMailer.php @@ -0,0 +1,31 @@ +addMailboxListHeader('From', ['no-reply@example.com']); + $headers->addMailboxListHeader('To', [$recipient]); + $headers->addTextHeader('Subject', 'Test message'); + + $this->mailer->send(new Message($headers, new TextPart('Message body content'))); + } +} diff --git a/tests/_app/config/routes.php b/tests/_app/config/routes.php index 1a6c8fd6..355a0c08 100644 --- a/tests/_app/config/routes.php +++ b/tests/_app/config/routes.php @@ -23,6 +23,7 @@ $routes->add('response_json', '/response_json')->controller(AppController::class . '::responseJsonFormat'); $routes->add('sample', '/sample')->controller(AppController::class . '::sample'); $routes->add('send_email', '/send-email')->controller(AppController::class . '::sendEmail'); + $routes->add('send_message', '/send-message')->controller(AppController::class . '::sendMessage'); $routes->add('test_page', '/test_page')->controller(AppController::class . '::testPage'); $routes->add('unprocessable_entity', '/unprocessable_entity')->controller(AppController::class . '::unprocessableEntity'); }; diff --git a/tests/_app/config/services.php b/tests/_app/config/services.php index 843504dd..5cae7a1a 100644 --- a/tests/_app/config/services.php +++ b/tests/_app/config/services.php @@ -23,6 +23,7 @@ use Tests\App\HttpClient\MockResponseFactory; use Tests\App\Listener\TestEventListener; use Tests\App\Logger\ArrayLogger; +use Tests\App\Mailer\MessageMailer; use Tests\App\Mailer\RegistrationMailer; use Tests\App\Notifier\NotifierFixture; use Tests\App\Repository\UserRepository; @@ -67,6 +68,7 @@ $services->alias('notifier.logger_notification_listener', 'notifier.notification_logger_listener')->public(); $services->set(RegistrationMailer::class)->arg('$mailer', service('mailer')); + $services->set(MessageMailer::class)->arg('$mailer', service('mailer')); $services->set(NotifierFixture::class)->arg('$dispatcher', service('event_dispatcher')); $services->set(TestEventListener::class)