Skip to content

Commit e8ab243

Browse files
committed
Enhance mailer assertions to support Message objects
1 parent 90aad3a commit e8ab243

8 files changed

Lines changed: 168 additions & 55 deletions

File tree

src/Codeception/Module/Symfony/MailerAssertionsTrait.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,23 +88,21 @@ public function dontSeeEmailIsSent(): void
8888
}
8989

9090
/**
91-
* Returns the last sent email.
91+
* Returns the last sent message or `null` if no message was sent.
92+
* The return type is `RawMessage`, which covers both `Email` and `Message` objects.
9293
* The function is based on `\Symfony\Component\Mailer\EventListener\MessageLoggerListener`, which means:
9394
* If your app performs an HTTP redirect after sending the email, you need to suppress it using [stopFollowingRedirects()](#stopFollowingRedirects) first.
9495
* See also: [grabSentEmails()](https://codeception.com/docs/modules/Symfony#grabSentEmails)
9596
*
9697
* ```php
9798
* <?php
9899
* $email = $I->grabLastSentEmail();
99-
* $address = $email->getTo()[0];
100-
* $I->assertSame('john_doe@example.com', $address->getAddress());
100+
* $I->assertEmailHasHeader('To', $email);
101101
* ```
102102
*/
103103
public function grabLastSentEmail(): ?RawMessage
104104
{
105-
$emails = $this->getMessageMailerEvents()->getMessages();
106-
107-
return $emails ? $emails[array_key_last($emails)] : null;
105+
return $this->grabLastSentRawMessage();
108106
}
109107

110108
/**
@@ -160,6 +158,13 @@ public function getMailerEvent(int $index = 0, ?string $transport = null): ?Mess
160158
return $this->getMessageMailerEvents()->getEvents($transport)[$index] ?? null;
161159
}
162160

161+
protected function grabLastSentRawMessage(): ?RawMessage
162+
{
163+
$messages = $this->getMessageMailerEvents()->getMessages();
164+
165+
return $messages ? $messages[array_key_last($messages)] : null;
166+
}
167+
163168
protected function getMessageMailerEvents(): MessageEvents
164169
{
165170
$logger = $this->grabCachedService(

src/Codeception/Module/Symfony/MimeAssertionsTrait.php

Lines changed: 38 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
trait MimeAssertionsTrait
1717
{
1818
/**
19-
* Verify that an email contains addresses with a [header](https://datatracker.ietf.org/doc/html/rfc4021)
19+
* Verify that a message contains addresses with a [header](https://datatracker.ietf.org/doc/html/rfc4021)
2020
* `$headerName` and its expected value `$expectedValue`.
21-
* If the Email object is not specified, the last email sent is used instead.
21+
* If no Message is specified, the last sent message is used instead.
2222
*
2323
* ```php
2424
* <?php
@@ -27,13 +27,12 @@ trait MimeAssertionsTrait
2727
*/
2828
public function assertEmailAddressContains(string $headerName, string $expectedValue, ?Message $email = null): void
2929
{
30-
$email = $this->verifyEmailObject($email, __FUNCTION__);
31-
$this->assertThat($email, new MimeConstraint\EmailAddressContains($headerName, $expectedValue));
30+
$this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailAddressContains($headerName, $expectedValue));
3231
}
3332

3433
/**
35-
* Verify that an email has sent the specified number `$count` of attachments.
36-
* If the Email object is not specified, the last email sent is used instead.
34+
* Verify that an email has the specified number `$count` of attachments.
35+
* If no Email is specified, the last sent email is used instead.
3736
*
3837
* ```php
3938
* <?php
@@ -42,13 +41,12 @@ public function assertEmailAddressContains(string $headerName, string $expectedV
4241
*/
4342
public function assertEmailAttachmentCount(int $count, ?Email $email = null): void
4443
{
45-
$email = $this->verifyEmailObject($email, __FUNCTION__);
46-
$this->assertThat($email, new MimeConstraint\EmailAttachmentCount($count));
44+
$this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailAttachmentCount($count));
4745
}
4846

4947
/**
50-
* Verify that an email has a [header](https://datatracker.ietf.org/doc/html/rfc4021) `$headerName`.
51-
* If the Email object is not specified, the last email sent is used instead.
48+
* Verify that a message has a [header](https://datatracker.ietf.org/doc/html/rfc4021) `$headerName`.
49+
* If no Message is specified, the last sent message is used instead.
5250
*
5351
* ```php
5452
* <?php
@@ -57,14 +55,13 @@ public function assertEmailAttachmentCount(int $count, ?Email $email = null): vo
5755
*/
5856
public function assertEmailHasHeader(string $headerName, ?Message $email = null): void
5957
{
60-
$email = $this->verifyEmailObject($email, __FUNCTION__);
61-
$this->assertThat($email, new MimeConstraint\EmailHasHeader($headerName));
58+
$this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailHasHeader($headerName));
6259
}
6360

