OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

StatusHistoryBackfillTest.php

7 KB
<?php

declare(strict_types=1);

use OpenNIT\Controllers\Admin\StatusAdminController;
use OpenNIT\Status\StatusManager;

/**
 * Deckt das rückwirkende Auffüllen der Verlaufsanzeige ab.
 *
 * Die Balken unter den Diensten zeigen 90 Tage; ohne erfasste Tage bleiben sie
 * leer. Die Aktion trägt diese Tage als „betriebsbereit" nach – und lässt dabei
 * alles unangetastet, was bereits erfasst ist: Eine protokollierte Störung ist
 * Teil der Historie und darf nicht nachträglich grün werden.
 */
return static function (): void {

    /** Der Rumpf der Aktion. */
    $aktion = static function (): string {
        $src = (string)file_get_contents(
            dirname(__DIR__) . '/src/Controllers/Admin/StatusAdminController.php'
        );
        $von = strpos($src, 'public function backfillHistory');
        return $von !== false ? substr($src, $von, 2200) : '';
    };

    // ----------------------------------------------------------------
    // Erreichbarkeit
    // ----------------------------------------------------------------

    test('Die Aktion liegt im Backend und verlangt das Admin-Recht', function () use ($aktion) {
        assertTrue(method_exists(StatusAdminController::class, 'backfillHistory'), 'Die Aktion fehlt');

        $app = (string)file_get_contents(dirname(__DIR__) . '/src/Core/Application.php');
        assertTrue(
            str_contains($app, "'/admin/statuspage/history/backfill'"),
            'Die Route fehlt'
        );
        assertTrue(str_contains($aktion(), "authorize('admin_statuspage')"), 'Keine Rechteprüfung');
    });

    test('Der Zeitraum entspricht der öffentlichen Anzeige', function () {
        // Die Seite zeigt getUptimeHistory(90); die Aktion muss denselben
        // Zeitraum füllen, sonst bleibt ein Teil des Balkens leer.
        $ctrl = (string)file_get_contents(dirname(__DIR__) . '/src/Controllers/Admin/StatusAdminController.php');
        $page = (string)file_get_contents(dirname(__DIR__) . '/src/Controllers/StatusController.php');

        assertTrue(str_contains($ctrl, 'private const HISTORY_DAYS = 90;'), 'Kein fester Zeitraum');
        assertTrue(str_contains($page, 'getUptimeHistory(90)'), 'Die Anzeige nutzt einen anderen Zeitraum');
    });

    // ----------------------------------------------------------------
    // Was geschrieben wird
    // ----------------------------------------------------------------

    test('Vorhandene Tage bleiben unverändert', function () use ($aktion) {
        // Der Doppelschlüssel (service_id, day) trifft erfasste Tage; die
        // Zuweisung auf sich selbst lässt die Zeile dann in Ruhe.
        $body = $aktion();

        assertTrue(
            str_contains($body, 'ON DUPLICATE KEY UPDATE service_id = status_service_history.service_id'),
            'Erfasste Tage würden überschrieben'
        );
        assertFalse(str_contains($body, 'DELETE FROM status_service_history'), 'Die Aktion löscht Historie');
        assertFalse(str_contains($body, 'UPDATE status_service_history SET'), 'Die Aktion ändert erfasste Tage');
    });

    test('Es entsteht genau ein Tag je Schritt, rückwärts bis heute', function () use ($aktion) {
        $body = $aktion();

        assertTrue(
            str_contains($body, 'for ($vorTagen = self::HISTORY_DAYS - 1; $vorTagen >= 0; $vorTagen--)'),
            'Der Zeitraum wird anders durchlaufen'
        );
        // 89 Tage zurück bis 0 = heute → 90 Tage einschließlich heute.
        assertTrue(str_contains($body, 'DATE_SUB(CURDATE(), INTERVAL ? DAY)'), 'Kein Tagesbezug');
    });

    test('Eingetragen wird betriebsbereit mit Schwere 0', function () use ($aktion) {
        assertTrue(str_contains($aktion(), "'operational', 0"), 'Es wird kein grüner Tag geschrieben');
    });

    test('Ohne Dienste passiert nichts', function () use ($aktion) {
        $body = $aktion();
        $leer = strpos($body, 'if (empty($dienste))');

        assertTrue($leer !== false, 'Der Fall wird nicht behandelt');
        assertTrue($leer < strpos($body, 'INSERT INTO status_service_history'), 'Es wird trotzdem geschrieben');
    });

    test('Der Vorgang wird protokolliert und der Cache verworfen', function () use ($aktion) {
        $body = $aktion();

        assertTrue(str_contains($body, "'status_history_backfilled'"), 'Kein Audit-Eintrag');
        assertTrue(str_contains($body, 'StatusManager::bustPublicCache()'), 'Die öffentliche Seite bliebe im Cache');
    });

    // ----------------------------------------------------------------
    // Die Regel selbst
    // ----------------------------------------------------------------

    /** Bildet nach, was ein Tag nach dem Auffüllen zeigt. */
    $auffuellen = static fn (?string $erfasst): string => $erfasst ?? 'operational';

    test('Ein leerer Tag wird grün', function () use ($auffuellen) {
        assertSame('operational', $auffuellen(null));
    });

    test('Ein Tag mit Störung behält seine Störung', function () use ($auffuellen) {
        foreach (['degraded', 'partial_outage', 'major_outage'] as $stand) {
            assertSame($stand, $auffuellen($stand), 'Überschrieben: ' . $stand);
        }
    });

    // ----------------------------------------------------------------
    // Gemeinsame Cache-Logik
    // ----------------------------------------------------------------

    test('Beide Controller verwerfen den Cache über dieselbe Stelle', function () {
        // Zwei Kopien des Pfades wären eine Quelle für stille Abweichung.
        assertTrue(method_exists(StatusManager::class, 'bustPublicCache'));

        foreach ([
            '/src/Controllers/StatusController.php',
            '/src/Controllers/Admin/StatusAdminController.php',
        ] as $datei) {
            $src = (string)file_get_contents(dirname(__DIR__) . $datei);
            assertTrue(str_contains($src, 'StatusManager::bustPublicCache()'), 'Nicht zentral: ' . $datei);
            assertFalse(
                str_contains($src, "/storage/cache/' . \$k . '.html'"),
                'Eigener Cache-Pfad in ' . $datei
            );
        }
    });

    // ----------------------------------------------------------------
    // Oberfläche
    // ----------------------------------------------------------------

    test('Der Knopf steht im Backend und fragt nach', function () {
        $tpl = (string)file_get_contents(dirname(__DIR__) . '/templates/admin/statuspage/settings.php');

        assertTrue(str_contains($tpl, 'action="/admin/statuspage/history/backfill"'), 'Kein Formular');
        assertTrue(str_contains($tpl, 'name="_csrf"'), 'Ohne CSRF-Feld');
        assertTrue(str_contains($tpl, 'onsubmit="return confirm('), 'Keine Rückfrage');
        assertTrue(str_contains($tpl, '<i class="fas fa-history"></i>'), 'Icon fehlt oder ohne Stil-Präfix');
    });

    test('Das Formular liegt nicht im Einstellungs-Formular', function () {
        // Verschachtelte Formulare sind ungültig; der Browser würde eines
        // davon verwerfen.
        $tpl   = (string)file_get_contents(dirname(__DIR__) . '/templates/admin/statuspage/settings.php');
        $ende  = strpos($tpl, '</form>');
        $aktion = strpos($tpl, 'action="/admin/statuspage/history/backfill"');

        assertTrue($ende !== false && $aktion !== false);
        assertTrue($aktion > $ende, 'Das Formular steht innerhalb des Einstellungs-Formulars');
    });

    test('Das Icon ist der Emoji-Umschaltung bekannt', function () {
        $map = (string)file_get_contents(dirname(__DIR__) . '/src/Core/IconMap.php');
        assertTrue(str_contains($map, "'fa-history'"), 'Nicht in der IconMap');
    });
};