queryString)) ?: []); if (self::$failQuery !== '' && str_starts_with($query, self::$failQuery)) { throw new PDOException('Injected installer fixture read failure', 1142); } return parent::execute($params); } } function installer_fixture_check(bool $condition, string $message) : void { if (!$condition) { throw new RuntimeException($message); } } $scenario = $_SERVER['argv'][1] ?? ''; $key = "$database_hostname:$database_port:$database_default"; $pdo = $database_sessions[$key]; $version = db_fetch_cell('SELECT cacti FROM version'); $backups = []; try { if (in_array($scenario, ['domain-read', 'ldap-read', 'count-read'], true)) { require_once CACTI_PATH_INSTALL . '/upgrades/1_3_0.php'; $pdo->exec('CREATE TABLE user_domains (domain_id INT PRIMARY KEY, domain_name VARCHAR(64), type INT, enabled VARCHAR(2), debug VARCHAR(2), defdomain INT, user_id INT)'); $pdo->exec("INSERT INTO user_domains VALUES (987, 'Installer fixture', 1, 'on', '', 0, 0)"); $pdo->exec('CREATE TABLE user_domains_ldap (domain_id INT PRIMARY KEY)'); // LDAP reads fail before accessing the deliberately minimal fixture row. if ($scenario === 'ldap-read') { $pdo->exec('INSERT INTO user_domains_ldap VALUES (987)'); } $pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS, [InstallerFailureStatement::class]); InstallerFailureStatement::$failQuery = match ($scenario) { 'domain-read' => 'SELECT * FROM user_domains', 'ldap-read' => 'SELECT * FROM user_domains_ldap', 'count-read' => 'SELECT COUNT(*) FROM login_providers', }; $database_upgrade_status = []; $cacti_upgrade_version = '1.3.0'; login_providers_convert_1_3_0(); InstallerFailureStatement::$failQuery = ''; installer_fixture_check(count($pdo->query("SHOW TABLES LIKE 'user_domains%'")->fetchAll()) === 2, 'Failed read destroyed legacy authentication tables'); installer_fixture_check(in_array(DB_STATUS_ERROR, array_column($database_upgrade_status['1.3.0'] ?? [], 'status'), true), 'Failed read was not recorded in migration status'); installer_fixture_check((int) $pdo->query('SELECT COUNT(*) FROM user_domains')->fetchColumn() === 1, 'Legacy authentication row was lost'); } elseif ($scenario === 'forced-upgrade-replay') { // Probe live metadata after each child process repairs the schema. define('IN_CACTI_INSTALL', 1); installer_fixture_check(!db_table_exists('user_domains') && !db_table_exists('user_domains_ldap'), 'Fixture is not an already upgraded schema'); db_execute_prepared('UPDATE version SET cacti = ?', [CACTI_VERSION_FULL]); installer_fixture_check(db_execute_prepared('INSERT INTO login_providers (id, name, type, parameters) VALUES (?, ?, ?, ?)', [9876, 'Forced replay fixture', 3, '{"fixture":"preserve"}']), 'Could not seed the migrated provider fixture'); installer_fixture_check(db_execute_prepared('INSERT INTO user_auth (id, username, realm) VALUES (?, ?, ?)', [9876, 'forced-replay-fixture', 10876]), 'Could not seed the provider realm fixture'); installer_fixture_check(db_execute_prepared('INSERT INTO host_template (id, hash, name) VALUES (?, ?, ?)', [9876, '0123456789abcdef0123456789abcdef', 'Forced replay template fixture']), 'Could not seed the host template fixture'); installer_fixture_check(db_execute_prepared('INSERT INTO host (id, host_template_id, description, hostname, disabled) VALUES (?, ?, ?, ?, ?)', [9876, 9876, 'Forced replay host fixture', '127.0.0.1', 'on']), 'Could not seed the disabled host fixture'); $providers = db_fetch_assoc('SELECT * FROM login_providers ORDER BY id'); $users = db_fetch_assoc('SELECT id, username, realm FROM user_auth ORDER BY id'); $hosts = db_fetch_cell('SELECT COUNT(*) FROM host'); $templates = db_fetch_cell('SELECT COUNT(*) FROM host_template'); installer_fixture_check((int) $hosts > 0 && (int) $templates > 0, 'Monitoring fixture is not populated'); foreach (['1.2.31', '1.2.31', '1.2.28', '1.2.22'] as $forced_version) { // A successful replay must still repair current schema changes. db_execute('ALTER TABLE host DROP COLUMN snmp_retries'); $output = []; exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(CACTI_PATH_CLI . '/upgrade_database.php') . ' --forcever=' . escapeshellarg($forced_version) . ' 2>&1', $output, $status); installer_fixture_check($status === 0, 'Forced replay failed: ' . implode(PHP_EOL, $output)); installer_fixture_check(db_column_exists('host', 'snmp_retries', false), 'Forced replay skipped a required schema repair'); installer_fixture_check(db_fetch_cell('SELECT cacti FROM version') === CACTI_VERSION_FULL, 'Replay did not persist the current source version'); installer_fixture_check(db_fetch_assoc('SELECT * FROM login_providers ORDER BY id') === $providers, 'Replay changed migrated providers'); installer_fixture_check(db_fetch_assoc('SELECT id, username, realm FROM user_auth ORDER BY id') === $users, 'Replay changed authentication realms'); installer_fixture_check(db_fetch_cell('SELECT COUNT(*) FROM host') === $hosts && db_fetch_cell('SELECT COUNT(*) FROM host_template') === $templates, 'Replay lost populated monitoring data'); installer_fixture_check(!db_table_exists('user_domains') && !db_table_exists('user_domains_ldap'), 'Replay recreated obsolete authentication tables'); installer_fixture_check((int) db_fetch_cell("SELECT COUNT(*) FROM processes WHERE tasktype = 'install' AND taskname = 'master'") === 0, 'Replay retained the installer lock'); } } elseif ($scenario === 'forced-cli-failure') { $path = CACTI_PATH_INSTALL . '/upgrades/1_2_30.php'; $backups[$path] = is_file($path) ? file_get_contents($path) : false; file_put_contents($path, '&1', $output, $status); installer_fixture_check($status !== 0 && str_contains(implode(PHP_EOL, $output), 'not found'), 'Forced replay swallowed a genuine migration failure'); installer_fixture_check(db_fetch_cell('SELECT cacti FROM version') === CACTI_VERSION_FULL, 'Failed replay downgraded the installed version'); installer_fixture_check((int) db_fetch_cell("SELECT COUNT(*) FROM processes WHERE tasktype = 'install' AND taskname = 'master'") === 0, 'Failed replay retained the installer lock'); } elseif (in_array($scenario, ['upgrade-warning', 'cli-failure'], true)) { foreach (['1_2_29', '1_2_30'] as $target) { $path = CACTI_PATH_INSTALL . '/upgrades/' . $target . '.php'; $backups[$path] = is_file($path) ? file_get_contents($path) : false; } file_put_contents(CACTI_PATH_INSTALL . '/upgrades/1_2_29.php', 'newInstanceWithoutConstructor(); $reflection->getProperty('old_cacti_version')->setValue($installer, '1.2.28'); $error = $reflection->getMethod('upgradeDatabase')->invoke($installer); installer_fixture_check($error !== '', 'Incomplete migration reported success'); } else { $output = []; exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(CACTI_PATH_CLI . '/upgrade_database.php') . ' --forcever=1.2.28 2>&1', $output, $status); installer_fixture_check($status !== 0, 'CLI migration failure returned success'); installer_fixture_check(str_contains(implode(PHP_EOL, $output), 'not found'), 'CLI fixture did not exercise the missing migration'); } installer_fixture_check(db_fetch_cell('SELECT cacti FROM version') === '1.2.28', 'Incomplete migration advanced the recorded version'); } elseif ($scenario === 'composer-failure') { $composer = tempnam(sys_get_temp_dir(), 'installer-composer-'); installer_fixture_check($composer !== false, 'Could not create Composer fixture'); file_put_contents($composer, '#!' . PHP_BINARY . PHP_EOL . 'newInstanceWithoutConstructor(); $reflection->getMethod('refreshVendorDependencies')->invoke($installer); $log = substr(file_get_contents($logPath), $offset); installer_fixture_check(str_contains($log, 'Composer refresh did not complete'), 'Failed Composer command did not log a warning'); installer_fixture_check(!str_contains($log, 'Composer dependencies refreshed.'), 'Failed Composer command reported success'); unlink($composer); } elseif ($scenario === 'installer-lock') { installer_fixture_check(register_process_start('install', 'master', 0, 86400), 'Could not register lock fixture'); set_config_option('install_eula', 'lock-owner-fixture'); set_config_option('log_install_json', '3'); try { $output = []; exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(CACTI_PATH_CLI . '/upgrade_database.php') . ' --forcever=1.2.28 2>&1', $output, $status); installer_fixture_check($status !== 0 && str_contains(implode(PHP_EOL, $output), 'already in progress'), 'CLI upgrader ignored an active installer'); $arguments = ['--accept-eula', '--install', '--force', '--debug=json:5', '--path=php_binary:' . PHP_BINARY, '--path=rrdtool:/usr/bin/rrdtool', '--path=snmpwalk:/usr/bin/snmpwalk', '--path=snmpget:/usr/bin/snmpget', '--path=snmpbulkwalk:/usr/bin/snmpbulkwalk', '--path=snmpgetnext:/usr/bin/snmpgetnext', '--path=fping:/usr/bin/fping']; $output = []; exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(CACTI_PATH_CLI . '/install_cacti.php') . ' ' . implode(' ', array_map('escapeshellarg', $arguments)) . ' 2>&1', $output, $status); installer_fixture_check($status !== 0 && str_contains(implode(PHP_EOL, $output), 'already in progress'), 'CLI installer ignored an active installer'); installer_fixture_check(db_fetch_cell("SELECT value FROM settings WHERE name = 'install_eula'") === 'lock-owner-fixture', 'Blocked CLI installer overwrote the active installer settings'); installer_fixture_check(db_fetch_cell("SELECT value FROM settings WHERE name = 'log_install_json'") === '3', 'Blocked CLI installer overwrote the active installer logging settings'); } finally { unregister_process('install', 'master', 0); } } else { throw new RuntimeException('Unknown fixture scenario'); } print "PASS: $scenario" . PHP_EOL; } finally { InstallerFailureStatement::$failQuery = ''; foreach ($backups as $path => $contents) { if ($contents === false) { unlink($path); } else { file_put_contents($path, $contents); } } db_execute_prepared('UPDATE version SET cacti = ?', [$version]); }