6461
/**
6562
* Verify that the [header](https://datatracker.ietf.org/doc/html/rfc4021)
66-
* `$headerName` of an email is not the expected one `$expectedValue`.
67-
* If the Email object is not specified, the last email sent is used instead.
63+
* `$headerName` of a message is not the expected one `$expectedValue`.
64+
* If no Message is specified, the last sent message is used instead.
6865
*
6966
* ```php
7067
* <?php
@@ -73,14 +70,13 @@ public function assertEmailHasHeader(string $headerName, ?Message $email = null)
7370
*/
7471
public function assertEmailHeaderNotSame(string $headerName, string $expectedValue, ?Message $email = null): void
7572
{
76-
$email = $this->verifyEmailObject($email, __FUNCTION__);
77-
$this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHeaderSame($headerName, $expectedValue)));
73+
$this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailHeaderSame($headerName, $expectedValue)));
7874
}
7975

8076
/**
8177
* Verify that the [header](https://datatracker.ietf.org/doc/html/rfc4021)
82-
* `$headerName` of an email is the same as expected `$expectedValue`.
83-
* If the Email object is not specified, the last email sent is used instead.
78+
* `$headerName` of a message is the same as expected `$expectedValue`.
79+
* If no Message is specified, the last sent message is used instead.
8480
*
8581
* ```php
8682
* <?php
@@ -89,13 +85,12 @@ public function assertEmailHeaderNotSame(string $headerName, string $expectedVal
8985
*/
9086
public function assertEmailHeaderSame(string $headerName, string $expectedValue, ?Message $email = null): void
9187
{
92-
$email = $this->verifyEmailObject($email, __FUNCTION__);
93-
$this->assertThat($email, new MimeConstraint\EmailHeaderSame($headerName, $expectedValue));
88+
$this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailHeaderSame($headerName, $expectedValue));
9489
}
9590

9691
/**
9792
* Verify that the HTML body of an email contains `$text`.
98-
* If the Email object is not specified, the last email sent is used instead.
93+
* If no Email is specified, the last sent email is used instead.
9994
*
10095
* ```php
10196
* <?php
@@ -104,13 +99,12 @@ public function assertEmailHeaderSame(string $headerName, string $expectedValue,
10499
*/
105100
public function assertEmailHtmlBodyContains(string $text, ?Email $email = null): void
106101
{
107-
$email = $this->verifyEmailObject($email, __FUNCTION__);
108-
$this->assertThat($email, new MimeConstraint\EmailHtmlBodyContains($text));
102+
$this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailHtmlBodyContains($text));
109103
}
110104

111105
/**
112106
* Verify that the HTML body of an email does not contain a text `$text`.
113-
* If the Email object is not specified, the last email sent is used instead.
107+
* If no Email is specified, the last sent email is used instead.
114108
*
115109
* ```php
116110
* <?php
@@ -119,13 +113,12 @@ public function assertEmailHtmlBodyContains(string $text, ?Email $email = null):
119113
*/
120114
public function assertEmailHtmlBodyNotContains(string $text, ?Email $email = null): void
121115
{
122-
$email = $this->verifyEmailObject($email, __FUNCTION__);
123-
$this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHtmlBodyContains($text)));
116+
$this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailHtmlBodyContains($text)));
124117
}
125118

126119
/**
127-
* Verify that an email does not have a [header](https://datatracker.ietf.org/doc/html/rfc4021) `$headerName`.
128-
* If the Email object is not specified, the last email sent is used instead.
120+
* Verify that a message does not have a [header](https://datatracker.ietf.org/doc/html/rfc4021) `$headerName`.
121+
* If no Message is specified, the last sent message is used instead.
129122
*
130123
* ```php
131124
* <?php
@@ -134,13 +127,12 @@ public function assertEmailHtmlBodyNotContains(string $text, ?Email $email = nul
134127
*/
135128
public function assertEmailNotHasHeader(string $headerName, ?Message $email = null): void
136129
{
137-
$email = $this->verifyEmailObject($email, __FUNCTION__);
138-
$this->assertThat($email, new LogicalNot(new MimeConstraint\EmailHasHeader($headerName)));
130+
$this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailHasHeader($headerName)));
139131
}
140132

