diff --git a/README.md b/README.md index 6a4078f..3b02dc5 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,60 @@ Example: * Param $email * Param $regex +### seeInLastEmailSender / seeInLastEmailRecipient + +Checks that the sender (or one of the recipients) of the last email contains a value. + +Example: + + seeInLastEmailSender('noreply@example.com'); + $I->seeInLastEmailRecipient('user@example.com'); + ?> + +* Param $text + +### Nth email actions + +Every "last email" action has an "nth email" counterpart that takes the +position as its first argument. Positions start at 1 and count in the order +the emails were received, so `1` is the first email and `2` the second. +For the `...To` variants, only emails sent to the given address are counted. +Like the existing `...To` actions, addresses match by substring, so +`user@example.com` also matches `superuser@example.com`. + +| Last email | Nth email | +| --- | --- | +| `seeInLastEmail($text)` | `seeInNthEmail($nth, $text)` | +| `dontSeeInLastEmail($text)` | `dontSeeInNthEmail($nth, $text)` | +| `seeInLastEmailSubject($text)` | `seeInNthEmailSubject($nth, $text)` | +| `dontSeeInLastEmailSubject($text)` | `dontSeeInNthEmailSubject($nth, $text)` | +| `seeInLastEmailSender($text)` | `seeInNthEmailSender($nth, $text)` | +| `seeInLastEmailRecipient($text)` | `seeInNthEmailRecipient($nth, $text)` | +| `seeInLastEmailTo($email, $text)` | `seeInNthEmailTo($nth, $email, $text)` | +| `dontSeeInLastEmailTo($email, $text)` | `dontSeeInNthEmailTo($nth, $email, $text)` | +| `seeInLastEmailSubjectTo($email, $text)` | `seeInNthEmailSubjectTo($nth, $email, $text)` | +| `dontSeeInLastEmailSubjectTo($email, $text)` | `dontSeeInNthEmailSubjectTo($nth, $email, $text)` | +| `grabMatchesFromLastEmail($regex)` | `grabMatchesFromNthEmail($nth, $regex)` | +| `grabFromLastEmail($regex)` | `grabFromNthEmail($nth, $regex)` | +| `grabMatchesFromLastEmailTo($email, $regex)` | `grabMatchesFromNthEmailTo($nth, $email, $regex)` | +| `grabFromLastEmailTo($email, $regex)` | `grabFromNthEmailTo($nth, $email, $regex)` | +| `grabUrlsFromLastEmail()` | `grabUrlsFromNthEmail($nth)` | +| `grabAttachmentsFromLastEmail()` | `grabAttachmentsFromNthEmail($nth)` | +| `seeAttachmentInLastEmail($filename)` | `seeAttachmentInNthEmail($nth, $filename)` | +| `seeEmailAttachmentCount($count)` | `seeNthEmailAttachmentCount($nth, $count)` | +| `lastMessage()` | `nthMessage($nth)` | +| `lastMessageTo($email)` | `nthMessageTo($nth, $email)` | + +Example: + + seeInNthEmail(1, 'Welcome!'); + $I->seeInNthEmailSubject(2, 'Please confirm your address'); + $I->seeInNthEmailTo(2, 'admin@example.com', 'A new user has signed up'); + ?> + ### seeEmailCount Asserts that a certain number of emails have been sent since the last time diff --git a/src/Module/MailCatcher.php b/src/Module/MailCatcher.php index 64a657f..02167e6 100644 --- a/src/Module/MailCatcher.php +++ b/src/Module/MailCatcher.php @@ -60,6 +60,11 @@ public function seeInLastEmail(string $expected): void $this->seeInEmail($email, $expected); } + public function seeInNthEmail(int $nth, string $expected): void + { + $this->seeInEmail($this->nthMessage($nth), $expected); + } + /** * See In Last Email subject * @@ -73,6 +78,11 @@ public function seeInLastEmailSubject(string $expected): void $this->seeInEmailSubject($email, $expected); } + public function seeInNthEmailSubject(int $nth, string $expected): void + { + $this->seeInEmailSubject($this->nthMessage($nth), $expected); + } + /** * Don't See In Last Email subject * @@ -84,6 +94,11 @@ public function dontSeeInLastEmailSubject(string $expected): void $this->dontSeeInEmailSubject($email, $expected); } + public function dontSeeInNthEmailSubject(int $nth, string $unexpected): void + { + $this->dontSeeInEmailSubject($this->nthMessage($nth), $unexpected); + } + /** * Don't See In Last Email * @@ -95,6 +110,31 @@ public function dontSeeInLastEmail(string $unexpected): void $this->dontSeeInEmail($email, $unexpected); } + public function dontSeeInNthEmail(int $nth, string $unexpected): void + { + $this->dontSeeInEmail($this->nthMessage($nth), $unexpected); + } + + public function seeInLastEmailSender(string $expected): void + { + $this->seeInEmailSender($this->lastMessage(), $expected); + } + + public function seeInNthEmailSender(int $nth, string $expected): void + { + $this->seeInEmailSender($this->nthMessage($nth), $expected); + } + + public function seeInLastEmailRecipient(string $expected): void + { + $this->seeInEmailRecipients($this->lastMessage(), $expected); + } + + public function seeInNthEmailRecipient(int $nth, string $expected): void + { + $this->seeInEmailRecipients($this->nthMessage($nth), $expected); + } + /** * See In Last Email To * @@ -108,6 +148,11 @@ public function seeInLastEmailTo(string $address, string $expected): void $this->seeInEmail($email, $expected); } + public function seeInNthEmailTo(int $nth, string $address, string $expected): void + { + $this->seeInEmail($this->nthMessageTo($nth, $address), $expected); + } + /** * Don't See In Last Email To * @@ -119,6 +164,11 @@ public function dontSeeInLastEmailTo(string $address, string $unexpected): void $this->dontSeeInEmail($email, $unexpected); } + public function dontSeeInNthEmailTo(int $nth, string $address, string $unexpected): void + { + $this->dontSeeInEmail($this->nthMessageTo($nth, $address), $unexpected); + } + /** * See In Last Email Subject To * @@ -132,6 +182,11 @@ public function seeInLastEmailSubjectTo(string $address, string $expected): void $this->seeInEmailSubject($email, $expected); } + public function seeInNthEmailSubjectTo(int $nth, string $address, string $expected): void + { + $this->seeInEmailSubject($this->nthMessageTo($nth, $address), $expected); + } + /** * Don't See In Last Email Subject To * @@ -143,6 +198,11 @@ public function dontSeeInLastEmailSubjectTo(string $address, string $unexpected) $this->dontSeeInEmailSubject($email, $unexpected); } + public function dontSeeInNthEmailSubjectTo(int $nth, string $address, string $unexpected): void + { + $this->dontSeeInEmailSubject($this->nthMessageTo($nth, $address), $unexpected); + } + public function lastMessage(): \Codeception\Util\Email { $messages = $this->messages(); @@ -155,6 +215,11 @@ public function lastMessage(): \Codeception\Util\Email return $this->emailFromId($last['id']); } + public function nthMessage(int $nth): \Codeception\Util\Email + { + return $this->emailAtPosition($this->messagesInReceivedOrder(), $nth, "No message found at position {$nth}"); + } + public function lastMessageTo(string $address): \Codeception\Util\Email { $ids = []; @@ -164,10 +229,8 @@ public function lastMessageTo(string $address): \Codeception\Util\Email } foreach ($messages as $message) { - foreach ($message['recipients'] as $recipient) { - if (strpos($recipient, $address) !== false) { - $ids[] = $message['id']; - } + if ($this->isSentTo($message, $address)) { + $ids[] = $message['id']; } } @@ -178,6 +241,19 @@ public function lastMessageTo(string $address): \Codeception\Util\Email return $this->emailFromId(max($ids)); } + public function nthMessageTo(int $nth, string $address): \Codeception\Util\Email + { + $messages = array_filter( + $this->messagesInReceivedOrder(), + fn (array $message): bool => $this->isSentTo($message, $address) + ); + if (empty($messages)) { + $this->fail("No messages sent to {$address}"); + } + + return $this->emailAtPosition($messages, $nth, "No message found at position {$nth} sent to {$address}"); + } + public function lastMessageFrom(string $address): \Codeception\Util\Email { $ids = []; @@ -221,6 +297,14 @@ public function grabMatchesFromLastEmail(string $regex): array return $this->grabMatchesFromEmail($email, $regex); } + /** + * @return mixed[] + */ + public function grabMatchesFromNthEmail(int $nth, string $regex): array + { + return $this->grabMatchesFromEmail($this->nthMessage($nth), $regex); + } + /** * Grab From Last Email * @@ -234,6 +318,11 @@ public function grabFromLastEmail(string $regex): string return $matches[0]; } + public function grabFromNthEmail(int $nth, string $regex): string + { + return $this->grabMatchesFromNthEmail($nth, $regex)[0]; + } + /** * Grab Matches From Last Email To * @@ -249,6 +338,14 @@ public function grabMatchesFromLastEmailTo(string $address, string $regex): arra return $this->grabMatchesFromEmail($email, $regex); } + /** + * @return mixed[] + */ + public function grabMatchesFromNthEmailTo(int $nth, string $address, string $regex): array + { + return $this->grabMatchesFromEmail($this->nthMessageTo($nth, $address), $regex); + } + /** * Grab From Last Email To * @@ -263,6 +360,11 @@ public function grabFromLastEmailTo(string $address, string $regex): string return $matches[0]; } + public function grabFromNthEmailTo(int $nth, string $address, string $regex): string + { + return $this->grabMatchesFromNthEmailTo($nth, $address, $regex)[0]; + } + /** * Grab Urls From Email * @@ -273,18 +375,15 @@ public function grabFromLastEmailTo(string $address, string $regex): string */ public function grabUrlsFromLastEmail(): array { - $regex = '#\bhttps?://[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#'; - $email = $this->lastMessage(); - - $message = Message::from($email->getSource(), false); - - $text = $message->getTextContent(); - preg_match_all($regex, $text, $text_matches); - - $html = $message->getHtmlContent(); - preg_match_all($regex, $html, $html_matches); + return $this->grabUrlsFromEmail($this->lastMessage()); + } - return array_merge($text_matches[0], $html_matches[0]); + /** + * @return string[] + */ + public function grabUrlsFromNthEmail(int $nth): array + { + return $this->grabUrlsFromEmail($this->nthMessage($nth)); } /** @@ -297,19 +396,15 @@ public function grabUrlsFromLastEmail(): array */ public function grabAttachmentsFromLastEmail(): array { - $email = $this->lastMessage(); - - $message = Message::from($email->getSource(), false); - - $attachments = []; - - foreach ($message->getAllAttachmentParts() as $attachmentPart) { - $filename = $attachmentPart->getFilename(); - $content = $attachmentPart->getContent(); - $attachments[$filename] = $content; - } + return $this->grabAttachmentsFromEmail($this->lastMessage()); + } - return $attachments; + /** + * @return array + */ + public function grabAttachmentsFromNthEmail(int $nth): array + { + return $this->grabAttachmentsFromEmail($this->nthMessage($nth)); } /** @@ -321,15 +416,12 @@ public function grabAttachmentsFromLastEmail(): array **/ public function seeAttachmentInLastEmail(string $expectedFilename): void { - $email = $this->lastMessage(); - $message = Message::from($email->getSource(), false); + $this->seeAttachmentInEmail($this->lastMessage(), $expectedFilename); + } - foreach ($message->getAllAttachmentParts() as $attachmentPart) { - if ($attachmentPart->getFilename() === $expectedFilename) { - return; - } - } - $this->fail("Filename not found in attachments."); + public function seeAttachmentInNthEmail(int $nth, string $expectedFilename): void + { + $this->seeAttachmentInEmail($this->nthMessage($nth), $expectedFilename); } /** @@ -351,9 +443,12 @@ public function seeEmailCount(int $expected): void **/ public function seeEmailAttachmentCount(int $expectedCount): void { - $email = $this->lastMessage(); - $message = Message::from($email->getSource(), false); - $this->assertEquals($expectedCount, $message->getAttachmentCount()); + $this->seeAttachmentCountInEmail($this->lastMessage(), $expectedCount); + } + + public function seeNthEmailAttachmentCount(int $nth, int $expectedCount): void + { + $this->seeAttachmentCountInEmail($this->nthMessage($nth), $expectedCount); } // ----------- HELPER METHODS BELOW HERE -----------------------// @@ -370,14 +465,49 @@ protected function messages(): array $messages = json_decode($response->getBody(), true); // Ensure messages are shown in the order they were recieved // https://github.com/sj26/mailcatcher/pull/184 - usort($messages, function ($messageA, $messageB): int { - $sortKeyA = $messageA['created_at'] . $messageA['id']; - $sortKeyB = $messageB['created_at'] . $messageB['id']; - return ($sortKeyA > $sortKeyB) ? -1 : 1; - }); + usort( + $messages, + static fn (array $messageA, array $messageB): int + => [$messageB['created_at'], $messageB['id']] <=> [$messageA['created_at'], $messageA['id']] + ); return $messages; } + protected function messagesInReceivedOrder(): array + { + $messages = $this->messages(); + if (empty($messages)) { + $this->fail("No messages received"); + } + + return array_reverse($messages); + } + + protected function emailAtPosition(array $messages, int $nth, string $notFoundMessage): \Codeception\Util\Email + { + if ($nth < 1) { + $this->fail("Position must be 1 or greater, {$nth} given"); + } + + $message = array_values($messages)[$nth - 1] ?? null; + if ($message === null) { + $this->fail($notFoundMessage); + } + + return $this->emailFromId($message['id']); + } + + protected function isSentTo(array $message, string $address): bool + { + foreach ($message['recipients'] as $recipient) { + if (str_contains($recipient, $address)) { + return true; + } + } + + return false; + } + /** * @param int|string $id */ @@ -411,10 +541,71 @@ protected function dontSeeInEmail(Email $email, string $unexpected): void $this->assertStringNotContainsString($unexpected, $email->getSourceQuotedPrintableDecoded(), "Email Does Not Contain"); } + protected function seeInEmailSender(Email $email, string $expected): void + { + $this->assertStringContainsString($expected, $email->getSender(), "Email Sender Contains"); + } + + protected function seeInEmailRecipients(Email $email, string $expected): void + { + $recipients = $email->getRecipients(); + $matchingRecipients = array_filter( + $recipients, + static fn (string $recipient): bool => str_contains($recipient, $expected) + ); + + $this->assertNotEmpty( + $matchingRecipients, + sprintf('Failed asserting that one of the email recipients [%s] contains "%s".', implode(', ', $recipients), $expected) + ); + } + protected function grabMatchesFromEmail(Email $email, string $regex): array { preg_match($regex, $email->getSourceQuotedPrintableDecoded(), $matches); $this->assertNotEmpty($matches, "No matches found for $regex"); return $matches; } + + /** + * @return string[] + */ + protected function grabUrlsFromEmail(Email $email): array + { + $regex = '#\bhttps?://[^,\s()<>]+(?:\([\w\d]+\)|([^,[:punct:]\s]|/))#'; + $message = Message::from($email->getSource(), false); + + preg_match_all($regex, $message->getTextContent() ?? '', $textMatches); + preg_match_all($regex, $message->getHtmlContent() ?? '', $htmlMatches); + + return array_merge($textMatches[0], $htmlMatches[0]); + } + + /** + * @return array + */ + protected function grabAttachmentsFromEmail(Email $email): array + { + $attachments = []; + foreach (Message::from($email->getSource(), false)->getAllAttachmentParts() as $attachmentPart) { + $attachments[$attachmentPart->getFilename()] = $attachmentPart->getContent(); + } + + return $attachments; + } + + protected function seeAttachmentInEmail(Email $email, string $expectedFilename): void + { + foreach (Message::from($email->getSource(), false)->getAllAttachmentParts() as $attachmentPart) { + if ($attachmentPart->getFilename() === $expectedFilename) { + return; + } + } + $this->fail("Filename not found in attachments."); + } + + protected function seeAttachmentCountInEmail(Email $email, int $expectedCount): void + { + $this->assertEquals($expectedCount, Message::from($email->getSource(), false)->getAttachmentCount()); + } } diff --git a/src/Util/Email.php b/src/Util/Email.php index a9699db..70da9a3 100644 --- a/src/Util/Email.php +++ b/src/Util/Email.php @@ -4,35 +4,16 @@ class Email { - /** - * @var int - */ - private $id; - - /** - * @var string[] - */ - private $recipients; - - /** - * @var string - */ - private $subject; - - /** - * @var string - */ - private $source; - /** * @param string[] $recipients */ - public function __construct(int $id, array $recipients, ?string $subject, string $source) - { - $this->id = $id; - $this->recipients = $recipients; - $this->subject = $subject; - $this->source = $source; + public function __construct( + private readonly int $id, + private readonly array $recipients, + private readonly ?string $subject, + private readonly string $source, + private readonly string $sender = '', + ) { } public function getId(): int @@ -40,6 +21,11 @@ public function getId(): int return $this->id; } + public function getSender(): string + { + return $this->sender; + } + /** * @return string[] */ @@ -65,6 +51,6 @@ public function getSourceQuotedPrintableDecoded(): string public static function createFromMailcatcherData(array $data): \Codeception\Util\Email { - return new self($data['id'], $data['recipients'], $data['subject'], $data['source']); + return new self($data['id'], $data['recipients'], $data['subject'], $data['source'], $data['sender'] ?? ''); } -} \ No newline at end of file +} diff --git a/tests/_support/Helper/Acceptance.php b/tests/_support/Helper/Acceptance.php index 8a9275f..02d7a89 100644 --- a/tests/_support/Helper/Acceptance.php +++ b/tests/_support/Helper/Acceptance.php @@ -10,12 +10,13 @@ class Acceptance extends \Codeception\Module { public function sendEmail( - string $to, + string|array $to, string $subject, string $body, bool $isHtml = false, ?string $encoding = null, - array $attachments = [] + array $attachments = [], + string $from = 'sender@example.com' ): void { $phpmailer = new PHPMailer(); $phpmailer->isSMTP(); @@ -28,8 +29,10 @@ public function sendEmail( $phpmailer->addAttachment($attachment, $attachmentName); } - $phpmailer->addAddress($to); - $phpmailer->setFrom("sender@example.com"); + foreach ((array) $to as $address) { + $phpmailer->addAddress($address); + } + $phpmailer->setFrom($from); $phpmailer->Subject = $subject; $phpmailer->Body = $body; $phpmailer->isHTML($isHtml); diff --git a/tests/acceptance/MailcatcherCest.php b/tests/acceptance/MailcatcherCest.php index 67c6c74..aec5cf9 100644 --- a/tests/acceptance/MailcatcherCest.php +++ b/tests/acceptance/MailcatcherCest.php @@ -284,4 +284,116 @@ public function test_fail_attachment_count_in_mail(AcceptanceTester $I) $I->seeEmailAttachmentCount(3); }); } + + public function test_see_in_nth_email(AcceptanceTester $I) + { + $I->sendEmail('user@example.com', 'Subject Line', "First"); + $I->sendEmail('user@example.com', 'Subject Line', "Second"); + $I->sendEmail('user@example.com', 'Subject Line', "Third"); + $I->seeInNthEmail(2, "Second"); + $I->dontSeeInNthEmail(2, "Third"); + } + + public function test_see_in_nth_email_subject(AcceptanceTester $I) + { + $I->sendEmail('user@example.com', 'First Subject', "Hello World!"); + $I->sendEmail('user@example.com', 'Second Subject', "Hello World!"); + $I->sendEmail('user@example.com', 'Third Subject', "Hello World!"); + $I->seeInNthEmailSubject(2, 'Second Subject'); + $I->dontSeeInNthEmailSubject(2, 'Third Subject'); + } + + public function test_fail_nth_email_beyond_received_emails(AcceptanceTester $I) + { + $I->sendEmail('user@example.com', 'Subject Line', "Hello World!"); + $I->expectThrowable(new Exception("No message found at position 2"), function() use ($I) { + $I->seeInNthEmail(2, "Hello World!"); + }); + } + + public function test_see_in_email_sender(AcceptanceTester $I) + { + $I->sendEmail('user@example.com', 'Subject Line', "Hello World!", false, null, [], 'first@example.com'); + $I->sendEmail('user@example.com', 'Subject Line', "Hello World!", false, null, [], 'second@example.com'); + $I->sendEmail('user@example.com', 'Subject Line', "Hello World!", false, null, [], 'third@example.com'); + $I->seeInLastEmailSender('third@example.com'); + $I->seeInNthEmailSender(2, 'second@example.com'); + } + + public function test_see_in_email_recipient(AcceptanceTester $I) + { + $I->sendEmail('userA@example.com', 'Subject Line', "Hello World!"); + $I->sendEmail('userB@example.com', 'Subject Line', "Hello World!"); + $I->sendEmail('userC@example.com', 'Subject Line', "Hello World!"); + $I->seeInLastEmailRecipient('userC@example.com'); + $I->seeInNthEmailRecipient(2, 'userB@example.com'); + } + + public function test_see_in_email_recipient_checks_each_recipient(AcceptanceTester $I) + { + $I->sendEmail(['alice@example.com', 'bob@example.com'], 'Subject Line', "Hello World!"); + $I->seeInLastEmailRecipient('bob@example.com'); + $I->expectThrowable(\PHPUnit\Framework\AssertionFailedError::class, function() use ($I) { + $I->seeInNthEmailRecipient(1, 'alice@example.com>, sendEmail($user, 'First Subject', "First"); + $I->sendEmail('userB@example.com', 'Other Subject', "Other"); + $I->sendEmail($user, 'Second Subject', "Second"); + $I->sendEmail($user, 'Third Subject', "Third"); + $I->seeInNthEmailTo(2, $user, "Second"); + $I->dontSeeInNthEmailTo(2, $user, "Other"); + $I->seeInNthEmailSubjectTo(2, $user, 'Second Subject'); + $I->dontSeeInNthEmailSubjectTo(2, $user, 'Other Subject'); + } + + public function test_grab_from_nth_email(AcceptanceTester $I) + { + $I->sendEmail("user@example.com", 'Subject Line', "Hello World!"); + $I->sendEmail("user@example.com", 'Subject Line', "Hello Codeception!"); + $I->sendEmail("user@example.com", 'Subject Line', "Hello Again!"); + $I->assertEquals(['Hello Codeception', 'Codeception'], $I->grabMatchesFromNthEmail(2, "/Hello (Codeception)/")); + $I->assertEquals('Hello Codeception', $I->grabFromNthEmail(2, "/Hello (Codeception)/")); + } + + public function test_grab_from_nth_email_to(AcceptanceTester $I) + { + $user = "user@example.com"; + $I->sendEmail($user, 'Subject Line', "Hello World!"); + $I->sendEmail("userB@example.com", 'Subject Line', "Hello Other!"); + $I->sendEmail($user, 'Subject Line', "Hello Codeception!"); + $I->assertEquals(['Hello Codeception', 'Codeception'], $I->grabMatchesFromNthEmailTo(2, $user, "/Hello (\w+)/")); + $I->assertEquals('Hello Codeception', $I->grabFromNthEmailTo(2, $user, "/Hello (\w+)/")); + } + + public function test_grab_urls_from_nth_email(AcceptanceTester $I) + { + $url = "http://example.com/list.php?page=56"; + $I->sendEmail("user@example.com", 'Subject Line', "I have no URLs."); + $I->sendEmail("user@example.com", 'Subject Line', "I'm in $url ."); + $I->sendEmail("user@example.com", 'Subject Line', "I'm in https://example.org ."); + $I->assertEquals([$url], $I->grabUrlsFromNthEmail(2)); + } + + public function test_attachments_in_nth_email(AcceptanceTester $I) + { + $user = "user@example.com"; + $attachments = [ + "image.jpg" => codecept_data_dir('image.jpg'), + "lorem.txt" => codecept_data_dir('lorem.txt'), + ]; + + $I->sendEmail($user, 'Email without attachments', "I have no attachments."); + $I->sendEmail($user, 'Email with attachments', "I have attachments.", false, null, $attachments); + $I->sendEmail($user, 'Email without attachments', "I have no attachments."); + + $I->assertEquals(['image.jpg', 'lorem.txt'], array_keys($I->grabAttachmentsFromNthEmail(2))); + $I->seeAttachmentInNthEmail(2, 'lorem.txt'); + $I->seeNthEmailAttachmentCount(2, 2); + $I->seeNthEmailAttachmentCount(3, 0); + } } diff --git a/tests/unit/Module/MailCatcherTest.php b/tests/unit/Module/MailCatcherTest.php index f27377c..496d286 100644 --- a/tests/unit/Module/MailCatcherTest.php +++ b/tests/unit/Module/MailCatcherTest.php @@ -146,39 +146,18 @@ public function testLastMessageFromNoMessages() * * @return void */ - public function lastMessageFrom() + public function testLastMessageFrom() { - $handler = new MockHandler([ - new Response(200, [], json_encode([ - [ - 'id' => 1, - 'created_at' => date('c'), - 'sender' => 'sender@example.com', - 'recipients' => ['user@example.com'], - ], - [ - 'id' => 2, - 'created_at' => date('c'), - 'sender' => 'sender2@example.com', - 'recipients' => ['user2@example.com'], - ], - [ - 'id' => 3, - 'created_at' => date('c'), - 'sender' => 'sender3@example.com', - 'recipients' => ['user3@example.com'], - ] - ])) - ]); - $client = new Client(['handler' => $handler]); - - $mailcatcher = new MailCatcherTest_TestClass(); - $mailcatcher->setClient($client); - - $this->assertEquals( - $mailcatcher->getLastMessageFrom('sender2@example.com'), - 2 + $mailcatcher = $this->mailcatcherWithMessages( + [ + $this->message(1, 'sender@example.com', ['user@example.com']), + $this->message(2, 'sender2@example.com', ['user2@example.com']), + $this->message(3, 'sender3@example.com', ['user3@example.com']), + ], + new Email(2, [], '', ''), ); + + $this->assertEquals(2, $mailcatcher->lastMessageFrom('sender2@example.com')->getId()); } public function testSeeInLastEmailTo() @@ -262,6 +241,156 @@ public function testSeeEmailCountFail() $mailcatcher->seeEmailCount(3); } + + public function testLastMessageUsesIdAsTieBreakerForSameTimestamp() + { + $mailcatcher = $this->mailcatcherWithMessages( + [$this->message(9), $this->message(10)], + new Email(9, [], '', 'ninth'), + new Email(10, [], '', 'tenth'), + ); + + $this->assertEquals(10, $mailcatcher->lastMessage()->getId()); + } + + public function testNthMessageCountsFromTheFirstReceivedEmail() + { + $mailcatcher = $this->mailcatcherWithMessages( + [$this->message(3), $this->message(1), $this->message(2)], + new Email(2, [], '', 'second'), + ); + + $this->assertEquals(2, $mailcatcher->nthMessage(2)->getId()); + } + + public function testNthMessageOrdersByReceivedTimeBeforeId() + { + $messages = [ + $this->message(1, createdAt: '2026-09-30T10:00:02+00:00'), + $this->message(2, createdAt: '2026-09-30T10:00:01+00:00'), + $this->message(3, createdAt: '2026-09-30T10:00:00+00:00'), + ]; + $emails = [new Email(1, [], '', 'newest'), new Email(3, [], '', 'oldest')]; + + $this->assertEquals(3, $this->mailcatcherWithMessages($messages, ...$emails)->nthMessage(1)->getId()); + $this->assertEquals(1, $this->mailcatcherWithMessages($messages, ...$emails)->nthMessage(3)->getId()); + $this->assertEquals(1, $this->mailcatcherWithMessages($messages, ...$emails)->lastMessage()->getId()); + } + + public function testNthMessageNoMessages() + { + $mailcatcher = $this->mailcatcherWithMessages([]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('No messages received'); + + $mailcatcher->nthMessage(1); + } + + public function testNthMessageBeyondReceivedEmails() + { + $mailcatcher = $this->mailcatcherWithMessages([$this->message(1)]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('No message found at position 2'); + + $mailcatcher->nthMessage(2); + } + + public function testNthMessageRejectsPositionBelowOne() + { + $mailcatcher = $this->mailcatcherWithMessages([$this->message(1)]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Position must be 1 or greater, 0 given'); + + $mailcatcher->nthMessage(0); + } + + public function testNthMessageToCountsOnlyEmailsSentToAddress() + { + $mailcatcher = $this->mailcatcherWithMessages( + [ + $this->message(1, recipients: ['', '']), + $this->message(2, recipients: ['']), + $this->message(3, recipients: ['']), + ], + new Email(3, [], '', 'second to userA'), + ); + + $this->assertEquals(3, $mailcatcher->nthMessageTo(2, 'userA@example.com')->getId()); + } + + public function testSeeInLastEmailSender() + { + $mailcatcher = new MailCatcherTest_TestClass(); + $mailcatcher->setLastMessage(new Email(1, [], '', '', '')); + + $mailcatcher->seeInLastEmailSender('sender@example.com'); + } + + public function testSeeInLastEmailSenderFail() + { + $mailcatcher = new MailCatcherTest_TestClass(); + $mailcatcher->setLastMessage(new Email(1, [], '', '', '')); + + $this->expectException(AssertionFailedError::class); + + $mailcatcher->seeInLastEmailSender('other@example.com'); + } + + public function testSeeInLastEmailRecipientMatchesAnyRecipient() + { + $mailcatcher = new MailCatcherTest_TestClass(); + $mailcatcher->setLastMessage(new Email(1, ['', ''], '', '')); + + $mailcatcher->seeInLastEmailRecipient('userB@example.com'); + } + + public function testSeeInLastEmailRecipientDoesNotMatchAcrossRecipients() + { + $mailcatcher = new MailCatcherTest_TestClass(); + $mailcatcher->setLastMessage(new Email(1, ['', ''], '', '')); + + $this->expectException(AssertionFailedError::class); + + $mailcatcher->seeInLastEmailRecipient(', '); + } + + public function testSeeInLastEmailRecipientFail() + { + $mailcatcher = new MailCatcherTest_TestClass(); + $mailcatcher->setLastMessage(new Email(1, [''], '', '')); + + $this->expectException(AssertionFailedError::class); + + $mailcatcher->seeInLastEmailRecipient('userB@example.com'); + } + + private function mailcatcherWithMessages(array $messages, Email ...$emails): MailCatcherTest_TestClass + { + $mailcatcher = new MailCatcherTest_TestClass(); + $mailcatcher->setClient(new Client(['handler' => new MockHandler([ + new Response(200, [], json_encode($messages)), + ])])); + $mailcatcher->setEmails(...$emails); + + return $mailcatcher; + } + + private function message( + int $id, + string $sender = '', + array $recipients = [''], + string $createdAt = '2026-09-30T10:00:00+00:00' + ): array { + return [ + 'id' => $id, + 'created_at' => $createdAt, + 'sender' => $sender, + 'recipients' => $recipients, + ]; + } } class MailCatcherTest_TestClass extends MailCatcher @@ -270,11 +399,28 @@ class MailCatcherTest_TestClass extends MailCatcher private $lastMessageTo; private $lastMessageFrom; + /** + * @var array + */ + private array $emails = []; + public function __construct() { } + public function setEmails(Email ...$emails) + { + foreach ($emails as $email) { + $this->emails[$email->getId()] = $email; + } + } + + protected function emailFromId($id): Email + { + return $this->emails[$id] ?? parent::emailFromId($id); + } + public function getClient() { return $this->mailcatcher; diff --git a/tests/unit/Util/EmailTest.php b/tests/unit/Util/EmailTest.php index ce3bf75..7341ed5 100644 --- a/tests/unit/Util/EmailTest.php +++ b/tests/unit/Util/EmailTest.php @@ -12,18 +12,28 @@ public function testGetters() $this->assertEquals(['some@email.com'], $email->getRecipients()); $this->assertEquals('Some subject', $email->getSubject()); $this->assertEquals('Source body', $email->getSource()); + $this->assertEquals('', $email->getSender()); + } + + public function testGetSender() + { + $email = new Email(1, ['some@email.com'], 'Some subject', 'Source body', ''); + + $this->assertEquals('', $email->getSender()); } public function testCreateFromMailcatcherData() { $email = Email::createFromMailcatcherData([ 'id' => 1, + 'sender' => '', 'recipients' => ['some@email.com'], 'subject' => 'Some subject', 'source' => 'Source body' ]); $this->assertEquals(1, $email->getId()); + $this->assertEquals('', $email->getSender()); $this->assertEquals(['some@email.com'], $email->getRecipients()); $this->assertEquals('Some subject', $email->getSubject()); $this->assertEquals('Source body', $email->getSource());