Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
/**
* Deckt ab, dass der Hypervisor-Abgleich verschwundene VMs kennzeichnet,
* statt sie zu loeschen – und dass nur vom Abgleich gefuehrte VMs betroffen
* sind.
*/
return static function (): void {
$wurzel = dirname(__DIR__);
$src = static fn (string $datei): string => (string)file_get_contents($wurzel . $datei);
// ----------------------------------------------------------------
// Abgleich
// ----------------------------------------------------------------
test('Der Abgleich loescht nichts von selbst', function () use ($src) {
// Gekennzeichnet wird, entschieden wird vom Betreiber.
$code = $src('/src/Hypervisor/HypervisorSyncService.php');
$von = strpos($code, 'private function markiereVermisste');
$block = substr($code, (int)$von, 1800);
assertTrue(str_contains($block, 'missing_since = NOW()'), 'Keine Kennzeichnung');
assertFalse((bool)preg_match('/\bDELETE\s+FROM\s+vm_vms\b/i', $block), 'Abgleich loescht VMs');
});
test('Von Hand angelegte VMs bleiben unberuehrt', function () use ($src) {
// Ohne hypervisor_vm_id fuehrt der Abgleich die VM nicht – sie kann
// also auch nicht vermisst werden.
$code = $src('/src/Hypervisor/HypervisorSyncService.php');
$von = strpos($code, 'private function markiereVermisste');
$block = substr($code, (int)$von, 1800);
assertTrue(str_contains($block, "hypervisor_vm_id IS NOT NULL AND hypervisor_vm_id <> ''"), 'Nicht gefuehrte VMs einbezogen');
// Beide Zweige – mit und ohne gemeldete VMs – nutzen dieselbe Einschraenkung.
assertSame(2, substr_count($block, '{$gefuehrt}'), 'Einschraenkung fehlt in einem Zweig');
});
test('Eine wieder aufgetauchte VM verliert die Kennzeichnung', function () use ($src) {
$code = $src('/src/Hypervisor/HypervisorSyncService.php');
// Im Abgleich selbst …
$von = strpos($code, 'private function markiereVermisste');
$block = substr($code, (int)$von, 1800);
assertTrue(str_contains($block, 'missing_since = NULL'), 'Kennzeichnung bleibt haengen');
// … und im Aktualisieren einer gemeldeten VM.
$upsert = substr($code, (int)strpos($code, 'private function upsertVm'), 1600);
assertTrue(str_contains($upsert, 'missing_since=NULL'), 'Gemeldete VM bleibt gekennzeichnet');
});
test('Der Zeitpunkt bleibt der erste Lauf ohne Meldung', function () use ($src) {
// Sonst stuende dort immer "heute" und man saehe nie, wie lange
// eine VM schon fehlt.
$code = $src('/src/Hypervisor/HypervisorSyncService.php');
$von = strpos($code, 'private function markiereVermisste');
$block = substr($code, (int)$von, 1800);
assertSame(2, substr_count($block, 'missing_since IS NULL'), 'Zeitpunkt wird ueberschrieben');
});
test('Meldet der Hypervisor gar nichts mehr, fehlen alle', function () use ($src) {
// Der Sonderfall braucht einen eigenen Zweig, weil NOT IN () kein
// gueltiges SQL ist.
$code = $src('/src/Hypervisor/HypervisorSyncService.php');
$von = strpos($code, 'private function markiereVermisste');
$block = substr($code, (int)$von, 1800);
assertTrue(str_contains($block, 'if ($gesehen === [])'), 'Leere Meldung nicht abgefangen');
});
test('Ein fehlgeschlagener Abgleich kennzeichnet nichts', function () use ($src) {
// Ein Verbindungsfehler wuerde sonst schlagartig alle VMs als
// verschwunden ausweisen.
$code = $src('/src/Hypervisor/HypervisorSyncService.php');
$von = strpos($code, 'public function syncHost');
// Der aeussere Fehlerzweig – erkennbar am Schreiben des Fehlerstatus.
$bis = strpos($code, "'UPDATE vm_hosts SET sync_status=?, sync_error=?", (int)$von);
$erfolg = substr($code, (int)$von, (int)$bis - (int)$von);
assertTrue(str_contains($erfolg, 'markiereVermisste'), 'Kennzeichnung nicht im Erfolgsfall');
// Im Fehlerzweig kommt sie nicht vor.
$fehler = substr($code, (int)$bis, 900);
assertFalse(str_contains($fehler, 'markiereVermisste'), 'Kennzeichnung im Fehlerfall');
assertTrue(str_contains($fehler, "'vms_missing' => 0"), 'Fehlerfall meldet eine Zahl');
});
test('Erst wird abgeglichen, dann gekennzeichnet', function () use ($src) {
$code = $src('/src/Hypervisor/HypervisorSyncService.php');
$von = strpos($code, 'public function syncHost');
$block = substr($code, (int)$von, 2600);
$upsert = strpos($block, '$this->upsertVm(');
$mark = strpos($block, 'markiereVermisste');
assertTrue($upsert !== false && $mark !== false, 'Eine Haelfte fehlt');
assertTrue($upsert < $mark, 'Kennzeichnung vor dem Abgleich');
});
// ----------------------------------------------------------------
// Aufraeumen
// ----------------------------------------------------------------
test('Aufgeraeumt wird nur, was auch vermisst wird', function () use ($src) {
// Eine mitgeschickte ID einer laufenden VM darf nichts loeschen.
$code = $src('/src/Content/VmManager.php');
$von = strpos($code, 'public function deleteMissingVms');
$block = substr($code, (int)$von, 1400);
assertTrue(str_contains($block, 'missing_since IS NOT NULL AND id IN'), 'Auswahl ungeprueft');
assertTrue(str_contains($block, '$this->deleteVm('), 'Kein Audit-Eintrag je VM');
});
test('Die Auswahl wird auf ganze Zahlen eingegrenzt', function () use ($src) {
$code = $src('/src/Content/VmManager.php');
$von = strpos($code, 'public function deleteMissingVms');
$block = substr($code, (int)$von, 700);
assertTrue(str_contains($block, "array_map('intval', \$ids)"), 'Keine Typumwandlung');
assertTrue(str_contains($block, 'if ($ids === [])'), 'Leere Auswahl nicht abgefangen');
});
// ----------------------------------------------------------------
// Oberflaeche
// ----------------------------------------------------------------
test('Die Aufraeum-Route steht vor der {id}-Route', function () use ($src) {
// Sonst matcht der Router {id}=missing.
$routen = $src('/src/Core/Application.php');
$missing = strpos($routen, "'/vms/missing'");
$byId = strpos($routen, "'/vms/{id}'");
assertTrue($missing !== false && $byId !== false, 'Eine Route fehlt');
assertTrue($missing < $byId, 'Reihenfolge vertauscht');
assertTrue(str_contains($routen, "'/vms/missing/cleanup'"), 'Keine Aufraeum-Route');
});
test('Das Aufraeumen verlangt das Recht zu aendern', function () use ($src) {
$routen = $src('/src/Core/Application.php');
$von = strpos($routen, "'/vms/missing/cleanup'");
$zeile = substr($routen, (int)$von, 220);
assertTrue(str_contains($zeile, "'capability' => 'manage_vms'"), 'Nur Lese-Recht verlangt');
$ctrl = $src('/src/Controllers/VmController.php');
$von = strpos($ctrl, 'public function vmsMissingCleanup');
assertTrue(str_contains(substr($ctrl, (int)$von, 400), "can('manage_vms')"), 'Controller prueft nicht');
});
test('Die Seite traegt den CSRF-Token und fragt nach', function () use ($src, $wurzel) {
assertTrue(is_file($wurzel . '/templates/vms/missing.php'), 'Keine Seite');
$tpl = $src('/templates/vms/missing.php');
assertSame(substr_count($tpl, '<form'), substr_count($tpl, 'name="_csrf"'), 'Formular ohne Token');
assertTrue(str_contains($tpl, 'confirm('), 'Loescht ohne Rueckfrage');
// Der Unterschied zwischen OpenNIT und Hypervisor wird benannt.
assertTrue(str_contains($tpl, 'nicht auf dem Hypervisor'), 'Wirkung nicht erklaert');
});
test('Die Seite nutzt nur zentrale Komponenten', function () use ($src) {
// Die vml-Klassen sind an den Style-Block von index.php gebunden und
// waeren hier ungestylt.
$tpl = $src('/templates/vms/missing.php');
foreach (['vml-wrap', 'vml-sub', 'vml-muted', 'vml-mono'] as $klasse) {
assertFalse(str_contains($tpl, 'class="' . $klasse), "Seitengebundene Klasse: {$klasse}");
}
assertTrue(str_contains($tpl, 'table-wrapper'), 'Keine zentrale Tabelle');
assertTrue(str_contains($tpl, 'status-pill'), 'Keine zentrale Auszeichnung');
});
test('Der Hinweis erscheint nur, wenn es etwas aufzuraeumen gibt', function () use ($src) {
$tpl = $src('/templates/vms/index.php');
$von = strpos($tpl, "(\$missing_count ?? 0) > 0");
assertTrue($von !== false, 'Kein bedingter Einstieg');
assertTrue(str_contains(substr($tpl, (int)$von, 400), '/vms/missing'), 'Kein Verweis');
$ctrl = $src('/src/Controllers/VmController.php');
assertTrue(str_contains($ctrl, "'missing_count' => \$mgr->countMissingVms()"), 'Zahl wird nicht geliefert');
});
test('Die Migration ist wiederholbar', function () use ($wurzel) {
$sql = (string)file_get_contents($wurzel . '/database/migrations/214_vm_missing_since.sql');
assertTrue(str_contains($sql, 'ADD COLUMN `missing_since`'), 'Spalte fehlt');
assertTrue(str_contains($sql, 'ADD KEY `idx_vmvm_missing`'), 'Kein Index');
assertFalse((bool)preg_match('/\bINSERT\s+INTO\b/i', $sql), 'Nacktes INSERT');
assertFalse(str_contains($sql, 'TRUNCATE'), 'Zerstoerendes Statement');
});
};