141133
/**
142134
* Verify the text body of an email contains a `$text`.
143-
* If the Email object is not specified, the last email sent is used instead.
135+
* If no Email is specified, the last sent email is used instead.
144136
*
145137
* ```php
146138
* <?php
@@ -149,13 +141,12 @@ public function assertEmailNotHasHeader(string $headerName, ?Message $email = nu
149141
*/
150142
public function assertEmailTextBodyContains(string $text, ?Email $email = null): void
151143
{
152-
$email = $this->verifyEmailObject($email, __FUNCTION__);
153-
$this->assertThat($email, new MimeConstraint\EmailTextBodyContains($text));
144+
$this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new MimeConstraint\EmailTextBodyContains($text));
154145
}
155146

156147
/**
157148
* Verify that the text body of an email does not contain a `$text`.
158-
* If the Email object is not specified, the last email sent is used instead.
149+
* If no Email is specified, the last sent email is used instead.
159150
*
160151
* ```php
161152
* <?php
@@ -164,19 +155,23 @@ public function assertEmailTextBodyContains(string $text, ?Email $email = null):
164155
*/
165156
public function assertEmailTextBodyNotContains(string $text, ?Email $email = null): void
166157
{
167-
$email = $this->verifyEmailObject($email, __FUNCTION__);
168-
$this->assertThat($email, new LogicalNot(new MimeConstraint\EmailTextBodyContains($text)));
158+
$this->assertThat($this->getMessageOrFail($email, __FUNCTION__), new LogicalNot(new MimeConstraint\EmailTextBodyContains($text)));
169159
}
170160

171161
/**
172-
* Returns the last email sent if $email is null. If no email has been sent it fails.
162+
* Resolves a Message for assertion or fails the test.
163+
*
164+
* Uses the provided `$message` or retrieves the last sent message.
165+
* Fails if no message is found, or if it is a plain RawMessage lacking the headers and structure required by Mime constraints.
173166
*/
174-
private function verifyEmailObject(?RawMessage $email, string $function): RawMessage
167+
private function getMessageOrFail(?Message $message, string $caller): Message
175168
{
176-
$email = $email ?: $this->grabLastSentEmail();
177-
$errorMsgTemplate = "There is no email to verify. An Email object was not specified when invoking '%s' and the application has not sent one.";
178-
return $email ?? Assert::fail(
179-
sprintf($errorMsgTemplate, $function)
180-
);
169+
$message ??= $this->grabLastSentRawMessage();
170+
171+
if (!$message instanceof Message) {
172+
Assert::fail(sprintf("No message to verify for '%s'. None was provided or sent by the application.", $caller));
173+
}
174+
175+
return $message;
181176
}
182177
}

tests/MailerAssertionsTest.php

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Symfony\Component\Mailer\Event\MessageEvent;
1010
use Symfony\Component\Mime\Address;
1111
use Symfony\Component\Mime\Email;
12+
use Symfony\Component\Mime\Message;
1213
use Tests\Support\CodeceptTestCase;
1314

1415
final class MailerAssertionsTest extends CodeceptTestCase
@@ -27,6 +28,12 @@ public function testAssertEmailCount(): void
2728
$this->assertEmailCount(1);
2829
}
2930

31+
public function testAssertEmailCountWithMessage(): void
32+
{
33+
$this->client->request('GET', '/send-message');
34+
$this->assertEmailCount(1);
35+
}
36+
3037
public function testAssertEmailIsNotQueued(): void
3138
{
3239
$this->client->request('GET', '/send-email');
@@ -58,18 +65,40 @@ public function testGetMailerEvent(): void
5865
$this->assertInstanceOf(MessageEvent::class, $this->getMailerEvent());
5966
}
6067

61-
public function testGrabLastSentEmail(): void
68+
public function testGrabLastSentEmailReturnsEmailInstance(): void
6269
{
6370
$this->client->request('GET', '/send-email');
6471
$email = $this->grabLastSentEmail();
6572
$this->assertInstanceOf(Email::class, $email);
66-
$this->assertSame('jane_doe@example.com', $email->getTo()[0]->getAddress());
6773
}
6874

69-
public function testGrabSentEmails(): void
75+
public function testGrabLastSentEmailReturnsMessageInstance(): void
76+
{
77+
$this->client->request('GET', '/send-message');
78+
$message = $this->grabLastSentEmail();
79+
$this->assertInstanceOf(Message::class, $message);
80+
$this->assertNotInstanceOf(Email::class, $message);
81+
}
82+
83+
public function testGrabLastSentEmailReturnsNullWhenNoMessagesSent(): void
84+
{
85+
$this->assertNull($this->grabLastSentEmail());
86+
}
87+
88+
public function testGrabSentEmailsWithEmailType(): void
7089
{
7190
$this->client->request('GET', '/send-email');
72-
$this->assertCount(1, $this->grabSentEmails());
91+
$emails = $this->grabSentEmails();
92+
$this->assertCount(1, $emails);
93+
$this->assertInstanceOf(Email::class, $emails[0]);
94+
}
95+
96+
public function testGrabSentEmailsWithMessageType(): void
97+
{
98+
$this->client->request('GET', '/send-message');
99+
$messages = $this->grabSentEmails();
100+
$this->assertCount(1, $messages);
101+
$this->assertInstanceOf(Message::class, $messages[0]);
73102
}
74103

