Files
urlshort-qr-gen/tests/QrRendererTest.php
T
florian.wilde 649f6ba247 fix: Lauffaehigkeit unter PHP 8.4 und 8.5 herstellen
Auf einem Hetzner-Webhosting mit PHP 8.5 brach die Anwendung beim ersten
Aufruf ab. Ausloeser war session.sid_length: seit PHP 8.4 als veraltet
gemarkt. Die Meldung wurde ausgegeben, damit waren Header raus - und
danach liess sich weder eine ini-Einstellung aendern noch eine Session
starten. Aus einer Randnotiz wurde so ein Totalausfall.

Behoben auf drei Ebenen:

1. Die veralteten Aufrufe sind weg. session.sid_length und
   session.sid_bits_per_character werden nicht mehr gesetzt; die
   Voreinstellungen liefern mit 128 Bit ohnehin genug Entropie.
   Ebenso entfallen curl_close(), imagedestroy() und finfo_close() -
   seit PHP 8.0 bzw. 8.5 wirkungslos und als veraltet gemeldet.

2. public/index.php puffert die Ausgabe von der ersten Zeile an. Viele
   Hoster erzwingen display_errors und lassen sich per ini_set() nicht
   davon abbringen. Gepuffert laesst sich Fremdausgabe vor dem Senden
   verwerfen und ins Fehlerprotokoll umleiten, statt die Antwort zu
   zerstoeren. Das schuetzt auch Binaerdownloads: eine einzelne
   Meldung haette bisher jedes PNG beschaedigt. Session::start()
   meldet ausserdem klar, wenn doch schon Ausgaben raus sind, statt
   eine Kaskade aus Warnungen zu erzeugen.

3. endroid/qr-code von 5.1 auf 6.0 angehoben. Version 5 markiert
   Parameter implizit als nullable, was PHP 8.4 bemaengelt - bei jedem
   erzeugten QR-Code. Version 6.1 waere ebenfalls verfuegbar, verlangt
   aber PHP 8.4+ und wuerde Hoster mit 8.2 oder 8.3 ausschliessen;
   6.0 laeuft ab 8.1.

Getestet wird jetzt gegen PHP 8.3, 8.4 und 8.5, jeweils mit
display_errors=On und error_reporting=E_ALL, damit Deprecations
sichtbar werden statt still im Log zu landen. Alle 232 Tests laufen auf
allen drei Versionen ohne eine einzige Meldung durch. Zusaetzlich ist
das Release-Paket in einer PHP-8.5-Umgebung mit fremdem Webroot
geprueft: Ersteinrichtung, Kurzlinks, Weiterleitung, QR-Codes und
Statistik, ohne Streuausgabe in Seiten oder Downloads.
2026-08-12 18:01:30 +02:00

164 lines
5.1 KiB
PHP

<?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);
}
}