Files
Classeo/backend/tests/Unit/Scolarite/Infrastructure/Service/HomeworkHtmlSanitizerTest.php
Mathias STRASSER ab835e5c3d feat: Permettre à l'enseignant de rédiger avec un éditeur riche et joindre des fichiers
Les enseignants avaient besoin de consignes plus claires pour les élèves :
le champ description en texte brut ne permettait ni mise en forme ni
partage de documents. Cette limitation obligeait à décrire verbalement
les ressources au lieu de les joindre directement.

L'éditeur WYSIWYG (TipTap) remplace le textarea avec gras, italique,
listes et liens. Le contenu HTML est sanitisé côté backend via
symfony/html-sanitizer pour prévenir les injections XSS. Les pièces
jointes (PDF, JPEG, PNG, max 10 Mo) sont uploadées via une API dédiée
avec validation MIME côté domaine et protection path-traversal sur le
téléchargement. Les descriptions en texte brut existantes restent
lisibles sans migration de données.
2026-03-24 16:08:48 +01:00

159 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Scolarite\Infrastructure\Service;
use App\Scolarite\Infrastructure\Service\HomeworkHtmlSanitizer;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
final class HomeworkHtmlSanitizerTest extends TestCase
{
private HomeworkHtmlSanitizer $sanitizer;
protected function setUp(): void
{
$config = (new HtmlSanitizerConfig())
->allowElement('p')
->allowElement('br')
->allowElement('strong')
->allowElement('em')
->allowElement('ul')
->allowElement('ol')
->allowElement('li')
->allowElement('a', ['href', 'target', 'rel']);
$this->sanitizer = new HomeworkHtmlSanitizer(new HtmlSanitizer($config));
}
#[Test]
public function itAllowsBoldText(): void
{
$html = '<p>Texte <strong>en gras</strong></p>';
self::assertSame($html, $this->sanitizer->sanitize($html));
}
#[Test]
public function itAllowsItalicText(): void
{
$html = '<p>Texte <em>en italique</em></p>';
self::assertSame($html, $this->sanitizer->sanitize($html));
}
#[Test]
public function itAllowsUnorderedLists(): void
{
$html = '<ul><li>Élément 1</li><li>Élément 2</li></ul>';
self::assertSame($html, $this->sanitizer->sanitize($html));
}
#[Test]
public function itAllowsOrderedLists(): void
{
$html = '<ol><li>Premier</li><li>Deuxième</li></ol>';
self::assertSame($html, $this->sanitizer->sanitize($html));
}
#[Test]
public function itAllowsLinksWithSafeAttributes(): void
{
$html = '<p><a href="https://example.com" target="_blank" rel="noopener noreferrer">Lien</a></p>';
self::assertSame($html, $this->sanitizer->sanitize($html));
}
#[Test]
public function itStripsScriptTags(): void
{
$result = $this->sanitizer->sanitize('<p>Texte</p><script>alert("xss")</script>');
self::assertStringNotContainsString('<script>', $result);
self::assertStringNotContainsString('alert', $result);
}
#[Test]
public function itStripsEventHandlers(): void
{
$result = $this->sanitizer->sanitize('<p onclick="alert(1)">Texte</p>');
self::assertStringNotContainsString('onclick', $result);
self::assertStringContainsString('Texte', $result);
}
#[Test]
public function itStripsStyleTags(): void
{
$result = $this->sanitizer->sanitize('<style>body{display:none}</style><p>Texte</p>');
self::assertStringNotContainsString('<style>', $result);
self::assertStringContainsString('Texte', $result);
}
#[Test]
public function itStripsIframeTags(): void
{
$result = $this->sanitizer->sanitize('<p>Texte</p><iframe src="evil.com"></iframe>');
self::assertStringNotContainsString('<iframe>', $result);
}
#[Test]
public function itStripsImgTags(): void
{
$result = $this->sanitizer->sanitize('<p>Texte</p><img src="x" onerror="alert(1)">');
self::assertStringNotContainsString('<img', $result);
self::assertStringNotContainsString('onerror', $result);
}
#[Test]
public function itHandlesEmptyString(): void
{
self::assertSame('', $this->sanitizer->sanitize(''));
}
#[Test]
public function itHandlesPlainText(): void
{
$result = $this->sanitizer->sanitize('Texte simple sans HTML');
self::assertSame('Texte simple sans HTML', $result);
}
#[Test]
public function itStripsJavascriptLinks(): void
{
$result = $this->sanitizer->sanitize('<a href="javascript:alert(1)">Clic</a>');
self::assertStringNotContainsString('javascript:', $result);
}
#[Test]
#[DataProvider('richContentProvider')]
public function itPreservesRichContent(string $input, string $expectedSubstring): void
{
$result = $this->sanitizer->sanitize($input);
self::assertStringContainsString($expectedSubstring, $result);
}
/** @return iterable<string, array{string, string}> */
public static function richContentProvider(): iterable
{
yield 'gras' => ['<strong>gras</strong>', '<strong>gras</strong>'];
yield 'italique' => ['<em>italique</em>', '<em>italique</em>'];
yield 'liste à puces' => ['<ul><li>item</li></ul>', '<li>item</li>'];
yield 'liste numérotée' => ['<ol><li>item</li></ol>', '<ol>'];
yield 'paragraphe' => ['<p>texte</p>', '<p>texte</p>'];
yield 'lien' => ['<a href="https://x.com">lien</a>', 'href="https://x.com"'];
}
}