Quelltext des aktuellen Standes
<?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, '<script>'), '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');
});
};