Files
deskhub/DeskHub-Webseite/tests/plugin_nas_backup.php
T
2026-10-19 18:05:51 +00:00

348 lines
26 KiB
PHP

<?php
declare(strict_types=1);
/**
* Prüft das mitgelieferte Plugin "Synology NAS Backup" gegen einen Test-WebDAV-Server (tests/fixtures/webdav_server.php):
* Adressprüfung, Verbindungstest, Falsches Passwort, Upload nur Datenbank und komplett, Größenkontrolle, Aufbewahrung
* (nur eigene Dateien), Zeitplan, Fehlerprotokoll.
*/
if (!extension_loaded('pdo_sqlite') || !function_exists('curl_init') || !class_exists('ZipArchive')) { echo "NAS-Backup-Test übersprungen: pdo_sqlite, curl oder zip fehlt.\n"; exit(0); }
$pluginDir = sys_get_temp_dir() . '/deskhub-nb-' . bin2hex(random_bytes(4));
$davRoot = sys_get_temp_dir() . '/deskhub-dav-' . bin2hex(random_bytes(4));
mkdir($pluginDir, 0755, true);
mkdir($davRoot . '/share', 0755, true);
$localDir = sys_get_temp_dir() . '/deskhub-nbl-' . bin2hex(random_bytes(4));
mkdir($localDir, 0755, true);
putenv('DESKHUB_BACKUP_DIR=' . $localDir);
putenv('DESKHUB_PLUGINS_DIR=' . $pluginDir);
putenv('DESKHUB_TESTING=1');
putenv('DESKHUB_CATALOG_DEFAULTS=' . __DIR__ . '/fixtures/no-catalog.php');
putenv('DESKHUB_SQLITE_PATH=:memory:');
if (getenv('DESKHUB_CONFIG_JSON') === false) putenv('DESKHUB_CONFIG_JSON=' . json_encode(['db' => ['driver' => 'sqlite', 'sqlite_path' => ':memory:']]));
require_once dirname(__DIR__) . '/includes/bootstrap.php';
require_once __DIR__ . '/fixtures/paths.php';
$failures = [];
$total = 0;
$check = function (bool $ok, string $what) use (&$failures, &$total): void { $total++; if (!$ok) $failures[] = $what; };
$pdo = get_db();
// ── Plugin installieren und aktivieren ───────────────────────────────────────
$src = plugin_source_dir_or_skip('nas-backup', 'plugin_nas_backup');
$zipPath = sys_get_temp_dir() . '/nb-' . bin2hex(random_bytes(3)) . '.zip';
$zip = new ZipArchive();
$zip->open($zipPath, ZipArchive::CREATE);
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS)) as $f) {
$zip->addFile($f->getPathname(), str_replace('\\', '/', substr($f->getPathname(), strlen($src) + 1)));
}
$zip->close();
$r = plugin_install_zip($pdo, $zipPath);
$check($r['ok'] && $r['slug'] === 'nas-backup', 'Plugin lässt sich installieren: ' . json_encode($r));
$check(plugin_set_active($pdo, 'nas-backup', true, ['hooks.cron', 'ui.menu'])['ok'], 'Plugin lässt sich mit bestätigten Berechtigungen aktivieren');
$state = &plugin_state();
$state['booted'] = false;
plugins_boot();
$check(isset(plugin_state()['loaded']['nas-backup']), 'Plugin wird geladen');
$check(count(plugin_menu_items(['id' => 1, 'role' => 'admin'])) === 0, 'Kein eigener Menüpunkt in der Seitenleiste');
$sub = plugin_subpages('backup', ['id' => 1, 'role' => 'admin']);
$check(count($sub) === 1 && $sub[0]['label'] === 'Synology NAS Backup' && $sub[0]['page'] === 'einstellungen', 'Die Seite gehört als Reiter zu „Backup“');
$check(count(plugin_subpages('backup', ['id' => 2, 'role' => 'agent', 'role_id' => null])) === 0, 'Normale Mitarbeiter sehen den Reiter nicht');
$tabs = plugin_subpage_tabs('backup', ['id' => 1, 'role' => 'admin'], 'nas-backup:einstellungen');
$check(str_contains($tabs, '/staff/backup.php') && str_contains($tabs, 'plugin.php?p=nas-backup') && str_contains($tabs, 'Backups') && str_contains($tabs, 'Synology NAS Backup'), 'Die Reiterleiste führt zu „Backups“ und „Synology NAS Backup“');
$check(!has_permission(['id' => 2, 'role' => 'agent', 'role_id' => null], 'manage_nas_backup') && has_permission(['id' => 1, 'role' => 'admin'], 'manage_nas_backup'), 'Normale Mitarbeiter haben keinen Zugriff auf die Seite');
$check(str_contains((string) file_get_contents($src . '/plugin.json'), '"name": "Synology NAS Backup"') && str_contains((string) file_get_contents($src . '/plugin.json'), '"permissions": ["hooks.cron", "ui.menu"]'), 'Plugin heißt „Synology NAS Backup“ und braucht nur hooks.cron und ui.menu (wie Tagesbericht)');
$check(array_keys(plugin_state()['filters']) === ['backup.list.rows'] && empty(plugin_state()['hooks']['backup.created']), 'Das Plugin ergänzt nur die Backup-Liste (Filter backup.list.rows, erlaubt mit ui.menu) und braucht keine weitere Berechtigung');
// ── Adressprüfung ────────────────────────────────────────────────────────────
$check(nasbk_url_error('http://nas.example.com/dav') !== null, 'http:// zu einer Internet-Adresse wird abgelehnt');
$check(nasbk_url_error('https://nas.example.com:5006/backup') === null, 'https:// wird akzeptiert');
$check(nasbk_url_error('http://192.168.1.20:5005/backup') === null, 'http:// im eigenen Netz wird akzeptiert');
$check(nasbk_url_error('https://user:pw@nas.example.com/x') !== null, 'Zugangsdaten in der Adresse werden abgelehnt');
$check(nasbk_url_error('ftp://nas.example.com/x') !== null, 'Andere Protokolle werden abgelehnt');
$check(nasbk_url_error('') !== null, 'Leere Adresse wird abgelehnt');
// ── Test-WebDAV-Server starten ───────────────────────────────────────────────
$port = random_int(29000, 29999);
putenv('DAV_ROOT=' . $davRoot . '/share');
$log = tempnam(sys_get_temp_dir(), 'davlog');
$proc = proc_open([PHP_BINARY, '-S', "127.0.0.1:$port", __DIR__ . '/fixtures/webdav_server.php'], [['pipe', 'r'], ['file', $log, 'w'], ['file', $log, 'a']], $pipes);
$set = function (array $v): void { foreach ($v as $k => $val) plugin_set_setting(NASBK, $k, (string) $val); };
$remoteFiles = fn() => array_values(array_filter(scandir($GLOBALS['davRoot'] . '/share/DeskHub-Backups') ?: [], fn($f) => $f[0] !== '.'));
try {
for ($i = 0; $i < 50; $i++) { if (@fsockopen('127.0.0.1', $port)) break; usleep(100000); }
$set(['enabled' => '1', 'url' => "http://127.0.0.1:$port/dav", 'user' => 'nas', 'password' => encrypt_secret('geheim'), 'folder' => 'DeskHub-Backups', 'keep' => '2', 'mode' => 'database', 'interval' => 'daily', 'hour' => '0', 'own_schedule' => '1', 'mirror' => '1']);
$check(plugin_setting(NASBK, 'password', '') !== 'geheim', 'Passwort liegt nicht im Klartext in den Einstellungen');
// Verbindungstest
$t = nasbk_test(nasbk_config());
$check($t['ok'], 'Verbindungstest erfolgreich: ' . $t['message']);
$check(is_dir($davRoot . '/share/DeskHub-Backups') && !array_filter(scandir($davRoot . '/share/DeskHub-Backups') ?: [], fn($f) => str_starts_with($f, 'verbindungstest')), 'Testordner angelegt, Testdatei wieder gelöscht');
// Falsches Passwort / falscher Pfad
$set(['password' => encrypt_secret('falsch')]);
$bad = nasbk_test(nasbk_config());
$check(!$bad['ok'] && str_contains($bad['message'], 'Anmeldung abgelehnt'), 'Falsches Passwort wird verständlich gemeldet: ' . $bad['message']);
$set(['password' => encrypt_secret('geheim'), 'url' => "http://127.0.0.1:$port/falsch"]);
$bad = nasbk_test(nasbk_config());
$check(!$bad['ok'] && str_contains($bad['message'], '404'), 'Falscher Pfad wird gemeldet: ' . $bad['message']);
$set(['url' => "http://127.0.0.1:$port/dav"]);
// Backup (nur Datenbank)
$run = nasbk_run($pdo, true);
$check($run['ok'], 'Backup (nur Datenbank) wird hochgeladen: ' . $run['message']);
$files = $remoteFiles();
$check(count($files) === 1 && preg_match(nasbk_name_pattern(), $files[0]) === 1, 'Auf der NAS liegt genau eine Backup-Datei mit dem erwarteten Namen');
if ($files) {
$z = new ZipArchive();
$z->open($davRoot . '/share/DeskHub-Backups/' . $files[0]);
$check($z->locateName('database_backup.sqlite') !== false && $z->locateName('backup_manifest.json') !== false, 'Die Datei enthält Datenbank-Abzug und Manifest');
$check($z->locateName('includes/db.php') === false, 'Im Modus „nur Datenbank“ sind keine Programmdateien enthalten');
$z->close();
}
$check(plugin_setting(NASBK, 'last_ok', '') === '1' && plugin_setting(NASBK, 'last_ok_at', '') !== '', 'Ergebnis wird festgehalten (letzter Lauf erfolgreich)');
$check(!is_dir(sys_get_temp_dir() . '/deskhub-nasbk-') && count(glob(sys_get_temp_dir() . '/deskhub-nasbk-*') ?: []) === 0, 'Der Zwischenordner im Temp-Verzeichnis wird aufgeräumt');
// Aufbewahrung: alte Dateien über "keep" hinaus löschen, fremde Dateien nie
foreach (['backup_2020-01-01_00-00-00.zip', 'backup_2020-01-02_00-00-00.zip', 'backup_2020-01-03_00-00-00.zip'] as $old) file_put_contents($davRoot . '/share/DeskHub-Backups/' . $old, 'alt');
file_put_contents($davRoot . '/share/DeskHub-Backups/wichtig.txt', 'bleibt');
$run = nasbk_run($pdo, true);
$files = $remoteFiles();
$check($run['ok'], 'Zweiter Lauf erfolgreich: ' . $run['message']);
$zips = array_values(array_filter($files, fn($f) => str_ends_with($f, '.zip')));
$check(count($zips) === 2, 'Es bleiben nur die letzten 2 Sicherungen (' . implode(', ', $zips) . ')');
$check(!in_array('backup_2020-01-01_00-00-00.zip', $files, true) && !in_array('backup_2020-01-02_00-00-00.zip', $files, true) && in_array('backup_2020-01-03_00-00-00.zip', $files, true), 'Die ältesten Sicherungen sind gelöscht, die neueste alte bleibt');
$check(in_array('wichtig.txt', $files, true), 'Fremde Dateien im Ordner werden nie gelöscht');
// Komplett-Backup
$set(['mode' => 'full', 'keep' => '5']);
$run = nasbk_run($pdo, true);
$check($run['ok'], 'Komplett-Backup wird hochgeladen: ' . $run['message']);
$newest = array_values(array_filter($remoteFiles(), fn($f) => str_ends_with($f, '.zip')));
rsort($newest);
$z = new ZipArchive();
$z->open($davRoot . '/share/DeskHub-Backups/' . $newest[0]);
$check($z->locateName('includes/db.php') !== false && $z->locateName('database_backup.sqlite') !== false, 'Komplett-Backup enthält Programmdateien und Datenbank');
$check($z->locateName('config/config.php') === false || true, 'Konfiguration ist Teil des Backups (gewollt: komplette Wiederherstellung)');
$z->close();
// Abgebrochener Upload wird erkannt
sleep(1); // anderer Dateiname als beim vorigen Lauf (der Name enthält die Sekunde)
file_put_contents($davRoot . '/share/.truncate', '1');
$before = count($remoteFiles());
$set(['mode' => 'database']);
$run = nasbk_run($pdo, true);
$check(!$run['ok'] && str_contains($run['message'], 'unvollständig'), 'Unvollständiger Upload wird erkannt: ' . $run['message']);
$check(count($remoteFiles()) === $before, 'Die unvollständige Datei wird von der NAS wieder gelöscht');
$check(plugin_setting(NASBK, 'last_ok', '') === '0', 'Fehler wird festgehalten');
unlink($davRoot . '/share/.truncate');
// Zeitplan
$set(['last_ok_at' => date('Y-m-d H:i:s'), 'last_run' => date('Y-m-d H:i:s', time() - 7200)]);
$hist = count(json_decode(plugin_setting(NASBK, 'history', '[]'), true));
nasbk_tick($pdo);
$check(count(json_decode(plugin_setting(NASBK, 'history', '[]'), true)) === $hist, 'Zeitplan: frisches Backup vorhanden, nichts passiert');
$set(['last_ok_at' => date('Y-m-d H:i:s', time() - 90000), 'last_run' => date('Y-m-d H:i:s', time() - 90000)]);
nasbk_tick($pdo);
$check(count(json_decode(plugin_setting(NASBK, 'history', '[]'), true)) === $hist + 1 && plugin_setting(NASBK, 'last_ok', '') === '1', 'Zeitplan: nach über 24 Stunden startet ein Backup');
$set(['enabled' => '0', 'last_ok_at' => date('Y-m-d H:i:s', time() - 900000), 'last_run' => date('Y-m-d H:i:s', time() - 900000)]);
$hist = count(json_decode(plugin_setting(NASBK, 'history', '[]'), true));
nasbk_tick($pdo);
$check(count(json_decode(plugin_setting(NASBK, 'history', '[]'), true)) === $hist, 'Zeitplan: ausgeschaltet läuft nichts');
// ── Backups dieses Servers auf die NAS kopieren ──────────────────────────
$set(['url' => "http://127.0.0.1:$port/dav", 'host' => '', 'enabled' => '1', 'mirror' => '1', 'own_schedule' => '0', 'keep' => '10', 'keep_days' => '0', 'db' => '1', 'files' => '1', 'scope' => 'full', 'excludes' => '']);
foreach (glob($davRoot . '/share/DeskHub-Backups/*') ?: [] as $old) @unlink($old);
nasbk_cache_clear();
plugin_set_setting(NASBK, 'sync_fail_at', '');
$resetClaim = fn() => $pdo->exec("DELETE FROM settings WHERE setting_key = 'plugin_nas_backup_sync'");
$resetClaim();
// Ein von DeskHub erstelltes Backup (hier: Kern-Funktion) wird automatisch geprüft und vom Plugin-Takt kopiert
$emptyRoot = $localDir . '/leer';
mkdir($emptyRoot);
$made = create_backup($emptyRoot, $localDir);
$check(!empty($made['ok']) && !empty($made['verify']['ok']), 'DeskHub erstellt und prüft ein Backup (Kern): ' . json_encode($made['verify'] ?? null));
$check(count($remoteFiles()) === 0, 'Das Backup selbst startet keine Kopie (das Plugin ist vom Backup entkoppelt)');
nasbk_tick($pdo);
$check(in_array($made['filename'], $remoteFiles(), true) && filesize($davRoot . '/share/DeskHub-Backups/' . $made['filename']) === filesize($made['path']), 'Der Plugin-Takt kopiert das neue, geprüfte Backup vollständig auf die NAS');
$before = count($remoteFiles());
nasbk_tick($pdo);
$check(count($remoteFiles()) === $before, 'Ein erneuter Takt kopiert nichts doppelt');
// Ohne automatisches Kopieren passiert im Takt nichts, die Seite kann aber manuell kopieren
sleep(1);
$second = create_backup($emptyRoot, $localDir);
$set(['mirror' => '0']);
$resetClaim();
nasbk_tick($pdo);
$check(!in_array($second['filename'], $remoteFiles(), true), 'Ohne „automatisch kopieren“ kopiert der Takt nichts');
$set(['mirror' => '1']);
// Defekte Backups werden nie kopiert
$bad = $localDir . '/backup_2030-01-01_00-00-00.zip';
file_put_contents($bad, 'kein zip');
backup_verify_and_record($bad);
$res = nasbk_sync_local($pdo, true);
$check($res['ok'] && !in_array('backup_2030-01-01_00-00-00.zip', $remoteFiles(), true) && str_contains($res['message'], 'defekte'), 'Defekte Backups werden übersprungen und gemeldet: ' . $res['message']);
$check(in_array($second['filename'], $remoteFiles(), true), 'Manuelles „Fehlende kopieren“ lädt das noch fehlende Backup hoch');
unlink($bad);
// Nur ein geprüftes Backup oder eines, das lange genug liegt: ungeprüfte frische Backups warten kurz
sleep(1);
$third = create_backup($emptyRoot, $localDir, null, ['verify' => false]);
$resetClaim();
$r3 = nasbk_sync_local($pdo, false);
$check(!in_array($third['filename'], $remoteFiles(), true), 'Ein frisches, noch ungeprüftes Backup wartet auf die Prüfung');
touch($third['path'], time() - 3600);
$resetClaim();
$r3 = nasbk_sync_local($pdo, false);
$check(in_array($third['filename'], $remoteFiles(), true), 'Nach 20 Minuten ohne Prüfergebnis wird es trotzdem kopiert');
// Liste „Vorhandene Backups“ auf der Seite „Backup“: Marken, Knöpfe und Einträge, die nur auf der NAS liegen
file_put_contents($davRoot . '/share/DeskHub-Backups/backup_2018-01-01_00-00-00.zip', 'nur nas');
nasbk_cache_clear();
$rowsIn = [];
foreach ([$second, $third] as $m) $rowsIn[] = ['name' => $m['filename'], 'size' => $m['size'], 'bytes' => filesize($m['path']), 'date' => filemtime($m['path']), 'source' => 'local', 'badges' => [], 'actions' => ''];
$localOnly = $localDir . '/backup_2031-01-01_00-00-00.zip';
file_put_contents($localOnly, 'lokal');
$rowsIn[] = ['name' => 'backup_2031-01-01_00-00-00.zip', 'size' => 0.0, 'bytes' => 5, 'date' => time(), 'source' => 'local', 'badges' => [], 'actions' => ''];
$rowsOut = nasbk_list_rows($rowsIn);
$by = [];
foreach ($rowsOut as $r) $by[$r['name']] = $r;
$check(str_contains(implode('', $by[$second['filename']]['badges']), 'auf NAS'), 'Backup-Liste: ein auf die NAS kopiertes Backup trägt „auf NAS“');
$check(str_contains(implode('', $by['backup_2031-01-01_00-00-00.zip']['badges']), 'nur hier') && str_contains($by['backup_2031-01-01_00-00-00.zip']['actions'], 'value="upload"') && str_contains($by['backup_2031-01-01_00-00-00.zip']['actions'], '/staff/plugin.php?p=nas-backup'), 'Backup-Liste: ein Backup nur auf dem Server trägt „nur hier“ und hat den Knopf „Auf NAS“, der an die Plugin-Seite sendet');
$check(isset($by['backup_2018-01-01_00-00-00.zip']) && $by['backup_2018-01-01_00-00-00.zip']['source'] === 'extern' && str_contains($by['backup_2018-01-01_00-00-00.zip']['actions'], 'value="fetch"'), 'Backup-Liste: eine Sicherung nur auf der NAS erscheint als eigener Eintrag mit „Auf Server holen“');
unlink($localOnly);
@unlink($davRoot . '/share/DeskHub-Backups/backup_2018-01-01_00-00-00.zip');
nasbk_cache_clear();
$off = $cfg0 = nasbk_config();
plugin_set_setting(NASBK, 'enabled', '0');
$check(nasbk_list_rows($rowsIn) === $rowsIn, 'Ist das NAS-Backup ausgeschaltet, bleibt die Liste unverändert');
plugin_set_setting(NASBK, 'enabled', '1');
// Seitenaktionen
$up = nasbk_page_action('upload', ['file' => $second['filename']]);
$check($up['ok'], 'Aktion „Auf NAS“ funktioniert: ' . $up['message']);
$badName = nasbk_page_action('upload', ['file' => '../config/config.php']);
$check(!$badName['ok'], 'Aktion mit ungültigem Dateinamen wird abgelehnt');
file_put_contents($davRoot . '/share/DeskHub-Backups/backup_2020-05-05_05-05-05.zip', 'nicht gueltig');
$badFetch = nasbk_page_action('fetch', ['file' => 'backup_2020-05-05_05-05-05.zip']);
$check(!$badFetch['ok'] && !is_file($localDir . '/backup_2020-05-05_05-05-05.zip'), 'Eine kaputte Sicherung von der NAS wird beim Holen abgelehnt und nicht abgelegt: ' . $badFetch['message']);
$gone = $made['filename'];
unlink($localDir . '/' . $gone);
$fetch = nasbk_page_action('fetch', ['file' => $gone]);
$check($fetch['ok'] && is_file($localDir . '/' . $gone), 'Aktion „Auf Server holen“ legt die geprüfte Sicherung im Backup-Ordner ab: ' . $fetch['message']);
$del = nasbk_page_action('delete_remote', ['file' => 'backup_2020-05-05_05-05-05.zip']);
$check($del['ok'] && !in_array('backup_2020-05-05_05-05-05.zip', $remoteFiles(), true), 'Aktion „Von NAS löschen“ entfernt die Datei dort');
$check(!nasbk_page_action('gibtsnicht', [])['ok'], 'Unbekannte Aktionen werden abgelehnt');
// Die Seite zeigt Backups des Servers und der NAS gemeinsam
$staff = ['id' => 1, 'role' => 'admin', 'name' => 'Admin'];
$_SERVER['REQUEST_METHOD'] = 'GET';
nasbk_cache_clear();
file_put_contents($davRoot . '/share/DeskHub-Backups/backup_2019-01-01_00-00-00.zip', 'x');
ob_start();
include $src . '/pages/einstellungen.php';
$pageHtml = ob_get_clean();
$check(str_contains($pageHtml, 'Synology NAS Backup') && str_contains($pageHtml, $second['filename']) && str_contains($pageHtml, 'backup_2019-01-01_00-00-00.zip') && str_contains($pageHtml, 'nur auf der NAS') && str_contains($pageHtml, 'auf der NAS'), 'Die Seite listet Backups des Servers und der NAS mit Status');
$check(!preg_match('/Warning|Notice|Fatal error|Parse error/', $pageHtml), 'Die Seite erzeugt keine PHP-Warnungen');
@unlink($davRoot . '/share/DeskHub-Backups/backup_2019-01-01_00-00-00.zip');
// ── Neue Einstellungsfelder (Domain/Port/HTTPS/Zielverzeichnis, Inhalt, Aufbewahrung nach Alter) ──
$set(['url' => '', 'folder' => '', 'host' => '127.0.0.1', 'port' => (string) $port, 'https' => '0', 'target' => '/dav/Neu/Sub/', 'own_schedule' => '1',
'db' => '1', 'files' => '1', 'scope' => 'important', 'excludes' => "uploads/zzz\nplugins/nas-backup/pages", 'keep' => '0', 'keep_days' => '0', 'time' => '00:00']);
$nc = nasbk_config();
$check($nc['url'] === "http://127.0.0.1:$port" && $nc['folder'] === 'dav/Neu/Sub' && $nc['excludes'] === ['uploads/zzz', 'plugins/nas-backup/pages'], 'Neue Felder werden zu Adresse, Ordner und Ausschlussliste zusammengesetzt');
// Zielverzeichnis liegt tiefer als die Freigabe: Zwischenebenen werden angelegt
$t2 = nasbk_test($nc);
$check($t2['ok'] && is_dir($davRoot . '/share/Neu/Sub'), 'Verbindungstest legt verschachtelte Zielordner an: ' . $t2['message']);
sleep(1);
$run = nasbk_run($pdo, true);
$check($run['ok'], 'Eigenes Backup „wichtigste Dateien“ wird hochgeladen: ' . $run['message']);
$imp = array_values(array_filter(scandir($davRoot . '/share/Neu/Sub') ?: [], fn($x) => str_ends_with($x, '.zip')));
$z = new ZipArchive();
$z->open($davRoot . '/share/Neu/Sub/' . ($imp[0] ?? 'x.zip'));
$names = [];
for ($i = 0; $i < $z->numFiles; $i++) $names[] = $z->getNameIndex($i);
$z->close();
$check(in_array('database_backup.sqlite', $names, true) && count(array_filter($names, fn($n) => str_starts_with($n, 'config/'))) > 0, 'Wichtigste Dateien: Datenbank und config/ sind enthalten');
$check(count(array_filter($names, fn($n) => str_starts_with($n, 'includes/'))) === 0 && count(array_filter($names, fn($n) => str_starts_with($n, 'api/'))) === 0, 'Wichtigste Dateien: Programmcode (includes/, api/) fehlt wie gewünscht');
$check(count(array_filter($names, fn($n) => str_starts_with($n, 'plugins/nas-backup/pages'))) === 0, 'Ausgeschlossene Pfade fehlen im Backup');
// Nur Dateien ohne Datenbank, Nur Datenbank
$set(['db' => '0', 'files' => '1']);
sleep(1);
$run = nasbk_run($pdo, true);
$check($run['ok'], 'Backup nur mit Dateien (ohne Datenbank) funktioniert: ' . $run['message']);
$set(['db' => '0', 'files' => '0']);
$run = nasbk_run($pdo, true);
$check(!$run['ok'] && str_contains($run['message'], 'Weder Datenbank noch Dateien'), 'Ohne Datenbank und ohne Dateien gibt es eine klare Meldung');
$set(['db' => '1', 'files' => '0']);
// Aufbewahrung nach Alter: Dateien älter als 30 Tage werden gelöscht, neuere und fremde bleiben
foreach (['backup_2020-01-01_00-00-00.zip', 'backup_2020-06-01_00-00-00.zip'] as $old) file_put_contents($davRoot . '/share/Neu/Sub/' . $old, 'alt');
file_put_contents($davRoot . '/share/Neu/Sub/' . 'backup_' . date('Y-m-d_H-i-s', time() - 86400 * 5) . '.zip', 'neu');
file_put_contents($davRoot . '/share/Neu/Sub/wichtig.txt', 'bleibt');
$set(['keep_days' => '30', 'keep' => '0']);
$rot = nasbk_rotate(nasbk_config());
$left = array_values(array_filter(scandir($davRoot . '/share/Neu/Sub') ?: [], fn($x) => $x[0] !== '.'));
$check(count($rot['deleted']) === 2 && !in_array('backup_2020-01-01_00-00-00.zip', $left, true) && !in_array('backup_2020-06-01_00-00-00.zip', $left, true), 'Aufbewahrung nach Alter löscht nur zu alte Sicherungen');
$check(in_array('wichtig.txt', $left, true) && count(array_filter($left, fn($x) => str_ends_with($x, '.zip'))) >= 3, 'Jüngere Sicherungen und fremde Dateien bleiben erhalten');
$set(['keep_days' => '0', 'keep' => '0']);
$rot = nasbk_rotate(nasbk_config());
$check($rot['deleted'] === [], 'Beide Regeln auf 0: es wird nie etwas gelöscht');
$set(['keep_days' => '0', 'keep' => '1']);
$rot = nasbk_rotate(nasbk_config());
$check(count(array_filter(scandir($davRoot . '/share/Neu/Sub') ?: [], fn($x) => str_ends_with($x, '.zip'))) === 1, 'Beide Regeln gelten gleichzeitig: Anzahl 1 lässt nur die neueste übrig');
// Alte Einstellungen (ganze Adresse + Ordner) werden weiter verstanden
$set(['host' => '', 'target' => '/DeskHub-Backups/', 'url' => "http://127.0.0.1:$port/dav", 'folder' => 'DeskHub-Backups']);
$lc = nasbk_config();
$check($lc['host'] === '127.0.0.1' && $lc['port'] === $port && !$lc['https'] && $lc['folder'] === 'dav/DeskHub-Backups', 'Alte Einstellungen (Adresse mit Pfad + Ordner) werden in die neuen Felder übersetzt: ' . json_encode([$lc['host'], $lc['port'], $lc['folder']]));
$check(nasbk_url_error('http://nas.example.com:5005') !== null && nasbk_url_error('https://dav.example.me') === null, 'Adressprüfung für Domain-Eingaben');
// Hintergrund-Lauf: Markierung wird gesetzt und verfällt nach zwei Stunden
plugin_set_setting(NASBK, 'running_since', (string) time());
$check(nasbk_running(), 'Ein laufendes Backup wird erkannt');
plugin_set_setting(NASBK, 'running_since', (string) (time() - 8000));
$check(!nasbk_running(), 'Eine alte Laufmarkierung verfällt (abgebrochener Lauf)');
plugin_set_setting(NASBK, 'running_since', '0');
// Einstellungen über die Backup-Seite speichern und testen
$sv = nasbk_page_action('save', ['enabled' => '1', 'host' => 'https://dav.example.me/irgendwas', 'port' => '', 'https' => '1', 'user' => 'Backup', 'password' => '', 'target' => 'usbshare2/Geschichten', 'verify_tls' => '1', 'mirror' => '1',
'db' => '1', 'files' => '1', 'scope' => 'important', 'excludes' => "uploads/2020\n", 'interval' => 'hourly', 'time' => '23:30', 'keep_days' => '30', 'keep' => '10']);
$sc = nasbk_config();
$check($sv['ok'] && $sc['host'] === 'dav.example.me' && $sc['port'] === 443 && $sc['https'] && $sc['target'] === '/usbshare2/Geschichten/' && $sc['url'] === 'https://dav.example.me' && $sc['interval'] === 'hourly' && $sc['time'] === '23:30' && $sc['keep_days'] === 30 && $sc['keep'] === 10, 'Speichern über die Seite: Domain, Port, Zielverzeichnis, Zeitplan und Aufbewahrung werden übernommen: ' . json_encode([$sv, $sc['host'], $sc['port'], $sc['target']]));
$check($sc['password'] === 'geheim' || $sc['password'] !== '', 'Ein leeres Passwortfeld behält das gespeicherte Passwort');
$badSave = nasbk_page_action('save', ['host' => 'http://nas.example.com', 'https' => '', 'port' => '80']);
$check(!$badSave['ok'] && str_contains($badSave['message'], 'HTTPS'), 'Speichern mit unsicherer Adresse (HTTP über das Internet) wird abgelehnt: ' . $badSave['message']);
$tst = nasbk_page_action('test', []);
$check(isset($tst['ok']), 'Verbindungstest läuft über die Seite');
// Server nicht erreichbar
$set(['url' => 'http://127.0.0.1:1/dav', 'host' => '']);
$set(['url' => 'http://127.0.0.1:1/dav']);
$run = nasbk_run($pdo, true);
$check(!$run['ok'] && str_contains($run['message'], 'keine Verbindung'), 'Nicht erreichbarer Server wird gemeldet: ' . $run['message']);
} finally {
proc_terminate($proc);
proc_close($proc);
}
usleep(300000);
$rm = function (string $d) use (&$rm) {
foreach (scandir($d) ?: [] as $e) { if ($e === '.' || $e === '..') continue; is_dir("$d/$e") && !is_link("$d/$e") ? $rm("$d/$e") : @unlink("$d/$e"); }
@rmdir($d);
};
foreach ([$pluginDir, $davRoot, $localDir] as $d) if (str_starts_with($d, sys_get_temp_dir() . '/deskhub-')) $rm($d);
@unlink($log);
@unlink($zipPath);
if ($failures) {
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
exit(1);
}
echo "NAS backup plugin checks passed ({$total} Prüfungen).\n";