OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

MonitoringTicketTest.php

10 KB
<?php

declare(strict_types=1);

use OpenNIT\Services\MonitoringTicketService;

/**
 * Deckt ab, wann aus einer Störung ein Ticket wird und wie es aussieht:
 * die Rangfolge der Schweregrade, die Dringlichkeit und der Aufbau von
 * Betreff und Beschreibung.
 */
return static function (): void {

    $quelle = static fn(string $pfad): string => (string)file_get_contents(dirname(__DIR__) . '/' . $pfad);

    /** Ruft eine private Aufbereitung des Dienstes auf – ohne Datenbank. */
    $ruf = static function (string $name, mixed ...$argumente): mixed {
        $dienst = (new \ReflectionClass(MonitoringTicketService::class))->newInstanceWithoutConstructor();
        $m = new \ReflectionMethod(MonitoringTicketService::class, $name);
        $m->setAccessible(true);
        return $m->invoke($dienst, ...$argumente);
    };

    $stoerung = [
        'id'           => 42,
        'source'       => 'zabbix',
        'host'         => 'srv-dc01',
        'hostname'     => 'Domaincontroller',
        'trigger_name' => 'Dienst „DNS" läuft nicht',
        'severity'     => 'high',
        'status'       => 'problem',
        'message'      => "Letzter Wert: down\nSeit 3 Minuten",
        'trigger_url'  => 'https://zabbix.example.com/tr_events.php?eventid=99',
        'created_at'   => '2026-09-17 08:15:00',
    ];

    test('Ab der Schwelle entsteht ein Ticket, darunter nicht', function () {
        assertTrue(MonitoringTicketService::severityReaches('disaster', 'high'));
        assertTrue(MonitoringTicketService::severityReaches('high', 'high'));
        assertFalse(MonitoringTicketService::severityReaches('average', 'high'));
        assertFalse(MonitoringTicketService::severityReaches('information', 'high'));
    });

    test('Die niedrigste Schwelle nimmt jede Störung', function () {
        foreach (MonitoringTicketService::SEVERITIES as $stufe) {
            assertTrue(
                MonitoringTicketService::severityReaches($stufe, 'not_classified'),
                $stufe . ' muss die unterste Schwelle erreichen'
            );
        }
    });

    test('Ein unbekanntes Wort erreicht keine Schwelle', function () {
        assertFalse(MonitoringTicketService::severityReaches('sonstiges', 'warning'));
        assertFalse(MonitoringTicketService::severityReaches('high', 'sonstiges'));
    });

    test('Die Dringlichkeit folgt dem Schweregrad', function () use ($ruf) {
        assertSame(1, $ruf('priority', 'disaster'));
        assertSame(2, $ruf('priority', 'high'));
        assertSame(2, $ruf('priority', 'average'));
        assertSame(3, $ruf('priority', 'warning'));
        assertSame(3, $ruf('priority', 'information'));
    });

    test('Der Betreff nennt Host und Trigger', function () use ($ruf, $stoerung) {
        assertSame('[Monitoring] srv-dc01: Dienst „DNS" läuft nicht', $ruf('subject', $stoerung));
    });

    test('Ohne Host bleibt der Trigger allein stehen', function () use ($ruf, $stoerung) {
        $ohneHost = array_merge($stoerung, ['host' => '', 'hostname' => '']);
        assertSame('[Monitoring] Dienst „DNS" läuft nicht', $ruf('subject', $ohneHost));
    });

    test('Der Betreff bleibt innerhalb der Spaltenbreite', function () use ($ruf, $stoerung) {
        $lang = array_merge($stoerung, ['trigger_name' => str_repeat('A', 900)]);
        assertTrue(mb_strlen((string)$ruf('subject', $lang)) <= 480);
    });

    test('Die Beschreibung führt die Eckdaten der Störung', function () use ($ruf, $stoerung) {
        $html = (string)$ruf('body', $stoerung);
        assertTrue(str_contains($html, 'srv-dc01'), 'Der Host fehlt');
        assertTrue(str_contains($html, 'Schweregrad: high'), 'Der Schweregrad fehlt');
        assertTrue(str_contains($html, 'Letzter Wert: down<br'), 'Die Meldung behält ihre Zeilen nicht');
        assertTrue(str_contains($html, 'href="https://zabbix.example.com/'), 'Der Verweis auf die Quelle fehlt');
    });

    test('Die Beschreibung führt zurück auf die Störung in OpenNIT', function () use ($ruf, $stoerung) {
        $html = (string)$ruf('body', $stoerung);
        assertTrue(str_contains($html, MonitoringTicketService::eventPath(42)), 'Der Rückweg in die Übersicht fehlt');
        assertTrue(str_contains($html, 'Ereignis #42'), 'Die Nummer der Störung fehlt');
    });

    test('Der Rückweg zeigt auf genau eine Störung', function () {
        assertSame('/monitoring?event=7', MonitoringTicketService::eventPath(7));
    });

    test('Fremde Zeichen in der Meldung bleiben Text', function () use ($ruf, $stoerung) {
        $boese = array_merge($stoerung, [
            'trigger_name' => '<script>alert(1)</script>',
            'message'      => '<img src=x onerror=alert(1)>',
        ]);
        $html = (string)$ruf('body', $boese);
        assertFalse(str_contains($html, '<script>'), 'Ein Skript-Tag darf nicht durchkommen');
        assertFalse(str_contains($html, '<img'), 'Ein Bild-Tag darf nicht durchkommen');
        assertTrue(str_contains($html, '&lt;script&gt;'), 'Der Text selbst bleibt sichtbar');
    });

    test('Der Vermerk am Ticket ist eine interne Notiz', function () use ($quelle) {
        // Eine interne Notiz lässt Status, Sortierung und den Zeitpunkt der
        // letzten Änderung unberührt und geht an niemanden hinaus – genau das
        // trennt den Vermerk vom Schließen des Tickets.
        $src = $quelle('src/Services/MonitoringTicketService.php');
        assertTrue(str_contains($src, "'is_internal' => 1,"), 'Der Vermerk ist keine interne Notiz');
        assertFalse(
            (bool)preg_match('/UPDATE\s+tickets\s+SET\s+status/i', $src),
            'Der Dienst darf den Status eines Tickets nicht setzen'
        );
        assertFalse(str_contains($src, 'closeTicket'), 'Der Dienst darf kein Ticket schließen');
        assertFalse(str_contains($src, 'sendEmail'), 'Der Vermerk darf nichts versenden');
    });

    test('Der Dienst kennt beide Richtungen einer Störung', function () use ($quelle) {
        $src = $quelle('src/Services/MonitoringTicketService.php');
        assertTrue(str_contains($src, 'Die Störung ist aufgelöst.'), 'Der Vermerk zur Auflösung fehlt');
        assertTrue(str_contains($src, 'Die Störung tritt wieder auf.'), 'Der Vermerk zum erneuten Auftreten fehlt');
        assertTrue(str_contains($src, 'Das Ticket bleibt offen'), 'Der Vermerk sagt nicht, dass das Ticket offen bleibt');
    });

    test('Jede Auflösung erreicht ihr Ticket', function () use ($quelle) {
        // Aufgeloest wird an drei Stellen: im Webhook, beim Abgleich ueber die
        // API und von Hand in der Uebersicht.
        $src = $quelle('src/Controllers/MonitoringController.php');
        assertSame(3, substr_count($src, '$this->noteResolvedTickets('), 'Ein Weg zur Auflösung vermerkt nichts');
    });

    // ----------------------------------------------------------------
    // Wiederholung desselben Vorfalls
    // ----------------------------------------------------------------

    test('Das Fenster hat eine Vorgabe und bleibt in Grenzen', function () use ($ruf) {
        assertSame(24, MonitoringTicketService::WINDOW_DEFAULT);
        // Ein Wert aus der Oberfläche darf die Abfrage nicht sprengen.
        assertTrue(str_contains(
            (string)file_get_contents(dirname(__DIR__) . '/src/Services/MonitoringTicketService.php'),
            'max(0, min(720, (int)$wert))'
        ), 'Das Fenster ist nicht begrenzt');
    });

    test('Nur ein offenes Ticket kommt als Wiederholung in Frage', function () use ($quelle) {
        // Ein gelöstes oder geschlossenes Ticket wird nie wieder geöffnet –
        // zwei Wochen später ist es ein neuer Vorfall.
        $src = $quelle('src/Services/MonitoringTicketService.php');
        assertTrue(str_contains($src, "private const ABGESCHLOSSEN = ['solved', 'closed'];"));
        assertTrue(str_contains($src, 'status NOT IN'), 'Der Ticketzustand wird nicht geprüft');
    });

    test('Dieselbe Störung heißt gleicher Host und gleicher Trigger', function () use ($quelle) {
        $src = $quelle('src/Services/MonitoringTicketService.php');
        $start = strpos($src, 'public function openTicketForRecurrence(');
        assertTrue($start !== false);
        $rumpf = substr($src, $start, 1800);

        assertTrue(str_contains($rumpf, 'host = ? AND trigger_name = ?'), 'Die Störung wird nicht eindeutig bestimmt');
        assertTrue(str_contains($rumpf, 'INTERVAL ? HOUR'), 'Das Fenster wirkt nicht');
        assertTrue(str_contains($rumpf, 'id <> ?'), 'Die Störung fände sich selbst');
        // Ohne beide Merkmale bleibt es ein eigener Vorfall.
        assertTrue(str_contains($rumpf, "if (\$host === '' || \$trigger === '')"), 'Unvollständige Angaben werden nicht abgefangen');
    });

    test('Ein abgeschaltetes Fenster erzeugt immer ein eigenes Ticket', function () use ($quelle) {
        $src = $quelle('src/Services/MonitoringTicketService.php');
        $start = strpos($src, 'public function openTicketForRecurrence(');
        $rumpf = substr($src, $start, 600);
        assertTrue(str_contains($rumpf, 'if ($fenster === 0)'), 'Der Aus-Zustand greift nicht');
    });

    test('Die Wiederholung vermerkt und legt nichts an', function () use ($quelle) {
        $src = $quelle('src/Services/MonitoringTicketService.php');
        $start = strpos($src, 'public function createOrAttach(');
        assertTrue($start !== false, 'Der gemeinsame Weg fehlt');
        $rumpf = substr($src, $start, 1400);

        assertTrue(str_contains($rumpf, '$this->noteReoccurred('), 'Die Wiederholung wird nicht vermerkt');
        assertTrue(str_contains($rumpf, "'attached' => true"), 'Der Aufrufer erfährt nicht, dass angehängt wurde');
        assertTrue(str_contains($rumpf, "'attached' => false"), 'Der Aufrufer erfährt nicht, dass neu angelegt wurde');
        // Auch die Wiederholung bekommt ihren Vermerk im Protokoll der
        // Alarm-Tickets, sonst zöge der Nachlauf ein zweites Ticket nach.
        assertTrue(str_contains($rumpf, 'INSERT INTO auto_ticket_log'), 'Die Zuordnung wird nicht festgehalten');
    });

    test('Nur http(s) wird zum Verweis', function () use ($ruf, $stoerung) {
        $js   = array_merge($stoerung, ['trigger_url' => 'javascript:alert(1)']);
        $html = (string)$ruf('body', $js);
        assertFalse(str_contains($html, 'javascript:'), 'Ein Programm-Schema darf nicht verlinkt werden');
    });
};