196 lines
7.3 KiB
PHP
196 lines
7.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Email;
|
|
|
|
use App\Config\EmailTextCatalog;
|
|
use App\Config\EmailTextKey;
|
|
use App\Email\EmailTextRenderer;
|
|
use App\Email\MailBodyRenderer;
|
|
use App\Email\Mailer;
|
|
use App\Repository\EmailTextRepository;
|
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
|
|
/**
|
|
* Renders every editable mail through the real template stack.
|
|
*
|
|
* The unit tests cover what the renderer produces; this covers that the result actually
|
|
* survives email/generic.html.twig and the layout around it - a mail that only breaks
|
|
* when it is assembled would otherwise go unnoticed until a teamer fails to receive it.
|
|
*/
|
|
class EmailTextMailRenderingTest extends KernelTestCase
|
|
{
|
|
/**
|
|
* The mails that go to someone without an account here, and are therefore sent through the
|
|
* template that empties the layout's closing "Zum Portal" button. Kept next to the assertion
|
|
* that every other mail *has* that button, so adding a third such mail is a decision rather
|
|
* than an accident.
|
|
*
|
|
* @var list<EmailTextKey>
|
|
*/
|
|
/**
|
|
* Stands in for APPLICATION_FORM_BOOKING_URL. Unlike the other placeholders this one is not
|
|
* caller-supplied - it reaches the body from configuration, before the Markdown is parsed -
|
|
* so the assertions below look for the URL itself rather than a "[name]" marker.
|
|
*/
|
|
private const BOOKING_URL = 'https://bookings.example.test/team-infocall';
|
|
|
|
/**
|
|
* @var list<EmailTextKey>
|
|
*/
|
|
private const NO_PORTAL_BUTTON = [
|
|
EmailTextKey::APPLICATION_FORM_RECEIVED,
|
|
EmailTextKey::APPLICATION_FORM_FOLLOW_UP,
|
|
];
|
|
|
|
/**
|
|
* @dataProvider provideKeys
|
|
*/
|
|
public function testTheMailRenders(EmailTextKey $key): void
|
|
{
|
|
self::bootKernel();
|
|
|
|
$container = self::getContainer();
|
|
$catalog = $container->get(EmailTextCatalog::class);
|
|
$definition = $catalog->get($key);
|
|
|
|
$placeholders = [];
|
|
|
|
foreach ($definition->getPlaceholderNames() as $name) {
|
|
$placeholders[$name] = '['.$name.']';
|
|
}
|
|
|
|
// Renders the delivered wording: the test environment has no database, and the
|
|
// defaults are what a mail falls back to anyway.
|
|
$repository = $this->createMock(EmailTextRepository::class);
|
|
$repository->method('findByKey')->willReturn(null);
|
|
|
|
$text = (new EmailTextRenderer(
|
|
$catalog,
|
|
$repository,
|
|
$container->get(MailBodyRenderer::class),
|
|
self::BOOKING_URL
|
|
))->render($key, $placeholders);
|
|
|
|
$withoutButton = in_array($key, self::NO_PORTAL_BUTTON, true);
|
|
|
|
$email = $container->get(Mailer::class)->create(['text' => $text], [
|
|
'from' => '[email protected]',
|
|
'to' => '[email protected]',
|
|
'subject' => $text->subject,
|
|
'template' => $withoutButton
|
|
? 'email/generic_no_button.html.twig'
|
|
: 'email/generic.html.twig',
|
|
'subject_parameters' => [],
|
|
'attachments' => [],
|
|
'transport' => null,
|
|
'bus_transport' => null,
|
|
]);
|
|
|
|
$html = $email->getHtmlBody();
|
|
|
|
$this->assertNotEmpty($html);
|
|
$this->assertStringContainsString($text->headline, $html);
|
|
if (true === $withoutButton) {
|
|
// The recipient filled in a form on a public website and has no account here, so a
|
|
// link to a login they cannot pass would be worse than no link.
|
|
$this->assertStringNotContainsString('Zum Portal', $html);
|
|
} else {
|
|
// The layout closes every mail with the same button, pointing at the site root
|
|
// rather than a deep link so that a client's link preview cannot trip the
|
|
// unauthorized-access logging.
|
|
$this->assertStringContainsString('class="button">Zum Portal</a>', $html);
|
|
}
|
|
|
|
$this->assertStringNotContainsString('/teamer/disposition/detail', $html);
|
|
|
|
// Every placeholder the mail declares has to reach the rendered body, or the
|
|
// wording lost a detail somewhere between catalogue and template.
|
|
foreach ($definition->getPlaceholderNames() as $name) {
|
|
if ('bookingUrl' === $name) {
|
|
// Configured, not passed in, and substituted before parsing - so it has to have
|
|
// become a real link target rather than a percent-encoded token.
|
|
$this->assertStringContainsString('href="'.self::BOOKING_URL.'"', $html);
|
|
|
|
continue;
|
|
}
|
|
|
|
$this->assertStringContainsString(
|
|
'['.$name.']',
|
|
$html.$email->getSubject(),
|
|
sprintf('Placeholder "%s" does not show up in the rendered mail', $name)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The follow-up mail is the one piece of delivered wording that leans on Markdown escaping:
|
|
* "Saisonmitarbeiter\*in" has a single asterisk, which CommonMark reads as emphasis unless it
|
|
* is escaped. Getting that wrong does not fail anything - it silently italicises a stretch of
|
|
* the mail - so it is pinned here.
|
|
*/
|
|
public function testTheFollowUpSurvivesItsMarkdown(): void
|
|
{
|
|
$html = $this->renderBody(EmailTextKey::APPLICATION_FORM_FOLLOW_UP);
|
|
|
|
$this->assertStringContainsString('Saisonmitarbeiter*in', $html);
|
|
$this->assertStringNotContainsString('<em>in', $html);
|
|
// The call dates are written as a list and the two parts as headings.
|
|
$this->assertStringContainsString('<ul>', $html);
|
|
$this->assertStringContainsString('Wochenteam', $html);
|
|
$this->assertStringContainsString('Saisonpersonal', $html);
|
|
}
|
|
|
|
/**
|
|
* An unconfigured booking URL must stop the mail, not render <a href=""> and invite an
|
|
* applicant to book a call they cannot reach. The endpoint turns this into a logged error and
|
|
* still answers the submission; a reviewer sending by hand sees it.
|
|
*/
|
|
public function testAnUnconfiguredBookingUrlRefusesToRender(): void
|
|
{
|
|
self::bootKernel();
|
|
|
|
$container = self::getContainer();
|
|
$repository = $this->createMock(EmailTextRepository::class);
|
|
$repository->method('findByKey')->willReturn(null);
|
|
|
|
$renderer = new EmailTextRenderer(
|
|
$container->get(EmailTextCatalog::class),
|
|
$repository,
|
|
$container->get(MailBodyRenderer::class),
|
|
''
|
|
);
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->expectExceptionMessage('APPLICATION_FORM_BOOKING_URL is not configured');
|
|
|
|
$renderer->render(EmailTextKey::APPLICATION_FORM_RECEIVED, []);
|
|
}
|
|
|
|
private function renderBody(EmailTextKey $key): string
|
|
{
|
|
self::bootKernel();
|
|
|
|
$container = self::getContainer();
|
|
$repository = $this->createMock(EmailTextRepository::class);
|
|
$repository->method('findByKey')->willReturn(null);
|
|
|
|
$text = (new EmailTextRenderer(
|
|
$container->get(EmailTextCatalog::class),
|
|
$repository,
|
|
$container->get(MailBodyRenderer::class),
|
|
self::BOOKING_URL
|
|
))->render($key, []);
|
|
|
|
return $text->bodyHtml;
|
|
}
|
|
|
|
public static function provideKeys(): iterable
|
|
{
|
|
foreach (EmailTextKey::cases() as $key) {
|
|
yield $key->value => [$key];
|
|
}
|
|
}
|
|
}
|