*/ /** * 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 */ private const NO_PORTAL_BUTTON = [ EmailTextKey::APPLICATION_FORM_RECEIVED, EmailTextKey::APPLICATION_FORM_FOLLOW_UP, ]; #[\PHPUnit\Framework\Attributes\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->createStub(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' => 'test@example.com', 'to' => 'teamer@example.com', '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', $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('in', $html); // The call dates are written as a list and the two parts as headings. $this->assertStringContainsString('