test: Testsuite fuer Kernlogik, QR-Codes, Domains und Sicherheit

232 Tests, unter anderem:
- Slug-Erzeugung, Kollisionsbehandlung und Validierung
- URL-Validierung inklusive IDN, Prozentkodierung und abgelehnter
  Protokolle; Punycode gegengeprueft gegen idn_to_ascii
- QR-Erzeugung in allen Groessen, mit Logo und Level-H-Automatik
- Weiterleitung gegen eine echte SQLite-Datenbank samt Domain-
  Aufloesung ueber den Host-Header
- Update einer bestehenden Installation auf mehrere Domains
- Angriffsseitig: gefaelschte Proxy-Header, Host-Header-Injektion,
  SVG mit Skripten, XXE und externen Referenzen
This commit is contained in:
2026-08-03 18:11:40 +02:00
parent 73a9bb09b1
commit 8a7939bee2
14 changed files with 2228 additions and 0 deletions
+164
View File
@@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace App\Tests;
use App\Qr\LogoStore;
use App\Qr\QrOptions;
use App\Qr\QrRenderer;
use PHPUnit\Framework\TestCase;
final class QrRendererTest extends TestCase
{
private string $uploadDir;
protected function setUp(): void
{
$this->uploadDir = sys_get_temp_dir() . '/urlshort-test-' . bin2hex(random_bytes(4));
mkdir($this->uploadDir, 0775, true);
}
protected function tearDown(): void
{
foreach (glob($this->uploadDir . '/*') ?: [] as $file) {
unlink($file);
}
if (is_dir($this->uploadDir)) {
rmdir($this->uploadDir);
}
}
private function renderer(): QrRenderer
{
return new QrRenderer(new LogoStore($this->uploadDir));
}
public function testPngIsGeneratedInRequestedSize(): void
{
$png = $this->renderer()->png('https://kurz.test/messe2026', new QrOptions(size: 512, margin: 4));
self::assertStringStartsWith("\x89PNG", $png);
$info = getimagesizefromstring($png);
self::assertIsArray($info);
self::assertSame(512, $info[0]);
self::assertSame(512, $info[1]);
}
/**
* @dataProvider sizeProvider
*/
public function testAllOfferedSizesWork(int $size): void
{
$info = getimagesizefromstring(
$this->renderer()->png('https://kurz.test/abc', new QrOptions(size: $size)),
);
self::assertIsArray($info);
self::assertSame($size, $info[0]);
}
/**
* @return list<array{0:int}>
*/
public static function sizeProvider(): array
{
return array_map(static fn (int $size): array => [$size], QrOptions::SIZES);
}
public function testSvgIsVectorAndCarriesTheColors(): void
{
$svg = $this->renderer()->svg(
'https://kurz.test/messe2026',
new QrOptions(foreground: '#0b4f9e', background: '#fff7ed', size: 1024),
);
self::assertStringContainsString('<svg', $svg);
self::assertStringContainsString('viewBox', $svg);
self::assertStringContainsString('#0b4f9e', $svg);
self::assertStringContainsString('#fff7ed', $svg);
// Vektor statt eingebetteter Pixelgrafik.
self::assertStringNotContainsString('data:image/png', $svg);
}
public function testMarginZeroProducesSmallerQuietZoneThanMarginTen(): void
{
$data = 'https://kurz.test/abc';
$withoutMargin = $this->renderer()->png($data, new QrOptions(size: 512, margin: 0));
$withMargin = $this->renderer()->png($data, new QrOptions(size: 512, margin: 10));
// Beide behalten die Zielgroesse ...
self::assertSame(512, (getimagesizefromstring($withoutMargin) ?: [0])[0]);
self::assertSame(512, (getimagesizefromstring($withMargin) ?: [0])[0]);
// ... unterscheiden sich aber im Bildinhalt.
self::assertNotSame($withoutMargin, $withMargin);
}
public function testLogoForcesErrorCorrectionLevelH(): void
{
$logo = $this->uploadDir . '/logo.png';
$this->writePngLogo($logo);
$options = (new QrOptions(errorCorrection: 'L'))->withLogoPath('logo.png');
self::assertSame('H', $options->errorCorrection);
$png = $this->renderer()->png('https://kurz.test/mitlogo', $options);
self::assertStringStartsWith("\x89PNG", $png);
}
public function testSvgLogoIsEmbeddedInSvgOutput(): void
{
file_put_contents(
$this->uploadDir . '/logo.svg',
'<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10">'
. '<circle cx="5" cy="5" r="5" fill="#ff0000"/></svg>',
);
$svg = $this->renderer()->svg(
'https://kurz.test/mitlogo',
(new QrOptions())->withLogoPath('logo.svg'),
);
self::assertStringContainsString('<image', $svg);
self::assertStringContainsString('data:image/svg+xml;base64,', $svg);
}
public function testDifferentColorsProduceDifferentImages(): void
{
$data = 'https://kurz.test/farben';
self::assertNotSame(
$this->renderer()->png($data, new QrOptions(foreground: '#000000')),
$this->renderer()->png($data, new QrOptions(foreground: '#0b4f9e')),
);
}
public function testOptionsAreClampedToValidRanges(): void
{
$options = new QrOptions(
foreground: 'unsinn',
size: 999,
margin: 99,
errorCorrection: 'X',
logoScale: 500,
);
self::assertSame('#000000', $options->foreground);
self::assertSame(512, $options->size);
self::assertSame(QrOptions::MAX_MARGIN, $options->margin);
self::assertSame('M', $options->errorCorrection);
self::assertSame(QrOptions::MAX_LOGO_SCALE, $options->logoScale);
}
private function writePngLogo(string $path): void
{
$image = imagecreatetruecolor(120, 120);
self::assertNotFalse($image);
imagefill($image, 0, 0, (int) imagecolorallocate($image, 255, 0, 0));
imagepng($image, $path);
imagedestroy($image);
}
}