Support RawMessage instead of Email for Mail/Mime assertions#232
Support RawMessage instead of Email for Mail/Mime assertions#232krrico wants to merge 2 commits into
Conversation
|
Just added the Test PR here: |
| * ``` | ||
| */ | ||
| public function assertEmailHeaderSame(string $headerName, string $expectedValue, ?Email $email = null): void | ||
| public function assertEmailHeaderSame(string $headerName, string $expectedValue, ?Message $email = null): void |
There was a problem hiding this comment.
I'm not sure whether it's better to use RawMessage as type hint everywhere and let the Symfony Mime Constraints used here run into the RuntimeException when they don't support RawMessage or Message.
746d68d to
f89c7ce
Compare
…awMessage and Message instead of Email only
f89c7ce to
e8ab243
Compare
|
Hey @krrico , thanks for the thorough bug report and the PR — you identified the root cause precisely and your proposed fix was on the right track. 🙌 I went ahead and implemented a refined version of your approach in e8ab243. Here's a summary of the decisions made and why. The core issue you correctly identifiedWhen Symfony's Why we didn't use
|
| Assertion | Parameter type | Why |
|---|---|---|
assertEmailHasHeader |
?Message |
EmailHasHeader constraint requires Message (rejects RawMessage) |
assertEmailAddressContains |
?Message |
EmailAddressContains constraint requires Message |
assertEmailHeaderSame |
?Message |
EmailHeaderSame constraint requires Message |
assertEmailHeaderNotSame |
?Message |
EmailHeaderSame constraint requires Message |
assertEmailNotHasHeader |
?Message |
EmailHasHeader constraint requires Message |
assertEmailAttachmentCount |
?Email |
EmailAttachmentCount constraint requires Email specifically |
assertEmailHtmlBodyContains |
?Email |
EmailHtmlBodyContains constraint requires Email specifically |
assertEmailHtmlBodyNotContains |
?Email |
EmailHtmlBodyContains constraint requires Email specifically |
assertEmailTextBodyContains |
?Email |
EmailTextBodyContains constraint requires Email specifically |
assertEmailTextBodyNotContains |
?Email |
EmailTextBodyContains constraint requires Email specifically |
The distinction maps directly to what Symfony's own Mime constraints check at runtime: header-based assertions only need a Message (which carries headers), while body and attachment assertions need a full Email (which adds body and part accessors). Since encrypted/signed messages are still Message instances — they have headers but no plain-text body — the header-based assertions will work on them out of the box.
The internal method: verifyEmailObject → getMessageOrFail
Your PR widened verifyEmailObject to (?RawMessage): RawMessage to match the broadest possible type. I renamed it to getMessageOrFail and gave it a tighter contract:
private function getMessageOrFail(?Message $message, string $function): Message- Accepts
?Message— so it is compatible with all public callers. - Returns
Message— the minimum type all Mime constraints accept. - Fails explicitly on bare
RawMessage— if neither a message is provided nor one was sent, or if what was sent is a plainRawMessagewith no headers, it fails the test with a clear message rather than letting Symfony's constraint throw aLogicExceptiondeep inside the assertion.
What grabLastSentEmail() returns now
grabLastSentEmail() now returns ?RawMessage (via an internal grabLastSentRawMessage() helper), matching the raw output of MessageLoggerListener. This means it covers both Email and Message objects. If you need to type-narrow the result yourself, instanceof is the right tool:
$message = $I->grabLastSentEmail(); // ?RawMessage — works for Email and Message
if ($message instanceof Email) {
// access Email-specific methods
}Tests added
I also added tests that cover your exact use case — sending a plain Message through the mailer and asserting on its headers:
// In MimeAssertionsTest
public function testHeaderAssertionsWorkWithSentMessage(): void
{
// /send-message dispatches a Message (not Email) through the mailer
$this->client->request('GET', '/send-message');
$this->assertEmailHasHeader('To');
$this->assertEmailAddressContains('To', 'jane_doe@example.com');
$this->assertEmailHeaderSame('Subject', 'Test message');
}One thing I'd like to ask of you
Could you update your PR in the testing repository (Codeception/symfony-module-tests#40) to match the simplified approach I've taken here?
Your current commit (71d207e) uses actual S/MIME encryption — a Crypto helper that generates real OpenSSL certificates at runtime, an ext-openssl dev dependency, and a SendEncryptedEmailController that calls SMimeEncrypter::encrypt(). That's a faithful reproduction of the real-world scenario, but it introduces a non-trivial infrastructure burden on the test suite (an OpenSSL extension requirement, ephemeral certificate files, etc.).
The key insight is that for testing the module's assertion methods, we don't actually need to encrypt anything. What matters is that the mailer receives a Message object instead of an Email. The simplest way to achieve that is to just construct and send a Message directly — no encryption, no certificates, no OpenSSL.
The simplified version I've added here does exactly that:
// tests/_app/Mailer/MessageMailer.php
public function send(string $recipient): void
{
$headers = new Headers();
$headers->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')));
}So in the testing repo, the ideal update would be to:
- Replace the
SendEncryptedEmailController/Crypto/SMimeEncryptersetup with a simple controller that sends a plainMessage - Remove the
ext-openssldev dependency fromcomposer.json - Update
IssuesCestto assert against this simpler/send-messageendpoint
Thanks again for the detailed issue and the groundwork in the PR!
Adjusted type hints of Mailer and Mime assertions traits to support RawMessage and Message instead of Email only in order to also support fetching sent Emails when they were encrypted or signed before which turns them into plain Message objects rather than Email objects (which is a child class of Message).