skip('support_lockout() compare-and-set guard absent -- PR #7352 not merged into develop yet'); return; } $body = $m[0]; $body = preg_replace('/^\s*header\([^;]*\);\s*$/m', '', $body); $body = preg_replace('/^\s*cacti_log\([^;]*\);\s*$/m', '', $body); $body = preg_replace('/\bexit\s*;/', 'return;', $body); // This test has a real DB connection, so a forced read genuinely hits the // settings table -- unlike the Unit test, which needs this same rewrite to // dodge the PHP_TESTING sentinel. Here it keeps the admin identity on the // seeded config-cache value instead of the real (unseeded) 'admin_user' // row, so the test stays focused on the cacti_lockout_status round trip // this PR actually changes rather than on pre-existing admin bootstrapping. // The admin comparison's outcome is unaffected: it only changes where the // value is read from. $body = str_replace("read_config_option('admin_user', true)", "read_config_option('admin_user')", $body); $body = preg_replace('/^function support_lockout\(\)/m', 'function lockout_csrf_integration_probe()', $body); eval($body); } lockout_csrf_integration_define(); if (!function_exists('lockout_csrf_integration_probe')) { return; } function lockout_csrf_integration_connect(): mixed { global $database_hostname, $database_username, $database_password, $database_default, $database_port; if (!extension_loaded('pdo_mysql')) { return false; } $database_hostname = getenv('CACTI_TEST_DB_HOST') ?: '127.0.0.1'; $database_username = getenv('CACTI_TEST_DB_USER') ?: 'cacti'; $database_password = getenv('CACTI_TEST_DB_PASS') ?: 'cacti'; $database_default = getenv('CACTI_TEST_DB_NAME') ?: 'cacti'; $database_port = (int) (getenv('CACTI_TEST_DB_PORT') ?: 3306); // db_connect_real() caches the handle in $database_sessions keyed by // host:port:db; once that happens, set_config_option()/read_config_option() // pick it up on their own without any $db_conn argument, exactly as // support_lockout() calls them. return db_connect_real($database_hostname, $database_username, $database_password, $database_default, 'mysql', $database_port, 1); } beforeEach(function () { global $no_http_headers, $config; $no_http_headers = true; $_SESSION = []; $_REQUEST = []; $config[OPTIONS_CLI]['admin_user'] = '1'; }); test('a locked, then unlocked, maintenance toggle round-trips through the real settings table', function () { $conn = lockout_csrf_integration_connect(); if (!is_object($conn)) { test()->markTestSkipped('no reachable MySQL/MariaDB connection; start docker-compose or set CACTI_TEST_DB_* to run this test.'); } if (!db_table_exists('settings', false, $conn)) { test()->markTestSkipped('connected, but the Cacti schema is not present (settings missing); import cacti.sql to run this test.'); } $original = read_config_option('cacti_lockout_status', true); try { // Start from a known unlocked state so the compare-and-set below is // deterministic regardless of what a prior run left behind. set_config_option('cacti_lockout_status', ''); expect(read_config_option('cacti_lockout_status', true))->toBe(''); // A stale 'expected' must be rejected and must not touch the row. $_SERVER['REQUEST_METHOD'] = 'POST'; $_SESSION[SESS_USER_ID] = '1'; $_REQUEST['expected'] = 'locked'; lockout_csrf_integration_probe(); expect($_SESSION[SESS_MESSAGES])->toHaveKey('lockout') ->and($_SESSION[SESS_MESSAGES]['lockout']['level'])->toBe(MESSAGE_LEVEL_INFO) ->and(read_config_option('cacti_lockout_status', true))->toBe(''); // A matching 'expected' locks Cacti; verify the JSON payload actually // persisted to the real database, not just that a message was raised. $_SESSION = []; $_SESSION[SESS_USER_ID] = '1'; $_REQUEST['expected'] = 'unlocked'; lockout_csrf_integration_probe(); $locked = read_config_option('cacti_lockout_status', true); expect($locked)->not->toBe(''); $decoded = json_decode((string) $locked, true); expect($decoded)->toBeArray()->and($decoded)->toHaveKey('time'); // Unlocking (matching 'expected' = 'locked') clears the row back out. $_SESSION = []; $_SESSION[SESS_USER_ID] = '1'; $_REQUEST['expected'] = 'locked'; lockout_csrf_integration_probe(); expect(read_config_option('cacti_lockout_status', true))->toBe(''); } finally { // Leave a developer's local database exactly as found. set_config_option('cacti_lockout_status', (string) $original); } });