75104
public function testSeeEmailIsSent(): void
@@ -80,10 +109,8 @@ public function testSeeEmailIsSent(): void
80109

81110
public function testEdgeCases(): void
82111
{
83-
// No emails sent
84112
$this->assertNull($this->grabLastSentEmail());
85113

86-
// Out of range index
87114
$this->client->request('GET', '/send-email');
88115
$this->assertNull($this->getMailerEvent(999));
89116
}

tests/MimeAssertionsTest.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66

77
use Codeception\Module\Symfony\MailerAssertionsTrait;
88
use Codeception\Module\Symfony\MimeAssertionsTrait;
9+
use PHPUnit\Framework\AssertionFailedError;
910
use Symfony\Component\Mime\Email;
11+
use Symfony\Component\Mime\Header\Headers;
12+
use Symfony\Component\Mime\Message;
13+
use Symfony\Component\Mime\Part\TextPart;
1014
use Tests\Support\CodeceptTestCase;
1115

1216
final class MimeAssertionsTest extends CodeceptTestCase
@@ -79,4 +83,44 @@ public function testAssertionsWorkWithProvidedEmail(): void
7983
$this->assertEmailTextBodyContains('Custom body text', $email);
8084
$this->assertEmailNotHasHeader('Cc', $email);
8185
}
86+
87+
public function testHeaderAssertionsWorkWithSentMessage(): void
88+
{
89+
$this->getService('mailer.message_logger_listener')->reset();
90+
$this->client->request('GET', '/send-message');
91+
92+
$this->assertEmailHasHeader('To');
93+
$this->assertEmailHasHeader('From');
94+
$this->assertEmailHasHeader('Subject');
95+
$this->assertEmailAddressContains('To', 'jane_doe@example.com');
96+
$this->assertEmailHeaderSame('Subject', 'Test message');
97+
$this->assertEmailHeaderNotSame('Subject', 'Wrong subject');
98+
$this->assertEmailNotHasHeader('Bcc');
99+
}
100+
101+
public function testHeaderAssertionsWithProvidedMessage(): void
102+
{
103+
$headers = new Headers();
104+
$headers->addMailboxListHeader('From', ['sender@example.com']);
105+
$headers->addMailboxListHeader('To', ['recipient@example.com']);
106+
$headers->addTextHeader('Subject', 'Test subject');
107+
108+
$message = new Message($headers, new TextPart('body content'));
109+
110+
$this->assertEmailHasHeader('To', $message);
111+
$this->assertEmailAddressContains('To', 'recipient@example.com', $message);
112+
$this->assertEmailHeaderSame('Subject', 'Test subject', $message);
113+
$this->assertEmailHeaderNotSame('Subject', 'Wrong subject', $message);
114+
$this->assertEmailNotHasHeader('Bcc', $message);
115+
}
116+
117+
public function testFailsWhenNoMessageSent(): void
118+
{
119+
$this->getService('mailer.message_logger_listener')->reset();
120+
121+
$this->expectException(AssertionFailedError::class);
122+
$this->expectExceptionMessage('No message to verify for');
123+
124+
$this->assertEmailHasHeader('To');
125+
}
82126
}

tests/_app/Controller/AppController.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Symfony\Component\Validator\Constraints\NotBlank;
2222
use Symfony\Contracts\HttpClient\HttpClientInterface;
2323
use Tests\App\Event\TestEvent;
24+
use Tests\App\Mailer\MessageMailer;
2425
use Tests\App\Mailer\RegistrationMailer;
2526
use Twig\Environment;
2627

@@ -175,6 +176,13 @@ public function sendEmail(RegistrationMailer $mailer): Response
175176
return new Response('Email sent');
176177
}
177178

179+
public function sendMessage(MessageMailer $mailer): Response
180+
{
181+
$mailer->send('jane_doe@example.com');
182+
183+
return new Response('Message sent');
184+
}
185+
178186
public function testPage(Environment $twig): Response
179187
{
180188
return new Response($twig->render('test_page.html.twig'));

0 commit comments

Comments
 (0)