SymfonyCasts / verify-email-bundle

Simple, stylish Email Verification for Symfony
https://symfonycasts.com
MIT License
414 stars 33 forks source link

Symfony upgrade 5.4 -> 6.4 brokes my tests #152

Closed badmansan closed 2 months ago

badmansan commented 12 months ago

I have some functional tests that check the registration process with email confirmation. It looks like this:

use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\MailerAssertionsTrait;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

final class RegisterUserAndLoginTest extends WebTestCase
{
    use MailerAssertionsTrait;

    private const SIGN_UP_FORM_NAME = 'sign_up_by_email_form';

    public function testRegisterForm(): void
    {
        $browser = self::createClient();
        $this->registerTheUser($browser);

        self::assertEmailCount(1);

        /** @var TemplatedEmail $email */
        $email = self::getMailerMessage();
        $context = $email->getContext();
        $emailBody = $email->getHtmlBody();

        self::assertArrayHasKey('signedUrl', $context);
        self::assertIsString($context['signedUrl']);
        self::assertStringContainsString(htmlspecialchars($context['signedUrl']), $emailBody);
        // ... other checks
    }

    private function registerTheUser(KernelBrowser $kernelBrowser): void
    {
        $kernelBrowser->request('GET', '/sign-up');

        $kernelBrowser->submitForm('Submit', [
            self::SIGN_UP_FORM_NAME . '[email]' => 'test@test.com',
            self::SIGN_UP_FORM_NAME . '[plainPassword][password]' => '1234567890',
            self::SIGN_UP_FORM_NAME . '[plainPassword][confirmation]' => '1234567890',
            self::SIGN_UP_FORM_NAME . '[agreeTerms]' => 1,
        ]);
    }
}

In test signedUrl is confirmation link that I generate from service during registration process:

// EmailConfirmationService
public function sendEmailConfirmation(string $verifyEmailRouteName, User $user, TemplatedEmail $templatedEmail): void
{
    $verifyEmailSignatureComponents = $this->verifyEmailHelper->generateSignature(
        $verifyEmailRouteName,
        $user->getId(),
        $user->getEmail(),
        ['id' => $user->getId()],
    );

    $context = $templatedEmail->getContext();
    $context['signedUrl'] = $verifyEmailSignatureComponents->getSignedUrl();

    $templatedEmail->context($context);

    $this->mailer->send($templatedEmail);
}

In symfony 5.4 this test works fine. But at 6.4 it brakes because of empty $email->getContext(). In the same time the body of email is ok, with correct link and all things works actually.

The problem is how email is rendered in later symfony. In 6.4 context erases after render. Here is BodyRenderer.php (6.4) and here BodyRenderer.php from 5.4. As you can see in 6.4 $message->markAsRendered() calls where context erases:

class TemplatedEmail extends Email
{
    // ...

    public function markAsRendered(): void
    {
        $this->textTemplate = null;
        $this->htmlTemplate = null;
        $this->context = [];
    }

Now I'm confused. What test should be written for such a check? Ok, I can parse raw html email body with regex. But how can I check that this link is the same I generated before in service?

jrushlow commented 7 months ago

Howdy @badmansan! sorry for the late reply to this. I'm working on the ability to generate tests over in maker-bundle for make:registration (https://github.com/symfony/maker-bundle/pull/1497). But in the meantime, here is an early WIP of what we'll generate over in maker:

class RegistrationFormTest extends WebTestCase
{
    public function testRegister(): void
    {
        $client = static::createClient();

        $container = static::getContainer();
        $em = $container->get('doctrine.orm.entity_manager');
        $userRepository = $container->get(UserRepository::class);

        foreach ($userRepository->findAll() as $user) {
            $em->remove($user);
        }

        $em->flush();

        self::assertCount(0, $userRepository->findAll());

        $client->request('GET', '/register');
        self::assertResponseIsSuccessful();

        $client->submitForm('Register', [
            'registration_form[email]' => 'jr@rushlow.dev',
            'registration_form[plainPassword]' => 'password',
            'registration_form[agreeTerms]' => true,
        ]);

        self::assertResponseRedirects('/');
        self::assertCount(1, $userRepository->findAll());
        self::assertFalse(($user = $userRepository->findAll()[0])->isVerified());

        // Use either assertQueuedEmailCount() || assertEmailCount() depending on your mailer setup
        self::assertQueuedEmailCount(1);
        // self::assertEmailCount(1);

        self::assertCount(1, $messages = $this->getMailerMessages());
        self::assertEmailTextBodyContains($messages[0], 'This link will expire in 1 hour.');

        $client->followRedirect();
        $client->loginUser($user);

        /** @var TemplatedEmail $templatedEmail */
        $templatedEmail = $messages[0];
        $messageBody = $templatedEmail->getHtmlBody();

        preg_match('#(http://localhost/verify/email.+)">#', $messageBody, $resetLink);

        $client->request('GET', $resetLink[1]);
        $client->followRedirect();

        self::assertTrue(static::getContainer()->get(UserRepository::class)->findAll()[0]->isVerified());
    }
}

Let me know if you this helps!

bocharsky-bw commented 2 months ago

Closing as no further feedback from the author.

If you still have this issue - please, feel free to reopen it again with more details