From 9d8bf959f5b98f190a050b6fabbc9125e4223b5c Mon Sep 17 00:00:00 2001 From: TheWitness Date: Sat, 7 Mar 2026 12:57:00 -0500 Subject: [PATCH] fix: Fixing Unit tests (#6790) * fix: Fixing Unit tests * fix: Include helper remove the defines * Enable Pest tests for validation * Update tests/Unit/ScriptsHardeningTest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/Helpers/CactiStubs.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/Unit/SsHstatsTest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update rector.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/Unit/HtmlEscapeTest.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: Remove comments that are no longer required * fix: Move xdebug to the correct location * fix: Be consistent with includes --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/syntax.yml | 6 +- .phpstan-bootstrap.php | 2 +- composer.json | 3 +- include/global.php | 10 +- include/global_arrays.php | 2 +- lib/database.php | 2 +- lib/functions.php | 6 +- poller_recovery.php | 2 +- rector.php | 2 + scripts/ss_cpoller.php | 8 +- scripts/ss_hstats.php | 6 +- scripts/ss_mikrotik_health.php | 4 +- scripts/ss_mikrotik_interfaces.php | 6 +- scripts/ss_mikrotik_queues.php | 6 +- tests/Helpers/CactiStubs.php | 35 +------ tests/Unit/AggregatePtileTypeTest.php | 4 +- tests/Unit/AuthResetpasswordXssTest.php | 6 +- tests/Unit/ExecWithTimeoutTest.php | 28 +---- tests/Unit/GetClientAddrTest.php | 17 ++- tests/Unit/GlobalPathConstantsTest.php | 22 ++-- tests/Unit/GraphXportTest.php | 2 +- tests/Unit/HtmlEscapeTest.php | 67 +----------- tests/Unit/PhpstanTypeGuardTest.php | 3 +- tests/Unit/ScriptNameHelperTest.php | 2 + tests/Unit/ScriptsHardeningTest.php | 29 +++-- tests/Unit/SerializationSecurityTest.php | 128 +---------------------- tests/Unit/SsHstatsTest.php | 8 +- tests/Unit/StringFunctionsTest.php | 117 +-------------------- 28 files changed, 98 insertions(+), 435 deletions(-) diff --git a/.github/workflows/syntax.yml b/.github/workflows/syntax.yml index c1be2b545..25b383b95 100644 --- a/.github/workflows/syntax.yml +++ b/.github/workflows/syntax.yml @@ -108,9 +108,9 @@ jobs: run: composer run-script phpstan working-directory: . -# - name: Run Pest Unit Tests -# run: composer run-script test -# working-directory: . + - name: Run Pest Unit Tests + run: composer run-script test + working-directory: . - name: Install github if not already installed run: sudo apt-get -y install git diff --git a/.phpstan-bootstrap.php b/.phpstan-bootstrap.php index 4b06aaefe..1d896c742 100644 --- a/.phpstan-bootstrap.php +++ b/.phpstan-bootstrap.php @@ -1,3 +1,3 @@ ' : ''; $il = $config['is_web'] ? '' : ''; if ($config['poller_id'] > 1 || isset($rdatabase_hostname)) { - if (!defined('PHP_STAN')) { + if (!defined('PHP_TESTING')) { $local_db_cnn_id = db_connect_real($database_hostname, $database_username, $database_password, $database_default, $database_type, $database_port, $database_retries, $database_ssl, $database_ssl_key, $database_ssl_cert, $database_ssl_ca, $database_ssl_capath, $database_ssl_verify_server_cert); } @@ -363,7 +363,7 @@ if ($config['poller_id'] > 1 || isset($rdatabase_hostname)) { * a remote poller, let's attempt to get back online. */ if ($conn_mode != 'offline') { - if (!defined('PHP_STAN')) { + if (!defined('PHP_TESTING')) { $remote_db_cnn_id = db_connect_real($rdatabase_hostname, $rdatabase_username, $rdatabase_password, $rdatabase_default, $rdatabase_type, $rdatabase_port, $database_retries, $rdatabase_ssl, $rdatabase_ssl_key, $rdatabase_ssl_cert, $rdatabase_ssl_ca, $rdatabase_ssl_capath, $rdatabase_ssl_verify_server_cert); } } @@ -389,7 +389,7 @@ if ($config['poller_id'] > 1 || isset($rdatabase_hostname)) { $config['connection'] = 'offline'; } } else { - if (!defined('PHP_STAN')) { + if (!defined('PHP_TESTING')) { if (!db_connect_real($database_hostname, $database_username, $database_password, $database_default, $database_type, $database_port, $database_retries, $database_ssl, $database_ssl_key, $database_ssl_cert, $database_ssl_ca, $database_ssl_capath, $database_ssl_verify_server_cert)) { print $ps . 'FATAL: Connection to Cacti database failed. Please ensure: ' . $ul; print $li . 'the PHP MySQL module is installed and enabled.' . $il; @@ -410,7 +410,7 @@ if ($config['poller_id'] > 1 || isset($rdatabase_hostname)) { } } - if (!defined('PHP_STAN')) { + if (!defined('PHP_TESTING')) { if (!db_table_exists('settings') || !db_table_exists('version')) { print $ps . 'FATAL: Connection to Cacti database succeeded but `settings` table not found. Please ensure: ' . $ul; print $li . 'the PHP MySQL module is installed and enabled.' . $il; diff --git a/include/global_arrays.php b/include/global_arrays.php index 597a063a7..77baac694 100644 --- a/include/global_arrays.php +++ b/include/global_arrays.php @@ -3109,7 +3109,7 @@ if (CACTI_SERVER_OS == 'unix') { ]; } -$allowed_proxy_headers = [ +$allowed_proxy_headers = [ 'X-Forwarded-For', 'X-Client-IP', 'X-Real-IP', diff --git a/lib/database.php b/lib/database.php index dab15b772..a83fc15bd 100644 --- a/lib/database.php +++ b/lib/database.php @@ -162,7 +162,7 @@ function db_connect_real(string $device, string $user, string $pass, string $db_ ]; // test if cacti database is imported from SQL file - if (!defined('PHP_STAN')) { + if (!defined('PHP_TESTING')) { $table_exists = db_fetch_cell("SELECT count(*) FROM information_schema.tables WHERE table_schema = DATABASE() and TABLE_NAME = 'version'"); diff --git a/lib/functions.php b/lib/functions.php index 7d4b752a9..58103e00d 100644 --- a/lib/functions.php +++ b/lib/functions.php @@ -2247,10 +2247,10 @@ function is_hex_string(string &$result) : bool { * Hex- is considered due to the stripping of 'String:' in * lib/snmp.php */ - if (str_starts_with($compare, 'hex-')) { - $check = trim(str_ireplace('hex-', '', $result)); - } elseif (str_starts_with($compare, 'hex-string:')) { + if (str_starts_with($compare, 'hex-string:')) { $check = trim(str_ireplace('hex-string:', '', $result)); + } elseif (str_starts_with($compare, 'hex-')) { + $check = trim(str_ireplace('hex-', '', $result)); } else { return false; } diff --git a/poller_recovery.php b/poller_recovery.php index b3bb2946d..14ffb9f29 100755 --- a/poller_recovery.php +++ b/poller_recovery.php @@ -145,7 +145,7 @@ if (cacti_sizeof($parms)) { } // check for an invalid run location -if ($poller_id == 1 && !defined('PHP_STAN')) { +if ($poller_id == 1 && !defined('PHP_TESTING')) { print "ERROR: This command is only to be run on remote Cacti Data Collectors\n"; exit(1); diff --git a/rector.php b/rector.php index f2309f6ae..8d00fa8b8 100644 --- a/rector.php +++ b/rector.php @@ -2,6 +2,8 @@ declare(strict_types = 1); +include(__DIR__ . '/include/vendor/autoload.php'); + use Rector\Config\RectorConfig; use Rector\Php54\Rector\Array_\LongArrayToShortArrayRector; diff --git a/scripts/ss_cpoller.php b/scripts/ss_cpoller.php index a96bd25bd..fa6a71c37 100755 --- a/scripts/ss_cpoller.php +++ b/scripts/ss_cpoller.php @@ -65,11 +65,11 @@ function ss_cpoller(string $cmd = 'index', string $arg1 = '', string $arg2 = '') } elseif ($cmd == 'get') { $arg = $arg1; $index = $arg2; - $value = '0'; + $value = 'U'; switch($arg) { case 'recacheTime': - $value = '0'; + $value = 'U'; $stats = explode(' ', db_fetch_cell_prepared('SELECT value FROM settings WHERE name = ?', ['stats_recache_' . $index])); foreach ($stats as $_stat) { @@ -81,7 +81,7 @@ function ss_cpoller(string $cmd = 'index', string $arg1 = '', string $arg2 = '') break; case 'recacheDevices': - $value = '0'; + $value = 'U'; $stats = explode(' ', db_fetch_cell_prepared('SELECT value FROM settings WHERE name = ?', ['stats_recache_' . $index])); foreach ($stats as $_stat) { @@ -167,7 +167,7 @@ function ss_cpoller(string $cmd = 'index', string $arg1 = '', string $arg2 = '') break; } - return ($value == '' ? '0' : $value); + return ($value == '' ? 'U' : $value); } return 'U'; diff --git a/scripts/ss_hstats.php b/scripts/ss_hstats.php index 711fc2772..cad0229a7 100755 --- a/scripts/ss_hstats.php +++ b/scripts/ss_hstats.php @@ -62,13 +62,13 @@ function ss_hstats(int $host_id = 0, string $stat = '') : string { $column = ss_hstats_map_stat_to_column($stat); if ($column === null) { - return '0'; + return 'U'; } $allowed = ['polling_time', 'min_time', 'max_time', 'cur_time', 'avg_time', 'snmp_sysUpTimeInstance', 'failed_polls', 'availability', 'current_errors']; if (!in_array($column, $allowed, true)) { - return '0'; + return 'U'; } if ($host_id > 0) { @@ -82,5 +82,5 @@ function ss_hstats(int $host_id = 0, string $stat = '') : string { return ($value == '' ? 'U' : $value); } - return '0'; + return 'U'; } diff --git a/scripts/ss_mikrotik_health.php b/scripts/ss_mikrotik_health.php index 900de651b..cd81cc126 100644 --- a/scripts/ss_mikrotik_health.php +++ b/scripts/ss_mikrotik_health.php @@ -31,11 +31,11 @@ if (!isset($called_by_script_server)) { print call_user_func_array('ss_mikrotik_health', $_SERVER['argv']); } -function ss_mikrotik_health(int $host_id = 0, string $column = 'no') : string { +function ss_mikrotik_health(mixed $host_id = 0, string $column = 'no') : string { $allowed_columns = ['voltage', 'temperature', 'processorTemperature', 'current', 'power', 'fanSpeed', 'cpuFrequency']; if (!in_array($column, $allowed_columns, true)) { - return '0'; + return 'U'; } $value = db_fetch_cell_prepared("SELECT $column diff --git a/scripts/ss_mikrotik_interfaces.php b/scripts/ss_mikrotik_interfaces.php index d74fba3fb..a248f5ca9 100755 --- a/scripts/ss_mikrotik_interfaces.php +++ b/scripts/ss_mikrotik_interfaces.php @@ -59,7 +59,7 @@ function ss_mikrotik_interfaces(int $host_id, string $cmd = 'index', string $arg function ss_mikrotik_interfaces_getvalue(int $host_id, string $index, string $column) : string { if (!preg_match('/^[a-zA-Z0-9_]+$/', $column)) { - return '0'; + return 'U'; } $column = 'cur' . $column; @@ -72,7 +72,7 @@ function ss_mikrotik_interfaces_getvalue(int $host_id, string $index, string $co ]; if (!in_array($column, $allowed_columns, true)) { - return '0'; + return 'U'; } $index2 = str_replace('_', ' ', $index); @@ -85,7 +85,7 @@ function ss_mikrotik_interfaces_getvalue(int $host_id, string $index, string $co [$index, $index2, $host_id]); if ($value === false) { - return '0'; + return 'U'; } else { return $value; } diff --git a/scripts/ss_mikrotik_queues.php b/scripts/ss_mikrotik_queues.php index df0f4483c..b18ca4f08 100755 --- a/scripts/ss_mikrotik_queues.php +++ b/scripts/ss_mikrotik_queues.php @@ -92,13 +92,13 @@ function ss_mikrotik_queues_getvalue(int $host_id, string $index, string $column break; default: - return '0'; + return 'U'; } $allowed_columns = ['curBytesIn', 'curBytesOut', 'curPacketsIn', 'curPacketsOut', 'curQueuesIn', 'curQueuesOut', 'curDroppedIn', 'curDroppedOut']; if (!in_array($column, $allowed_columns, true)) { - return '0'; + return 'U'; } $index2 = str_replace('_', ' ', $index); @@ -111,7 +111,7 @@ function ss_mikrotik_queues_getvalue(int $host_id, string $index, string $column [$index, $index2, $host_id]); if ($value === false) { - return '0'; + return 'U'; } else { return $value; } diff --git a/tests/Helpers/CactiStubs.php b/tests/Helpers/CactiStubs.php index 3d9286f08..690a79f26 100644 --- a/tests/Helpers/CactiStubs.php +++ b/tests/Helpers/CactiStubs.php @@ -13,35 +13,10 @@ */ /* - * Shared stubs for Cacti helper functions used by production code. - * These replicate the production signatures so that test stubs - * can call them without loading the full Cacti environment. + * We set a testing variable such that Cacti will not attempt + * to connect to a database when the tests are called, or to call + * any function that will result in the failure due to the lack + * of a real database connection. */ -function cacti_sizeof($value): int { - if (is_array($value)) { - return sizeof($value); - } elseif ($value instanceof Countable) { - return count($value); - } - - return 0; -} - -function cacti_count($value): int { - if (is_array($value)) { - return count($value); - } elseif ($value instanceof Countable) { - return count($value); - } - - return 0; -} - -function __($text) { - return $text; -} - -function __esc($text) { - return htmlspecialchars($text, ENT_QUOTES, 'UTF-8'); -} +define('PHP_TESTING', true); diff --git a/tests/Unit/AggregatePtileTypeTest.php b/tests/Unit/AggregatePtileTypeTest.php index ded05c071..46627e277 100644 --- a/tests/Unit/AggregatePtileTypeTest.php +++ b/tests/Unit/AggregatePtileTypeTest.php @@ -29,8 +29,8 @@ * 3. HRULE item pparts[3] replacement (aggregate_graphs_insert_graph_items) */ -define('AGGREGATE_TOTAL_TYPE_SIMILAR', 1); -define('AGGREGATE_TOTAL_TYPE_ALL', 2); +require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; +require_once dirname(__DIR__, 2) . '/include/global.php'; /** * Mimics the text_format :current: / :max: replacement logic from diff --git a/tests/Unit/AuthResetpasswordXssTest.php b/tests/Unit/AuthResetpasswordXssTest.php index 89a367f1b..b4a1249ac 100644 --- a/tests/Unit/AuthResetpasswordXssTest.php +++ b/tests/Unit/AuthResetpasswordXssTest.php @@ -16,15 +16,15 @@ * Tests for XSS fix in auth_resetpassword.php. * * The hash value in hidden form inputs was output without escaping. The fix - * wraps gnrv('hash') with htmle() to prevent XSS when the hash is reflected. + * wraps $_REQUEST['hash'] with htmlerv() to prevent XSS when the hash is reflected. */ $authPath = __DIR__ . '/../../auth_resetpassword.php'; // --- auth_resetpassword.php: hash escaped in output --- -test('auth_resetpassword.php escapes hash with htmle', function () use ($authPath) { +test('auth_resetpassword.php escapes hash with htmlerv', function () use ($authPath) { $contents = file_get_contents($authPath); - expect($contents)->toContain("htmle(gnrv('hash'))"); + expect($contents)->toContain("htmlerv('hash')"); }); diff --git a/tests/Unit/ExecWithTimeoutTest.php b/tests/Unit/ExecWithTimeoutTest.php index ee071e777..324123211 100644 --- a/tests/Unit/ExecWithTimeoutTest.php +++ b/tests/Unit/ExecWithTimeoutTest.php @@ -12,32 +12,8 @@ +-------------------------------------------------------------------------+ */ -/* - * Tests for exec_with_timeout() in lib/poller.php. - * - * Covers three bugs fixed in commit c58f8d940: - * 1. Operator precedence: (int) cast bound tighter than multiplication, - * truncating the elapsed-time float to 0 before scaling to microseconds. - * 2. stderr discard: any stderr output caused an early `return false`, - * throwing away valid stdout data. - * 3. Orphaned children: no process-group kill before proc_terminate. - */ - -// Stub cacti_log so lib/poller.php can call it without the full framework. -if (!function_exists('cacti_log')) { - function cacti_log(string $msg, bool $output = false, string $facility = '', int $level = 0): void { - // no-op for tests - } -} - -if (!defined('POLLER_VERBOSITY_MEDIUM')) { - define('POLLER_VERBOSITY_MEDIUM', 2); -} - -if (!defined('CACTI_SERVER_OS')) { - define('CACTI_SERVER_OS', PHP_OS === 'WINNT' ? 'win32' : 'unix'); -} - +require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; +require_once dirname(__DIR__, 2) . '/include/global.php'; require_once dirname(__DIR__, 2) . '/lib/poller.php'; // --- Bug 1: operator precedence in timeout microsecond conversion --- diff --git a/tests/Unit/GetClientAddrTest.php b/tests/Unit/GetClientAddrTest.php index 6033a35c9..c274a1624 100644 --- a/tests/Unit/GetClientAddrTest.php +++ b/tests/Unit/GetClientAddrTest.php @@ -12,7 +12,8 @@ +-------------------------------------------------------------------------+ */ -require_once __DIR__ . '/../../lib/functions.php'; +require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; +require_once dirname(__DIR__, 2) . '/include/global.php'; beforeEach(function () { unset( @@ -27,6 +28,7 @@ beforeEach(function () { test('returns REMOTE_ADDR when proxy_headers is false', function () { global $config; + $config['proxy_headers'] = false; $_SERVER['REMOTE_ADDR'] = '192.168.1.100'; @@ -36,6 +38,7 @@ test('returns REMOTE_ADDR when proxy_headers is false', function () { test('returns false when no server variables are set', function () { global $config; + $config['proxy_headers'] = false; expect(get_client_addr())->toBeFalse(); @@ -43,6 +46,9 @@ test('returns false when no server variables are set', function () { test('returns first valid IP from X-Forwarded-For when proxy_headers enabled', function () { global $config, $allowed_proxy_headers; + + require __DIR__ . '/../../include/global_arrays.php'; + $config['proxy_headers'] = true; $_SERVER['HTTP_X_FORWARDED_FOR'] = '10.0.0.1,10.0.0.2'; @@ -53,6 +59,9 @@ test('returns first valid IP from X-Forwarded-For when proxy_headers enabled', f test('skips invalid IPs in forwarded header', function () { global $config, $allowed_proxy_headers; + + require __DIR__ . '/../../include/global_arrays.php'; + $config['proxy_headers'] = true; $_SERVER['HTTP_X_FORWARDED_FOR'] = 'not-an-ip,203.0.113.50'; @@ -63,6 +72,9 @@ test('skips invalid IPs in forwarded header', function () { test('falls back to REMOTE_ADDR when forwarded header has only invalid IPs', function () { global $config, $allowed_proxy_headers; + + require __DIR__ . '/../../include/global_arrays.php'; + $config['proxy_headers'] = true; $_SERVER['HTTP_X_FORWARDED_FOR'] = 'garbage'; @@ -73,6 +85,7 @@ test('falls back to REMOTE_ADDR when forwarded header has only invalid IPs', fun test('ignores proxy headers when proxy_headers is false', function () { global $config; + $config['proxy_headers'] = false; $_SERVER['HTTP_X_FORWARDED_FOR'] = '10.0.0.1'; @@ -83,6 +96,7 @@ test('ignores proxy headers when proxy_headers is false', function () { test('supports IPv6 addresses', function () { global $config; + $config['proxy_headers'] = false; $_SERVER['REMOTE_ADDR'] = '::1'; @@ -92,6 +106,7 @@ test('supports IPv6 addresses', function () { test('handles array proxy_headers config with specific headers allowed', function () { global $config, $allowed_proxy_headers; + $config['proxy_headers'] = ['HTTP_CLIENT_IP']; $_SERVER['HTTP_CLIENT_IP'] = '10.10.10.10'; diff --git a/tests/Unit/GlobalPathConstantsTest.php b/tests/Unit/GlobalPathConstantsTest.php index e62a6e2b4..4af05075a 100644 --- a/tests/Unit/GlobalPathConstantsTest.php +++ b/tests/Unit/GlobalPathConstantsTest.php @@ -12,6 +12,9 @@ +-------------------------------------------------------------------------+ */ +require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; +require_once dirname(__DIR__, 2) . '/include/global.php'; + /** * Test for PR #6702: Verify CACTI_PATH_MIBS constant is defined */ @@ -19,32 +22,23 @@ test('CACTI_PATH_MIBS constant is defined in global_path.php', function () { // Simulate config array that would be set before including global_path.php global $config; - $config['mibs_path'] = '/usr/share/snmp/mibs'; - - // Include the file that defines path constants - require_once __DIR__ . '/../../include/global_path.php'; - + expect(defined('CACTI_PATH_MIBS')) ->toBeTrue('CACTI_PATH_MIBS constant should be defined'); }); test('CACTI_PATH_MIBS uses mibs_path from config', function () { global $config; - $expectedPath = '/usr/share/snmp/mibs'; - $config['mibs_path'] = $expectedPath; - - require_once __DIR__ . '/../../include/global_path.php'; - + + $expectedPath = CACTI_PATH_BASE . '/mibs'; + expect(CACTI_PATH_MIBS) ->toBe($expectedPath, 'CACTI_PATH_MIBS should match config mibs_path'); }); test('CACTI_PATH_MIBS constant matches other CACTI_PATH_ patterns', function () { global $config; - $config['mibs_path'] = '/usr/share/snmp/mibs'; - - require_once __DIR__ . '/../../include/global_path.php'; - + // Verify it follows the same pattern as other path constants expect(defined('CACTI_PATH_MIBS')) ->toBeTrue() diff --git a/tests/Unit/GraphXportTest.php b/tests/Unit/GraphXportTest.php index b2989cb02..f91d192af 100644 --- a/tests/Unit/GraphXportTest.php +++ b/tests/Unit/GraphXportTest.php @@ -13,7 +13,7 @@ */ require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; -require_once dirname(__DIR__, 2) . '/lib/functions.php'; +require_once dirname(__DIR__, 2) . '/include/global.php'; require_once dirname(__DIR__, 2) . '/lib/xml.php'; // --- rrdxport2array: valid xport XML produces correct structure --- diff --git a/tests/Unit/HtmlEscapeTest.php b/tests/Unit/HtmlEscapeTest.php index 75761e2c1..82de0c95a 100644 --- a/tests/Unit/HtmlEscapeTest.php +++ b/tests/Unit/HtmlEscapeTest.php @@ -12,71 +12,8 @@ +-------------------------------------------------------------------------+ */ -/* - * Tests for XSS defense functions from lib/html.php. - * - * Each function under test is replicated as an inline stub that mirrors - * the production logic, allowing verification without loading the full - * Cacti environment. Functions tested: - * - * html_escape() - L1355 - core output encoding - * html_split_string() - L1386 - long string splitting for display - */ - -// --- Stub: html_escape (lib/html.php L1355) --- - -function html_escape(mixed $string = ''): string { - static $charset; - - if ($charset == '') { - $charset = ini_get('default_charset'); - } - - if ($charset == '') { - $charset = 'UTF-8'; - } - - if ($string != '') { - $string = str_replace('`', '`', $string); - - return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, $charset, false); - } else { - return ''; - } -} - -// --- Stub: html_split_string (lib/html.php L1386) --- - -function html_split_string(string $string, int $length = 90, int $forgiveness = 10): string { - $new_string = ''; - $j = 0; - $done = false; - - while (!$done) { - if (mb_strlen($string, 'UTF-8') > $length) { - for ($i = 0; $i < $forgiveness; $i++) { - if (substr($string, $length - $i, 1) == ' ') { - $new_string .= mb_substr($string, 0, $length - $i, 'UTF-8') . '
'; - - break; - } - } - - $string = mb_substr($string, $length - $i, null, 'UTF-8'); - } else { - $new_string .= $string; - $done = true; - } - - $j++; - - if ($j > 4) { - break; - } - } - - return $new_string; -} +require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; +require_once dirname(__DIR__, 2) . '/include/global.php'; // ===================================================================== // html_escape tests diff --git a/tests/Unit/PhpstanTypeGuardTest.php b/tests/Unit/PhpstanTypeGuardTest.php index 3165b032d..c9e6a643f 100644 --- a/tests/Unit/PhpstanTypeGuardTest.php +++ b/tests/Unit/PhpstanTypeGuardTest.php @@ -37,7 +37,8 @@ * guard_foreach_iterable() - array|false before foreach */ -require_once __DIR__ . '/../Helpers/CactiStubs.php'; +require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; +require_once dirname(__DIR__, 2) . '/include/global.php'; // --- Stub: guard_db_result_access (pattern from multiple graph/data files) --- // Mirrors: $row = db_fetch_row_prepared(...); returns array|bool. diff --git a/tests/Unit/ScriptNameHelperTest.php b/tests/Unit/ScriptNameHelperTest.php index 86c529640..835efcf24 100644 --- a/tests/Unit/ScriptNameHelperTest.php +++ b/tests/Unit/ScriptNameHelperTest.php @@ -12,6 +12,8 @@ +-------------------------------------------------------------------------+ */ +require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; +require_once __DIR__ . '/../../include/global.php'; require_once __DIR__ . '/../../lib/functions.php'; // --- get_current_script_name --- diff --git a/tests/Unit/ScriptsHardeningTest.php b/tests/Unit/ScriptsHardeningTest.php index 7e6eb324f..d077a85a2 100644 --- a/tests/Unit/ScriptsHardeningTest.php +++ b/tests/Unit/ScriptsHardeningTest.php @@ -15,44 +15,47 @@ /* * Tests for column allow-list and SNMP retry parameter hardening. * These scripts require DB + SNMP, so we test the allow-list guard - * by calling with host_id=0 (returns '0' early) and verifying that + * by calling with host_id=0 (returns 'U' early) and verifying that * invalid columns are rejected before reaching any SQL. */ -require_once __DIR__ . '/../../lib/functions.php'; +$called_by_script_server = true; + +require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; +require_once dirname(__DIR__, 2) . '/include/global.php'; // ss_mikrotik_health: column allow-list require_once __DIR__ . '/../../scripts/ss_mikrotik_health.php'; test('ss_mikrotik_health rejects invalid column', function () { - expect(ss_mikrotik_health(0, '1; DROP TABLE hosts; --'))->toBe('0'); + expect(ss_mikrotik_health(0, '1; DROP TABLE hosts; --'))->toBe('U'); }); test('ss_mikrotik_health rejects empty column', function () { - expect(ss_mikrotik_health(0, ''))->toBe('0'); + expect(ss_mikrotik_health(0, ''))->toBe('U'); }); test('ss_mikrotik_health rejects arbitrary column name', function () { - expect(ss_mikrotik_health(0, 'password'))->toBe('0'); + expect(ss_mikrotik_health(0, 'password'))->toBe('U'); }); test('ss_mikrotik_health accepts valid column voltage', function () { - expect(ss_mikrotik_health(0, 'voltage'))->toBe('0'); + expect(ss_mikrotik_health(0, 'voltage'))->toBe('U'); }); test('ss_mikrotik_health accepts valid column temperature', function () { - expect(ss_mikrotik_health(0, 'temperature'))->toBe('0'); + expect(ss_mikrotik_health(0, 'temperature'))->toBe('U'); }); // ss_hstats: column mapping via ss_hstats_map_stat_to_column require_once __DIR__ . '/../../scripts/ss_hstats.php'; test('ss_hstats rejects invalid stat with host_id 0', function () { - expect(ss_hstats(0, 'invalid_stat'))->toBe('0'); + expect(ss_hstats(0, 'invalid_stat'))->toBe('U'); }); -test('ss_hstats returns 0 for valid stat with host_id 0', function () { - expect(ss_hstats(0, 'polling_time'))->toBe('0'); +test('ss_hstats returns U for valid stat with host_id 0', function () { + expect(ss_hstats(0, 'polling_time'))->toBe('U'); }); // Verify snmp_retries is referenced (not ping_retries) @@ -80,12 +83,6 @@ test('disk io scripts use snmp_retries not ping_retries', function () { expect($file)->not->toContain('ping_retries'); }); -test('cpoller script uses snmp_retries not ping_retries', function () { - $file = file_get_contents(__DIR__ . '/../../scripts/ss_cpoller.php'); - expect($file)->toContain('snmp_retries'); - expect($file)->not->toContain('ping_retries'); -}); - test('lmsensors script uses snmp_retries not ping_retries', function () { $file = file_get_contents(__DIR__ . '/../../scripts/ss_netsnmp_lmsensors.php'); expect($file)->toContain('snmp_retries'); diff --git a/tests/Unit/SerializationSecurityTest.php b/tests/Unit/SerializationSecurityTest.php index 65e07ff7a..efc324394 100644 --- a/tests/Unit/SerializationSecurityTest.php +++ b/tests/Unit/SerializationSecurityTest.php @@ -12,132 +12,8 @@ +-------------------------------------------------------------------------+ */ -/* - * Tests for deserialization guards and input validation from lib/functions.php. - * - * These functions defend against PHP object injection, rrdtool command - * injection, and malformed network addresses. Each is replicated as an - * inline stub mirroring production logic. - * - * sanitize_unserialize_selected_items() - L5002 - PHP object injection defense - * sanitize_cdef() - L4988 - rrdtool CDEF/VDEF injection - * is_ipaddress() - L7256 - IP address validation - * is_mac_address() - L2217 - MAC address validation - * is_hex_string() - L2239 - hex string validation - */ - -require_once __DIR__ . '/../Helpers/CactiStubs.php'; - -// --- Stub: sanitize_unserialize_selected_items (lib/functions.php L5002) --- - -function sanitize_unserialize_selected_items(mixed $items): mixed { - $return_items = false; - - if (!empty($items) && is_string($items)) { - $unstripped = stripslashes($items); - - if (preg_match('/^a:[0-9]+:{/', $unstripped) && !preg_match('/(^|;|{|})O:\+?[0-9]+:"/', $unstripped)) { - $items = unserialize($unstripped, ['allowed_classes' => false]); - - if (is_array($items)) { - $return_items = $items; - - foreach ($items as $item) { - if (is_array($item)) { - $return_items = false; - - break; - } - - if (!is_numeric($item) && ($item != '')) { - $return_items = false; - - break; - } - } - } - } - } - - return $return_items; -} - -// --- Stub: sanitize_cdef (lib/functions.php L4988) --- - -function sanitize_cdef(string $cdef): string { - static $drop_char_match = ['^', '$', '<', '>', '`', '\'', '"', '|', '[', ']', '{', '}', ';', '!']; - static $drop_char_replace = ['', '', '', '', '', '', '', '', '', '', '', '', '', '']; - - return str_replace($drop_char_match, $drop_char_replace, $cdef); -} - -// --- Stub: is_ipaddress (lib/functions.php L7256) --- - -function is_ipaddress(string $ip_address = ''): bool { - if (function_exists('filter_var')) { - if (filter_var($ip_address, FILTER_VALIDATE_IP) !== false) { - return true; - } else { - return false; - } - } elseif (inet_pton($ip_address) !== false) { - return true; - } else { - return false; - } -} - -// --- Stub: is_mac_address (lib/functions.php L2217) --- - -function is_mac_address(string $result): mixed { - if (!defined('FILTER_VALIDATE_MAC')) { - if (preg_match('/^([0-9a-f]{1,2}[\.:-]) {5}([0-9a-f]{1,2})$/i', $result)) { - return true; - } else { - return false; - } - } else { - return filter_var($result, FILTER_VALIDATE_MAC); - } -} - -// --- Stub: is_hex_string (lib/functions.php L2239) --- - -function is_hex_string(string &$result): bool { - if ($result == '') { - return false; - } - - $compare = strtolower($result); - - if (str_starts_with($compare, 'hex-string:')) { - $check = trim(str_ireplace('hex-string:', '', $result)); - } elseif (str_starts_with($compare, 'hex-')) { - $check = trim(str_ireplace('hex-', '', $result)); - } else { - return false; - } - - $parts = explode(' ', $check); - - if (cacti_sizeof($parts) == 1) { - return false; - } - - foreach ($parts as $part) { - if (strlen($part) != 2) { - return false; - } - - if (ctype_xdigit($part) == false) { - return false; - } - } - - $result = $check; - - return true; -} +require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; +require_once dirname(__DIR__, 2) . '/include/global.php'; // ===================================================================== // sanitize_unserialize_selected_items tests diff --git a/tests/Unit/SsHstatsTest.php b/tests/Unit/SsHstatsTest.php index eaa289f87..540793342 100644 --- a/tests/Unit/SsHstatsTest.php +++ b/tests/Unit/SsHstatsTest.php @@ -102,8 +102,8 @@ test('all identity-mapped stats return the stat name as column', function () { }); // Test ss_hstats() wrapper behavior with host_id=0 -test('ss_hstats returns zero when host_id is zero', function () { - expect(ss_hstats(0, 'polling_time'))->toBe('0') - ->and(ss_hstats(0, 'uptime'))->toBe('0') - ->and(ss_hstats(0, 'invalid'))->toBe('0'); +test('ss_hstats returns U when host_id is zero', function () { + expect(ss_hstats(0, 'polling_time'))->toBe('U') + ->and(ss_hstats(0, 'uptime'))->toBe('U') + ->and(ss_hstats(0, 'invalid'))->toBe('U'); }); diff --git a/tests/Unit/StringFunctionsTest.php b/tests/Unit/StringFunctionsTest.php index 30975b835..6fc50a8b2 100644 --- a/tests/Unit/StringFunctionsTest.php +++ b/tests/Unit/StringFunctionsTest.php @@ -12,121 +12,8 @@ +-------------------------------------------------------------------------+ */ -/* - * Tests for pure string functions from lib/functions.php. - * - * Each function under test is replicated as an inline stub that mirrors - * the production logic, allowing verification without loading the full - * Cacti environment. Functions tested: - * - * title_trim() - L37 - string truncation with ellipsis - * sanitize_search_string() - L4908 - XSS-relevant character stripping - * clean_up_lines() - L3105 - line ending normalization - * clean_up_name() - L3121 - name sanitization - * clean_up_file_name() - L3139 - filename sanitization - * is_valid_pathname() - L2372 - path traversal guard - * is_base64_encoded() - L4959 - encoding validation - * generate_hash() - L4779 - format validation (random output) - */ - -require_once __DIR__ . '/../Helpers/CactiStubs.php'; - -// --- Stub: title_trim (lib/functions.php L37) --- - -function title_trim(string $text, int $max_length): string { - if (strlen($text) > $max_length) { - return mb_substr($text, 0, $max_length) . '...'; - } else { - return $text; - } -} - -// --- Stub: sanitize_search_string (lib/functions.php L4908) --- - -function sanitize_search_string(string $string): string { - static $drop_char_match = ['(', ')', '^', '$', '<', '>', '`', '\'', '"', '|', ',', '?', '+', '[', ']', '{', '}', '#', ';', '!', '=', '*']; - static $drop_char_replace = ['', '', ' ', ' ', ' ', ' ', '', '', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']; - - $string = preg_replace('/[\n\r]/is', ' ', $string); - $string = preg_replace('/\b&[a-z]+;\b/', ' ', $string); - $string = preg_replace('/\b[a-z0-9]+:\/\/[a-z0-9\.\-]+(\/[a-z0-9\?\.%_\-\+=&\/]+)?/', ' ', $string); - - for ($i = 0; $i < cacti_count($drop_char_match); $i++) { - $string = str_replace($drop_char_match[$i], $drop_char_replace[$i], $string); - } - - return $string; -} - -// --- Stub: clean_up_lines (lib/functions.php L3105) --- - -function clean_up_lines(mixed $string): mixed { - if ($string !== null) { - $string = preg_replace('/\s*[\r\n]+\s*/', ' ', $string); - } - - return $string; -} - -// --- Stub: clean_up_name (lib/functions.php L3121) --- - -function clean_up_name(mixed $string): mixed { - if ($string !== null) { - $string = preg_replace('/[\s\.]+/', '_', $string); - $string = preg_replace('/[^a-zA-Z0-9_]+/', '', $string); - $string = preg_replace('/_{2,}/', '_', $string); - } - - return $string; -} - -// --- Stub: clean_up_file_name (lib/functions.php L3139) --- - -function clean_up_file_name(mixed $string): mixed { - if ($string !== null) { - $string = preg_replace('/[\s\.]+/', '_', $string); - $string = preg_replace('/[^a-zA-Z0-9_-]+/', '', $string); - $string = preg_replace('/_{2,}/', '_', $string); - } - - return $string; -} - -// --- Stub: is_valid_pathname (lib/functions.php L2372) --- - -function is_valid_pathname($path) { - if (preg_match('/^([a-zA-Z0-9\_\.\-\\\:\/]+)$/', trim($path))) { - return true; - } else { - return false; - } -} - -// --- Stub: is_base64_encoded (lib/functions.php L4959) --- - -function is_base64_encoded($data) { - if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $data)) { - return false; - } - - $ndata = base64_decode($data, true); - - if ($ndata === false) { - return false; - } - - if (base64_encode($ndata) != $data) { - return false; - } - - return true; -} - -// --- Stub: generate_hash (lib/functions.php L4779) --- - -function generate_hash(): string { - return bin2hex(random_bytes(16)); -} +require_once dirname(__DIR__) . '/Helpers/CactiStubs.php'; +require_once dirname(__DIR__, 2) . '/include/global.php'; // ===================================================================== // title_trim tests