Files
urlshort-qr-gen/tests/DomainCheckerTest.php
florian.wilde 8a7939bee2 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
2026-08-03 18:11:40 +02:00

126 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests;
use App\Database;
use App\Migrator;
use App\Repository\SettingRepository;
use App\Service\DomainChecker;
use PHPUnit\Framework\TestCase;
/**
* Die vollstaendige Kette (Domain kommt bei dieser Installation an)
* laesst sich nur gegen einen laufenden Server pruefen - das passiert
* im End-to-End-Test. Hier geht es um die Teile, die ohne Netz
* eindeutig sind.
*/
final class DomainCheckerTest extends TestCase
{
private DomainChecker $checker;
private SettingRepository $settings;
protected function setUp(): void
{
$db = Database::sqlite(':memory:');
(new Migrator($db, dirname(__DIR__) . '/migrations'))->migrate();
$this->settings = new SettingRepository($db);
$this->checker = new DomainChecker($this->settings);
}
/**
* @param array<string,mixed> $extra
* @return array<string,mixed>
*/
private function domain(string $baseUrl, array $extra = []): array
{
return ['id' => 1, 'base_url' => $baseUrl, 'host' => '', 'is_default' => 0] + $extra;
}
public function testSecretIsStableAcrossCalls(): void
{
$first = $this->settings->secret();
self::assertSame($first, $this->settings->secret());
self::assertGreaterThanOrEqual(32, strlen($first));
}
public function testResultHasTheExpectedShape(): void
{
$result = $this->checker->check($this->domain('http://127.0.0.1:1'));
self::assertArrayHasKey('status', $result);
self::assertArrayHasKey('summary', $result);
self::assertArrayHasKey('checks', $result);
self::assertArrayHasKey('hint', $result);
self::assertContains($result['status'], [DomainChecker::OK, DomainChecker::WARNING, DomainChecker::ERROR]);
foreach ($result['checks'] as $check) {
self::assertArrayHasKey('label', $check);
self::assertArrayHasKey('status', $check);
self::assertArrayHasKey('text', $check);
self::assertNotSame('', $check['text']);
}
}
public function testIpAddressNeedsNoDnsEntry(): void
{
$result = $this->checker->check($this->domain('http://127.0.0.1:1'));
self::assertSame('DNS', $result['checks'][0]['label']);
self::assertSame(DomainChecker::OK, $result['checks'][0]['status']);
self::assertStringContainsString('kein DNS-Eintrag', $result['checks'][0]['text']);
}
public function testUnreachableDomainIsReportedAsError(): void
{
// Auf Port 1 lauscht nichts.
$result = $this->checker->check($this->domain('http://127.0.0.1:1'));
self::assertSame(DomainChecker::ERROR, $result['status']);
self::assertStringContainsString('nicht auf diese Installation', $result['summary']);
self::assertNotNull($result['hint'], 'Bei Misserfolg muss der Hairpinning-Hinweis erscheinen.');
}
public function testNonResolvableHostFails(): void
{
// ".invalid" ist laut RFC 2606 reserviert und existiert nie.
$result = $this->checker->check($this->domain('https://gibt-es-nicht.invalid'));
self::assertSame(DomainChecker::ERROR, $result['status']);
// Je nach Resolver scheitert schon DNS oder erst der Abruf -
// in beiden Faellen darf die Domain nicht als in Ordnung gelten.
$labels = array_column($result['checks'], 'label');
self::assertContains('DNS', $labels);
}
public function testPortIsStrippedBeforeDnsLookup(): void
{
// Enthielte der Hostname noch den Port, waere die Auflösung
// zwangslaeufig fehlgeschlagen.
$result = $this->checker->check($this->domain('http://localhost:1'));
self::assertSame('DNS', $result['checks'][0]['label']);
self::assertSame(DomainChecker::OK, $result['checks'][0]['status'], $result['checks'][0]['text']);
self::assertStringContainsString('localhost', $result['checks'][0]['text']);
self::assertStringNotContainsString('localhost:1', $result['checks'][0]['text']);
}
public function testCheckDoesNotThrowOnOddInput(): void
{
foreach ([
'http://127.0.0.1:1',
'https://gibt-es-nicht.invalid',
'http://localhost:1/unterverzeichnis',
] as $baseUrl) {
$result = $this->checker->check($this->domain($baseUrl));
self::assertIsArray($result, $baseUrl);
self::assertNotSame('', $result['summary'], $baseUrl);
}
}
}