Files
myep-team/tests/Email/MailBodyRendererTest.php

175 lines
6.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Email;
use App\Email\MailBodyRenderer;
use PHPUnit\Framework\TestCase;
/**
* The renderer is the only thing standing between what an admin typed and the HTML of a
* mail, and between what a teamer typed and that same HTML. Most of what is asserted here
* is where that line runs: admin text may become markup, a substituted value may not.
*/
class MailBodyRendererTest extends TestCase
{
private MailBodyRenderer $renderer;
protected function setUp(): void
{
$this->renderer = new MailBodyRenderer();
}
public function testDoubleAsterisksMakeTextBoldAndSingleOnesItalic(): void
{
$this->assertSame(
'<p><strong>fett</strong> und <em>kursiv</em></p>',
$this->renderer->render('**fett** und *kursiv*', [])
);
}
public function testDashesBecomeABulletList(): void
{
$this->assertSame(
"<ul>\n<li>Ausweis</li>\n<li>Bankverbindung</li>\n</ul>",
$this->renderer->render("- Ausweis\n- Bankverbindung", [])
);
}
public function testNumbersBecomeANumberedList(): void
{
$this->assertSame(
"<ol>\n<li>Erstens</li>\n<li>Zweitens</li>\n</ol>",
$this->renderer->render("1. Erstens\n2. Zweitens", [])
);
}
/**
* The mail layout sets its own h1 above the body and styles nothing below h3, so every
* heading is folded into that range rather than producing a tag nobody styled.
*/
public function testHeadingsAreFoldedIntoTheLevelsTheMailStyles(): void
{
$this->assertSame(
"<h2>Gross</h2>\n<h2>Mittel</h2>\n<h3>Klein</h3>\n<h3>Winzig</h3>",
$this->renderer->render("# Gross\n\n## Mittel\n\n### Klein\n\n###### Winzig", [])
);
}
public function testBlankLinesBecomeParagraphsAndSingleBreaksStay(): void
{
$this->assertSame(
"<p>Erste Zeile<br />\nZweite Zeile</p>\n<p>Neuer Absatz</p>",
$this->renderer->render("Erste Zeile\nZweite Zeile\n\nNeuer Absatz", [])
);
}
public function testBareUrlsAndMailAddressesBecomeLinks(): void
{
$html = $this->renderer->render('Schreib an [email protected] oder https://ep-reisen.de/team.', []);
$this->assertStringContainsString('<a href="mailto:[email protected]">[email protected]</a>', $html);
// The full stop ends the sentence, it is not part of the address.
$this->assertStringContainsString('<a href="https://ep-reisen.de/team">https://ep-reisen.de/team</a>.', $html);
}
public function testAnEscapedAsteriskStaysLiteral(): void
{
$this->assertSame('<p>Ein Sternchen: *</p>', $this->renderer->render('Ein Sternchen: \*', []));
}
/**
* Admins write Markdown, so markup they type is dropped rather than passed through.
*/
public function testMarkupTypedByAnAdminIsStripped(): void
{
$html = $this->renderer->render('Hallo <b>alle</b> <script>alert(1)</script>', []);
$this->assertStringNotContainsString('<b>', $html);
$this->assertStringNotContainsString('<script', $html);
}
/**
* An image in a mailing would be loaded from an address nobody reviewed, so only the
* alt text survives.
*/
public function testAnImageIsReducedToItsAltText(): void
{
$html = $this->renderer->render('![Ein Bild](https://example.org/pixel.png)', []);
$this->assertStringNotContainsString('<img', $html);
$this->assertStringContainsString('Ein Bild', $html);
}
public function testAnUnsafeLinkLosesItsTarget(): void
{
$html = $this->renderer->render('[klick](javascript:alert(1))', []);
$this->assertStringNotContainsString('javascript:', $html);
$this->assertStringContainsString('klick', $html);
}
public function testPlaceholdersAreSubstituted(): void
{
$this->assertSame(
'<p>Hallo Anna, dein Einsatz Skireise</p>',
$this->renderer->render(
'Hallo {{vorname}}, dein Einsatz {destination}',
['{{vorname}}' => 'Anna', '{destination}' => 'Skireise']
)
);
}
/**
* The load-bearing case. A teamer's own words go in after the parser has run, so
* nothing in them can become a tag, a heading, a list or emphasis - whatever they
* happen to contain.
*/
public function testNothingInAPlaceholderValueBecomesMarkup(): void
{
$html = $this->renderer->render('Kommentar: {kommentar}', [
'{kommentar}' => "**nicht fett**\n- keine Liste\n# keine Überschrift <b>x</b>",
]);
$this->assertStringNotContainsString('<strong>', $html);
$this->assertStringNotContainsString('<ul>', $html);
$this->assertStringNotContainsString('<h1', $html);
$this->assertStringNotContainsString('<h2', $html);
$this->assertStringNotContainsString('<b>', $html);
$this->assertStringContainsString('**nicht fett**', $html);
$this->assertStringContainsString('- keine Liste', $html);
$this->assertStringContainsString('&lt;b&gt;x&lt;/b&gt;', $html);
}
/**
* A rejection comment arrives as one multi-line value inside a single paragraph.
*/
public function testLineBreaksInsideAPlaceholderValueSurvive(): void
{
$this->assertSame(
"<p>Erste Zeile<br />\nZweite Zeile</p>",
$this->renderer->render('{kommentar}', ['{kommentar}' => "Erste Zeile\nZweite Zeile"])
);
}
/**
* Documented rather than desired: substitution runs over the finished HTML, so a token
* an admin put in backticks is replaced like any other. Harmless, but surprising enough
* to pin down.
*/
public function testATokenInACodeSpanIsStillSubstituted(): void
{
$this->assertSame(
'<p><code>Skireise</code></p>',
$this->renderer->render('`{destination}`', ['{destination}' => 'Skireise'])
);
}
public function testAnEmptyBodyRendersAsNothing(): void
{
$this->assertSame('', $this->renderer->render('', []));
$this->assertSame('', $this->renderer->render(null, []));
}
}