messages implemented
This commit is contained in:
144
tests/Unit/ContentSanitizerTest.php
Normal file
144
tests/Unit/ContentSanitizerTest.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
use App\Services\ContentSanitizer;
|
||||
|
||||
// ── Rendering ─────────────────────────────────────────────────────────────────
|
||||
|
||||
test('render converts bold markdown to strong tag', function () {
|
||||
$html = ContentSanitizer::render('**bold text**');
|
||||
expect($html)->toContain('<strong>bold text</strong>');
|
||||
});
|
||||
|
||||
test('render converts italic markdown to em tag', function () {
|
||||
$html = ContentSanitizer::render('*italic text*');
|
||||
expect($html)->toContain('<em>italic text</em>');
|
||||
});
|
||||
|
||||
test('render converts inline code to code tag', function () {
|
||||
$html = ContentSanitizer::render('use `code`');
|
||||
expect($html)->toContain('<code>code</code>');
|
||||
});
|
||||
|
||||
test('render auto-links URLs', function () {
|
||||
$html = ContentSanitizer::render('visit https://example.com for more');
|
||||
expect($html)->toContain('<a');
|
||||
expect($html)->toContain('href="https://example.com"');
|
||||
});
|
||||
|
||||
test('render returns empty string for null input', function () {
|
||||
expect(ContentSanitizer::render(null))->toBe('');
|
||||
});
|
||||
|
||||
test('render returns empty string for whitespace-only input', function () {
|
||||
expect(ContentSanitizer::render(' '))->toBe('');
|
||||
});
|
||||
|
||||
// ── XSS Prevention ────────────────────────────────────────────────────────────
|
||||
|
||||
test('render strips script tags', function () {
|
||||
$html = ContentSanitizer::render('<script>alert("xss")</script>hello');
|
||||
// The <script> tag itself must be gone (cannot execute)
|
||||
expect($html)->not()->toContain('<script>');
|
||||
// The word "hello" after the script block should appear
|
||||
expect($html)->toContain('hello');
|
||||
// Text inside script is rendered as harmless plain text — acceptable
|
||||
// Critical: no executable script element exists in the output
|
||||
});
|
||||
|
||||
test('render strips iframe tags', function () {
|
||||
$html = ContentSanitizer::render('<iframe src="evil.com"></iframe>');
|
||||
expect($html)->not()->toContain('<iframe');
|
||||
});
|
||||
|
||||
test('render strips javascript: links', function () {
|
||||
$html = ContentSanitizer::render('[click me](javascript:alert(1))');
|
||||
expect($html)->not()->toContain('javascript:');
|
||||
});
|
||||
|
||||
test('render strips style attributes', function () {
|
||||
$html = ContentSanitizer::render('<b style="color:red">red</b>');
|
||||
expect($html)->not()->toContain('style=');
|
||||
});
|
||||
|
||||
test('render strips onclick attributes', function () {
|
||||
$html = ContentSanitizer::render('<b onclick="evil()">text</b>');
|
||||
expect($html)->not()->toContain('onclick');
|
||||
});
|
||||
|
||||
test('render adds rel=noopener to external links', function () {
|
||||
$html = ContentSanitizer::render('[link](https://example.com)');
|
||||
expect($html)->toContain('rel="noopener noreferrer nofollow"');
|
||||
});
|
||||
|
||||
// ── Legacy HTML conversion ────────────────────────────────────────────────────
|
||||
|
||||
test('render converts legacy bold HTML to markdown output', function () {
|
||||
$html = ContentSanitizer::render('<b>old bold</b>');
|
||||
expect($html)->toContain('<strong>');
|
||||
expect($html)->not()->toContain('<script>');
|
||||
});
|
||||
|
||||
test('render converts br tags to line breaks', function () {
|
||||
$html = ContentSanitizer::render("line one<br>line two");
|
||||
// Should not contain the raw <br> tag in unexpected ways
|
||||
expect($html)->toContain('line one');
|
||||
expect($html)->toContain('line two');
|
||||
});
|
||||
|
||||
// ── Plain text ────────────────────────────────────────────────────────────────
|
||||
|
||||
test('stripToPlain removes all HTML', function () {
|
||||
$plain = ContentSanitizer::stripToPlain('<p>Hello <b>world</b>!</p>');
|
||||
expect($plain)->toBe('Hello world!');
|
||||
});
|
||||
|
||||
test('stripToPlain converts br to newline', function () {
|
||||
$plain = ContentSanitizer::stripToPlain("line one<br>line two");
|
||||
expect($plain)->toContain("\n");
|
||||
});
|
||||
|
||||
// ── Validation ────────────────────────────────────────────────────────────────
|
||||
|
||||
test('validate returns error for raw HTML tags', function () {
|
||||
$errors = ContentSanitizer::validate('<script>evil</script>');
|
||||
expect($errors)->not()->toBeEmpty();
|
||||
expect(implode(' ', $errors))->toContain('HTML');
|
||||
});
|
||||
|
||||
test('validate passes for clean markdown', function () {
|
||||
$errors = ContentSanitizer::validate('**bold** and *italic* and `code`');
|
||||
expect($errors)->toBeEmpty();
|
||||
});
|
||||
|
||||
test('validate returns error when content is too long', function () {
|
||||
$errors = ContentSanitizer::validate(str_repeat('a', 10_001));
|
||||
expect($errors)->not()->toBeEmpty();
|
||||
});
|
||||
|
||||
test('validate returns error when emoji density exceeds threshold', function () {
|
||||
// 10 fire emoji + 4 spaces = 14 chars; emoji count = 10; density = 10/14 ≈ 0.71 > 0.40
|
||||
$floodContent = implode(' ', array_fill(0, 10, '🔥'));
|
||||
$errors = ContentSanitizer::validate($floodContent);
|
||||
expect($errors)->not()->toBeEmpty();
|
||||
expect(implode(' ', $errors))->toContain('emoji');
|
||||
});
|
||||
|
||||
test('validate accepts content with reasonable emoji usage', function () {
|
||||
// 3 emoji in a 50-char string — density ≈ 0.06, well below threshold
|
||||
$errors = ContentSanitizer::validate('Great work on this piece 🎨 love the colours ❤️ keep it up 👏');
|
||||
expect($errors)->toBeEmpty();
|
||||
});
|
||||
|
||||
// ── collapseFlood ─────────────────────────────────────────────────────────────
|
||||
|
||||
test('collapseFlood delegates to LegacySmileyMapper and collapses runs', function () {
|
||||
$input = implode(' ', array_fill(0, 8, '🍺'));
|
||||
$result = ContentSanitizer::collapseFlood($input);
|
||||
expect($result)->toContain('×8');
|
||||
expect(substr_count($result, '🍺'))->toBeLessThanOrEqual(5);
|
||||
});
|
||||
|
||||
test('collapseFlood returns unchanged string when no flood present', function () {
|
||||
$input = 'Nice art 🎨 love it ❤️';
|
||||
expect(ContentSanitizer::collapseFlood($input))->toBe($input);
|
||||
});
|
||||
110
tests/Unit/LegacySmileyMapperTest.php
Normal file
110
tests/Unit/LegacySmileyMapperTest.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
use App\Services\LegacySmileyMapper;
|
||||
|
||||
test('convert beer smiley to emoji', function () {
|
||||
$result = LegacySmileyMapper::convert('great work :beer');
|
||||
expect($result)->toContain('🍺');
|
||||
expect($result)->not()->toContain(':beer');
|
||||
});
|
||||
|
||||
test('convert multiple smileys in one string', function () {
|
||||
$result = LegacySmileyMapper::convert(':clap great art :love it');
|
||||
expect($result)->toContain('👏');
|
||||
expect($result)->toContain('❤️');
|
||||
});
|
||||
|
||||
test('does not replace smiley codes embedded in words', function () {
|
||||
// ":lol" should only be replaced when it stands alone
|
||||
$result = LegacySmileyMapper::convert('something:lol mixed');
|
||||
// The code is embedded in a word, so it should NOT be replaced
|
||||
expect($result)->toContain(':lol');
|
||||
});
|
||||
|
||||
test('detect returns found codes', function () {
|
||||
$found = LegacySmileyMapper::detect('nice :beer and :lol');
|
||||
expect($found)->toContain(':beer');
|
||||
expect($found)->toContain(':lol');
|
||||
});
|
||||
|
||||
test('detect returns empty array for clean text', function () {
|
||||
$found = LegacySmileyMapper::detect('hello world, no smileys here');
|
||||
expect($found)->toBeEmpty();
|
||||
});
|
||||
|
||||
test('convert returns original string when no codes present', function () {
|
||||
$input = 'This has no smiley codes.';
|
||||
$result = LegacySmileyMapper::convert($input);
|
||||
expect($result)->toBe($input);
|
||||
});
|
||||
|
||||
test('getMap returns non-empty array', function () {
|
||||
$map = LegacySmileyMapper::getMap();
|
||||
expect($map)->toBeArray()->not()->toBeEmpty();
|
||||
expect($map[':beer'])->toBe('🍺');
|
||||
});
|
||||
|
||||
test('convert handles empty string', function () {
|
||||
expect(LegacySmileyMapper::convert(''))->toBe('');
|
||||
});
|
||||
|
||||
// ── collapseFlood ─────────────────────────────────────────────────────────────
|
||||
|
||||
test('collapseFlood leaves short runs untouched', function () {
|
||||
// 5 beer mugs with spaces — at or below the maxRun=5 default, no collapse.
|
||||
$input = '\ud83c\udf7a \ud83c\udf7a \ud83c\udf7a \ud83c\udf7a \ud83c\udf7a';
|
||||
expect(LegacySmileyMapper::collapseFlood($input))->toBe($input);
|
||||
});
|
||||
|
||||
test('collapseFlood collapses a run-together flood', function () {
|
||||
// 8 fire emoji in a row (no spaces) — should produce 5 + ×8
|
||||
$input = str_repeat('🔥', 8);
|
||||
$result = LegacySmileyMapper::collapseFlood($input);
|
||||
expect($result)->toContain('×8');
|
||||
// output has at most maxRun (5) copies of the emoji
|
||||
$count = substr_count($result, '🔥');
|
||||
expect($count)->toBeLessThanOrEqual(5);
|
||||
});
|
||||
|
||||
test('collapseFlood collapses a space-separated flood', function () {
|
||||
// 10 clap emoji separated by single spaces
|
||||
$input = implode(' ', array_fill(0, 10, '👏'));
|
||||
$result = LegacySmileyMapper::collapseFlood($input);
|
||||
expect($result)->toContain('×10');
|
||||
$count = substr_count($result, '👏');
|
||||
expect($count)->toBeLessThanOrEqual(5);
|
||||
});
|
||||
|
||||
test('collapseFlood respects custom maxRun', function () {
|
||||
$input = implode(' ', array_fill(0, 8, '❤️'));
|
||||
$result = LegacySmileyMapper::collapseFlood($input, maxRun: 3);
|
||||
expect($result)->toContain('×8');
|
||||
$count = substr_count($result, '❤️');
|
||||
expect($count)->toBeLessThanOrEqual(3);
|
||||
});
|
||||
|
||||
test('collapseFlood does not affect regular text or mixed content', function () {
|
||||
$input = 'Nice work \ud83d\udc4d really cool \ud83d\udd25';
|
||||
$result = LegacySmileyMapper::collapseFlood($input);
|
||||
expect($result)->toBe($input); // nothing to collapse
|
||||
});
|
||||
|
||||
test('collapseFlood handles empty string', function () {
|
||||
expect(LegacySmileyMapper::collapseFlood(''))->toBe('');
|
||||
});
|
||||
|
||||
// ── detect() colon-lookahead regression ──────────────────────────────────────
|
||||
|
||||
test('detect does not match smiley code with trailing colon', function () {
|
||||
// ':sad:' used to partially match ':sad' leaving a stray ':'.
|
||||
// After the fix, ':' is not in the lookahead, so ':sad:' should be
|
||||
// detected only if followed by whitespace / punctuation (not ':').
|
||||
// A bare ':sad:' surrounded by spaces must still be detected.
|
||||
$found = LegacySmileyMapper::detect(':sad and more text');
|
||||
expect($found)->toContain(':sad');
|
||||
|
||||
// ':sad:' where the colon immediately follows should NOT be detected
|
||||
// because ':' is no longer in [.,!?;] lookahead.
|
||||
$foundColon = LegacySmileyMapper::detect('text:sad: more');
|
||||
expect($foundColon)->not()->toContain(':sad');
|
||||
});
|
||||
Reference in New Issue
Block a user