Files
deskhub/DeskHub-Webseite/tests/plugin_update_migrations.php
2026-10-24 20:14:43 +00:00

115 lines
8.3 KiB
PHP

<?php
declare(strict_types=1);
/**
* Prüft, dass ein Update eines aktiven Plugins seine neuen Datenbank-Änderungen automatisch einspielt (vor dem Laden des Plugins),
* dass fehlerhafte Änderungen sichtbar werden statt still zu scheitern, und die Meldungen nach Installation und Update.
*/
$pluginDir = sys_get_temp_dir() . '/deskhub-um-' . bin2hex(random_bytes(4));
mkdir($pluginDir, 0755, true);
putenv('DESKHUB_PLUGINS_DIR=' . $pluginDir);
putenv('DESKHUB_TESTING=1');
putenv('DESKHUB_CATALOG_DEFAULTS=' . __DIR__ . '/fixtures/no-catalog.php');
putenv('DESKHUB_SQLITE_PATH=:memory:');
require_once dirname(__DIR__) . '/includes/bootstrap.php';
$failures = [];
$check = function (bool $ok, string $what) use (&$failures): void {
if (!$ok) $failures[] = $what;
};
$pdo = get_db();
$mkZip = function (string $version, array $migrations, array $perms = ['db.migrations', 'hooks.cron']) use ($pluginDir): string {
$p = sys_get_temp_dir() . '/um-' . bin2hex(random_bytes(3)) . '.zip';
$z = new ZipArchive();
$z->open($p, ZipArchive::CREATE);
$z->addFromString('updprobe/plugin.json', json_encode(['slug' => 'updprobe', 'name' => 'Update-Probe', 'version' => $version, 'permissions' => $perms]));
// Das Plugin liest beim Laden die neue Spalte: ohne eingespielte Änderung käme es zu einem Fehler
$z->addFromString('updprobe/plugin.php', "<?php\nplugin_add_hook('cron.run', function () {});\n\$GLOBALS['updprobe_loaded'] = (\$GLOBALS['updprobe_loaded'] ?? 0) + 1;\n");
foreach ($migrations as $name => $sql) $z->addFromString('updprobe/migrations/' . $name, $sql);
$z->close();
return $p;
};
$reboot = function (): void {
$state = &plugin_state();
$state['booted'] = false;
plugins_boot();
};
$cols = fn() => array_column($pdo->query('PRAGMA table_info(plugin_updprobe_items)')->fetchAll(), 'name');
// Version 1 mit Tabelle
$v1 = $mkZip('1.0.0', ['001_items.sqlite.sql' => 'CREATE TABLE IF NOT EXISTS plugin_updprobe_items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)', '001_items.mysql.sql' => 'CREATE TABLE IF NOT EXISTS plugin_updprobe_items (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL)']);
$r = plugin_install_zip($pdo, $v1);
$check($r['ok'] && !$r['updated'], 'Version 1 lässt sich installieren');
$check(plugin_install_message($r, plugins_installed($pdo)['updprobe']) === 'Update-Probe 1.0.0 installiert (nicht signiert). Es ist noch nicht aktiv.', 'Meldung bei Neuinstallation: ' . plugin_install_message($r, plugins_installed($pdo)['updprobe']));
$check(plugin_set_active($pdo, 'updprobe', true, ['db.migrations', 'hooks.cron'])['ok'], 'Version 1 lässt sich aktivieren');
$check($cols() === ['id', 'name'], 'Tabelle von Version 1 ist da');
$reboot();
$check(($GLOBALS['updprobe_loaded'] ?? 0) === 1, 'Plugin wird geladen');
$hash1 = $pdo->query("SELECT package_hash FROM plugins WHERE slug = 'updprobe'")->fetchColumn();
$check(get_setting('plugin_migrated.updprobe', '') === $hash1, 'Paketstand von Version 1 ist als eingespielt vermerkt');
$migrationsBefore = (int) $pdo->query("SELECT COUNT(*) FROM schema_migrations WHERE migration LIKE 'plugin_updprobe_%'")->fetchColumn();
// Version 2 bringt eine neue Spalte mit; das Plugin bleibt aktiv
$v2 = $mkZip('1.1.0', [
'001_items.sqlite.sql' => 'CREATE TABLE IF NOT EXISTS plugin_updprobe_items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)',
'001_items.mysql.sql' => 'CREATE TABLE IF NOT EXISTS plugin_updprobe_items (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL)',
'002_extra.sqlite.sql' => 'ALTER TABLE plugin_updprobe_items ADD COLUMN extra TEXT NULL',
'002_extra.mysql.sql' => 'ALTER TABLE plugin_updprobe_items ADD COLUMN extra VARCHAR(50) NULL',
]);
$r2 = plugin_install_zip($pdo, $v2);
$row = plugins_installed($pdo)['updprobe'];
$check($r2['ok'] && $r2['updated'] && (int) $row['is_active'] === 1, 'Update eines aktiven Plugins: bleibt aktiv');
$msg = plugin_install_message($r2, $row);
$check(str_contains($msg, 'auf Version 1.1.0 aktualisiert') && str_contains($msg, 'läuft weiter') && !str_contains($msg, 'noch nicht aktiv'), 'Meldung beim Update eines aktiven Plugins: ' . $msg);
$check(!in_array('extra', $cols(), true), 'unmittelbar nach dem Update ist die neue Spalte noch nicht da (läuft beim nächsten Aufruf)');
$reboot();
$check(in_array('extra', $cols(), true), 'beim nächsten Aufruf wird die neue Spalte automatisch eingespielt');
$hash2 = $pdo->query("SELECT package_hash FROM plugins WHERE slug = 'updprobe'")->fetchColumn();
$check($hash2 !== $hash1 && get_setting('plugin_migrated.updprobe', '') === $hash2, 'Paketstand von Version 2 ist vermerkt');
$check((int) $pdo->query("SELECT COUNT(*) FROM schema_migrations WHERE migration LIKE 'plugin_updprobe_%'")->fetchColumn() === $migrationsBefore + 1, 'nur die neue Änderung wurde eingespielt');
$reboot();
$check((int) $pdo->query("SELECT COUNT(*) FROM schema_migrations WHERE migration LIKE 'plugin_updprobe_%'")->fetchColumn() === $migrationsBefore + 1, 'weitere Aufrufe spielen nichts mehr ein');
$check((int) $pdo->query("SELECT fail_count FROM plugins WHERE slug = 'updprobe'")->fetchColumn() === 0, 'keine Fehler gezählt');
// Version 3 mit kaputter Änderung: Fehler wird sichtbar, das Plugin wird nicht stillschweigend weiterbenutzt
$v3 = $mkZip('1.2.0', [
'001_items.sqlite.sql' => 'CREATE TABLE IF NOT EXISTS plugin_updprobe_items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)',
'002_extra.sqlite.sql' => 'ALTER TABLE plugin_updprobe_items ADD COLUMN extra TEXT NULL',
'003_kaputt.sqlite.sql' => 'ALTER TABLE plugin_gibt_es_nicht ADD COLUMN x TEXT NULL',
]);
plugin_install_zip($pdo, $v3);
$reboot();
$p = plugins_installed($pdo)['updprobe'];
$check((int) $p['fail_count'] >= 1 && str_contains((string) $p['last_error'], 'Datenbank-Änderung fehlgeschlagen'), 'fehlerhafte Änderung wird gemeldet: ' . $p['last_error']);
$check(get_setting('plugin_migrated.updprobe', '') === $hash2, 'der kaputte Stand gilt nicht als eingespielt (nächster Versuch beim nächsten Aufruf)');
// Update mit neuer Berechtigung (nicht signiert): Plugin wird angehalten, Meldung sagt es
$v4 = $mkZip('1.3.0', ['001_items.sqlite.sql' => 'CREATE TABLE IF NOT EXISTS plugin_updprobe_items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)'], ['db.migrations', 'hooks.cron', 'ui.menu']);
plugin_set_active($pdo, 'updprobe', true, ['db.migrations', 'hooks.cron']);
$r4 = plugin_install_zip($pdo, $v4);
$check($r4['needs_reapproval'] === ['ui.menu'] && (int) plugins_installed($pdo)['updprobe']['is_active'] === 0, 'nicht signiertes Update mit neuer Berechtigung wird angehalten');
$check(str_contains(plugin_install_message($r4, plugins_installed($pdo)['updprobe']), 'zusätzliche Berechtigungen') && str_contains(plugin_install_message($r4, plugins_installed($pdo)['updprobe']), 'nicht aktiv'), 'Meldung nennt die zusätzlichen Berechtigungen und dass das Plugin nicht aktiv ist');
// Update eines inaktiven Plugins ohne neue Berechtigungen
plugin_set_active($pdo, 'updprobe', true, ['db.migrations', 'hooks.cron', 'ui.menu']);
plugin_set_active($pdo, 'updprobe', false);
$v5 = $mkZip('1.4.0', ['001_items.sqlite.sql' => 'CREATE TABLE IF NOT EXISTS plugin_updprobe_items (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)'], ['db.migrations', 'hooks.cron', 'ui.menu']);
$r5 = plugin_install_zip($pdo, $v5);
$check(str_contains(plugin_install_message($r5, plugins_installed($pdo)['updprobe']), 'weiterhin nicht aktiv'), 'Meldung beim Update eines inaktiven Plugins');
// Ohne Berechtigung db.migrations kein automatisches Einspielen
$check(!str_contains((string) file_get_contents(dirname(__DIR__) . '/includes/plugins.php'), "plugin_ensure_migrated(\$slug, (string) (\$row['package_hash'] ?? ''));\n") || str_contains((string) file_get_contents(dirname(__DIR__) . '/includes/plugins.php'), "in_array('db.migrations', \$manifest['permissions'], true)) plugin_ensure_migrated"), 'nur Plugins mit der Berechtigung db.migrations bekommen automatische Änderungen');
foreach (scandir($pluginDir) ?: [] as $entry) {
if ($entry === '.' || $entry === '..') continue;
is_dir($pluginDir . '/' . $entry) ? plugin_rmdir($pluginDir . '/' . $entry) : @unlink($pluginDir . '/' . $entry);
}
@rmdir($pluginDir);
foreach ([$v1, $v2, $v3, $v4, $v5] as $z) @unlink($z);
if ($failures) {
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
exit(1);
}
echo 'Plugin update migration checks passed.' . "\n";