Show a maintenance page instead of a blank 500 and update in one click
Emergency layer logs fatal errors to var/log/notfall.log and answers 503. The system page checks for a new release on load, shows its notes, runs backup, download, switch, migration and health check in one flow with live progress, and switches back automatically when the new version fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
339a48c810
commit
a6a334fe68
15 files changed
+973
-42
No files matched your search
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests;
|
||||
|
||||
use App\EventSubscriber\ErrorLogSubscriber;
|
||||
use App\Ops\Notfall;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
/**
|
||||
* Kein leerer 500er mehr (ADR 113).
|
||||
*
|
||||
* Kantarci Reisen am 09.10.2026: Datenbank per FTP ersetzt, danach eine weiße Seite auf jeder
|
||||
* Adresse und kein Hinweis darauf, warum. Seitdem sehen Besucher eine Wartungsseite mit 503, und
|
||||
* der Grund steht in var/log/notfall.log.
|
||||
*/
|
||||
final class NotfallTest extends TestCase
|
||||
{
|
||||
private string $root;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->root = str_replace('\\', '/', sys_get_temp_dir()).'/kant-notfall-'.bin2hex(random_bytes(6));
|
||||
mkdir($this->root, 0o775, true);
|
||||
Notfall::einschalten($this->root);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach (glob($this->root.'/var/log/*') ?: [] as $datei) {
|
||||
unlink($datei);
|
||||
}
|
||||
@rmdir($this->root.'/var/log');
|
||||
@rmdir($this->root.'/var');
|
||||
@rmdir($this->root);
|
||||
}
|
||||
|
||||
private function ereignis(\Throwable $fehler): ExceptionEvent
|
||||
{
|
||||
$kernel = $this->createStub(HttpKernelInterface::class);
|
||||
|
||||
return new ExceptionEvent($kernel, Request::create('/leistungen'), HttpKernelInterface::MAIN_REQUEST, $fehler);
|
||||
}
|
||||
|
||||
public function testAServerErrorBecomesAMaintenancePageAndALogLine(): void
|
||||
{
|
||||
$ursache = new \PDOException('file is not a database');
|
||||
$ereignis = $this->ereignis(new \RuntimeException('Abfrage fehlgeschlagen', 0, $ursache));
|
||||
|
||||
(new ErrorLogSubscriber(debug: false))->onException($ereignis);
|
||||
|
||||
$antwort = $ereignis->getResponse();
|
||||
self::assertNotNull($antwort);
|
||||
self::assertSame(503, $antwort->getStatusCode(), 'Für Suchmaschinen: gleich wieder da, nicht kaputt.');
|
||||
self::assertSame('300', $antwort->headers->get('Retry-After'));
|
||||
self::assertStringContainsString('Die Website ist gleich wieder da.', (string) $antwort->getContent());
|
||||
self::assertStringNotContainsString('database', (string) $antwort->getContent(), 'Einzelheiten gehören ins Protokoll, nicht auf die Seite.');
|
||||
$protokoll = (string) file_get_contents($this->root.'/'.Notfall::PROTOKOLL);
|
||||
self::assertStringContainsString('GET /leistungen – RuntimeException: Abfrage fehlgeschlagen', $protokoll);
|
||||
self::assertStringContainsString('Ursache: PDOException: file is not a database', $protokoll, 'Die eigentliche Ursache steckt eine Ebene tiefer.');
|
||||
}
|
||||
|
||||
public function testANotFoundIsNoEmergencyAndDevelopmentKeepsItsDetailedPage(): void
|
||||
{
|
||||
$nichtGefunden = $this->ereignis(new NotFoundHttpException('Gibt es nicht'));
|
||||
(new ErrorLogSubscriber(debug: false))->onException($nichtGefunden);
|
||||
self::assertNull($nichtGefunden->getResponse());
|
||||
self::assertFileDoesNotExist($this->root.'/'.Notfall::PROTOKOLL);
|
||||
|
||||
// „Zugriff verweigert“ führt zur Anmeldung - die Sicherheitsschicht entscheidet, nicht der Notfall.
|
||||
$verweigert = $this->ereignis(new \Symfony\Component\Security\Core\Exception\AccessDeniedException());
|
||||
(new ErrorLogSubscriber(debug: false))->onException($verweigert);
|
||||
self::assertNull($verweigert->getResponse());
|
||||
self::assertFileDoesNotExist($this->root.'/'.Notfall::PROTOKOLL);
|
||||
|
||||
$entwicklung = $this->ereignis(new \LogicException('Fehler beim Entwickeln'));
|
||||
(new ErrorLogSubscriber(debug: true))->onException($entwicklung);
|
||||
self::assertNull($entwicklung->getResponse(), 'Im Entwicklungsmodus bleibt die ausführliche Fehlerseite.');
|
||||
self::assertStringContainsString('LogicException: Fehler beim Entwickeln', (string) file_get_contents($this->root.'/'.Notfall::PROTOKOLL));
|
||||
}
|
||||
|
||||
public function testTheFrontControllerLoadsTheEmergencyLayerBeforeAnythingElse(): void
|
||||
{
|
||||
$einstieg = (string) file_get_contents(dirname(__DIR__).'/public/index.php');
|
||||
self::assertLessThan(strpos($einstieg, 'config/bootstrap.php'), strpos($einstieg, 'Notfall::einschalten'), 'Scheitert schon der Start, muss die Notfallschicht stehen.');
|
||||
self::assertStringContainsString('catch (Throwable $fehler)', $einstieg);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user