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.
164 lines
5.1 KiB
PHP
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);
|
|
}
|
|
}
|