Files
cacti/tests/Unit/Core/Availability_Tests/PingOrSnmpAvailabilityTest.php
Sean Mancini 96033267cb refactor(tests): reorganize unit tests into domain category folders (#7733)
* refactor(tests): reorganize unit tests into domain category folders

- Consolidate per-advisory GHSA regression tests into Security/ subfolders by vuln class
- Group all 186 test files into domain folders: DataCollection/, Ui/, Core/,
  Database/, Installer/, Scripts/, Security/, Plugin/
- Strip GitHub issue/GHSA numbers from filenames; keep IDs in test descriptions
- Fix all __DIR__ relative paths to match new folder nesting depth
- Update CI workflow path triggers and phpunit XML configs for new file locations
- Update .github/copilot-instructions.md with unit test organization guidelines
- Add tests/vendor/ to .gitignore

* add gaurds

* fix up guard for unit tests

* fix redeclaration of cacti_log in tests

* fix updates to unit tests

* fix test patjhs

* fix: update test __DIR__ paths for reorganized test structure

* fix: conditionally define read_user_setting to prevent redeclaration in tests

* fix: conditionally define functions to prevent redeclaration in tests

- Wrap clean_up_lines, raise_message, cacti_debug_backtrace, and get_debug_prefix
  with function_exists() checks to prevent conflicts when test stubs are loaded
  before lib/functions.php in the same PHP process

* fix: add unit test bootstrap to prevent function redeclaration errors

- Create tests/bootstrap-unit.php to load core libraries before tests
- Ensures lib/functions.php loads before test stubs define stub functions
- Test stubs use function_exists() checks, so they detect pre-loaded functions
- Update phpunit.xml to use bootstrap-unit.php for Unit testsuite
- Fixes 'Cannot redeclare read_user_setting()' errors when tests run in same process

* fix: load test stubs before global.php in unit test bootstrap

- Reorder bootstrap to load CactiStubs.php and UnitStubs.php first
- Ensures stub functions like __() are available when global.php loads
- global.php calls get_cacti_version_text() which needs __() for translations
- Fixes 'Call to undefined function __()' error during bootstrap

* fix: remove function conflicts from test stubs

- Remove stub definitions for functions already in lib/functions.php:
  cacti_sizeof, cacti_log, cacti_strtolower, read_config_option,
  read_user_setting, cacti_count
- Keep translation function stubs (__(), __esc()) needed by global.php
- Update bootstrap to load lib/functions.php after stubs
- This prevents function redeclaration errors while maintaining test isolation

* fix: prevent constant and variable redeclaration in bootstrap

- Remove explicit PHP_TESTING and CACTI_TEST_BOOTSTRAP defines from bootstrap
  (CactiStubs.php now handles this with guards)
- Add guards to CactiStubs.php to prevent redefinition of PHP_TESTING
- Only set CACTI_TEST_BOOTSTRAP env vars if not already set
- Fixes PHP warnings about 'Constant X already defined'

* fix: remove redundant lib/functions.php load from bootstrap

include/global.php already loads lib/functions.php at lines 287-288.
The bootstrap was trying to load it again after global.php, but global.php
needs format_cacti_version() to be available on lines 293-294 to define
CACTI_VERSION and CACTI_VERSION_FULL constants.

Solution: Let global.php handle loading lib/functions.php itself, and don't
load it separately in the bootstrap.

* fix: remove more conflicting function stubs from test helpers

- Remove stubs for functions also defined in lib/functions.php:
  raise_message, clean_up_lines, cacti_debug_backtrace, get_debug_prefix
- Remove POLLER_ID constant stub (defined unconditionally by global.php)
- Keep only translation functions and test-specific helpers
- This prevents function/constant redeclaration errors

* fix: minimize test stubs to only translation functions

Remove ALL helper functions and constants from UnitStubs.php that are:
- Defined in lib/functions.php (raise_message, clean_up_lines, etc.)
- Defined in lib/html_utility.php (srv, set_request_var)
- Defined in global_constants.php (POLLER_VERBOSITY_*, MESSAGE_LEVEL_*)
- Defined in global.php (POLLER_ID)

Keep ONLY translation functions (__(), __esc()) that are:
- Called by global.php during initialization
- Not defined elsewhere
- Required for proper bootstrap sequence

This eliminates all constant and function redeclaration errors.

* fix: remove __esc stub - already defined in global_languages.php

Keep only __() translation function stub, which is needed by global.php
and not defined elsewhere in Cacti core.

* fix: handle no-arg case in __() translation stub

The __() function may be called with just a string and no format arguments.
Previously, vsprintf() would fail with 'arguments array must contain 2 items'.
Now, if no args are provided, return the text as-is.
2026-08-16 19:23:31 -07:00

220 lines
7.6 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| This program is free software; you can redistribute it and/or |
| modify it under the terms of the GNU General Public License |
| as published by the Free Software Foundation; either version 2 |
| of the License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU General Public License for more details. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* In Ping-OR-SNMP mode Net_Ping::ping() set $snmp_result to true and then
* skipped the SNMP test, because the guard read
* "$avail_method != AVAIL_SNMP_OR_PING". The mode then resolved to
* "true || $ping_result", so a device could never be reported down however
* unreachable it was. SNMP is now attempted whenever the ping fails.
*
* ping_icmp() and ping_snmp() are the only things here that touch the network,
* so a subclass replaces them with recorded answers and the real ping() runs.
*/
require_once dirname(__DIR__, 4) . '/include/global_constants.php';
require_once dirname(__DIR__, 4) . '/lib/ping.php';
if (!function_exists(__NAMESPACE__ . '\\__') && !function_exists('\\__')) {
function __($text, ...$args) {
return $args === [] ? $text : vsprintf($text, $args);
}
}
if (!function_exists(__NAMESPACE__ . '\\cacti_log') && !function_exists('\\cacti_log')) {
function cacti_log($message, $print = false, $tag = 'GENERAL', $level = 1) {
}
}
/**
* A Net_Ping whose probes answer from the test instead of the network.
*/
class RecordingPing extends Net_Ping {
/** @var bool the answer ping_icmp() should give */
public bool $icmp_answer = false;
/** @var bool the answer ping_snmp() should give */
public bool $snmp_answer = false;
/** @var int how many times ping_snmp() was called */
public int $snmp_calls = 0;
public function ping_icmp() : bool {
$this->ping_status = $this->icmp_answer ? '1.000' : 'down';
$this->ping_response = $this->icmp_answer ? 'ok' : 'unreachable';
return $this->icmp_answer;
}
public function ping_snmp() : bool {
$this->snmp_calls++;
$this->snmp_status = $this->snmp_answer ? '1.000' : 'down';
$this->snmp_response = $this->snmp_answer ? 'ok' : 'no response';
return $this->snmp_answer;
}
}
/**
* Builds a probe for one device.
*
* @param array $host The device fields ping() reads.
* @param bool $icmp_answer Whether the ICMP probe should succeed.
* @param bool $snmp_answer Whether the SNMP probe should succeed.
*
* @return RecordingPing A probe ready to ping().
*/
function ping_probe(array $host, bool $icmp_answer, bool $snmp_answer) : RecordingPing {
$ping = new RecordingPing();
$ping->host = $host + ['hostname' => 'dev.example.net', 'snmp_community' => 'public', 'snmp_version' => 2];
$ping->icmp_answer = $icmp_answer;
$ping->snmp_answer = $snmp_answer;
return $ping;
}
/* Without sockets, ping() rewrites every availability method to AVAIL_SNMP, so
the OR/AND distinctions the first group asserts would collapse. The fallback
group is the mirror image and only runs where sockets really is missing, so
between the two environments every branch is exercised. Run the fallback
group locally with: php -d disable_functions=socket_create */
function ping_needs_sockets() : void {
if (!function_exists('socket_create')) {
test()->markTestSkipped('sockets support is not enabled in this PHP');
}
}
/**
* Skips a test when sockets is present, i.e. when the fallback cannot trigger.
*
* @return void
*/
function ping_needs_no_sockets() : void {
if (function_exists('socket_create')) {
test()->markTestSkipped('sockets support is enabled, so the fallback cannot trigger');
}
}
test('an OR-mode device answering neither ping nor SNMP is reported down', function () {
ping_needs_sockets();
$ping = ping_probe([], false, false);
// pre-fix this returned true: $snmp_result was assumed true and never tested
expect($ping->ping(AVAIL_SNMP_OR_PING, PING_ICMP))->toBeFalse();
});
test('an OR-mode device that fails ping but answers SNMP is reported up', function () {
ping_needs_sockets();
$ping = ping_probe([], false, true);
expect($ping->ping(AVAIL_SNMP_OR_PING, PING_ICMP))->toBeTrue()
->and($ping->snmp_calls)->toBe(1);
});
test('a successful ping still short-circuits the SNMP probe in OR mode', function () {
ping_needs_sockets();
$ping = ping_probe([], true, false);
// the point of OR mode: one success is enough, so do not pay for the walk
expect($ping->ping(AVAIL_SNMP_OR_PING, PING_ICMP))->toBeTrue()
->and($ping->snmp_calls)->toBe(0);
});
test('a v1/v2 device with no community is still assumed up when the ping fails', function () {
ping_needs_sockets();
$ping = ping_probe(['snmp_community' => '', 'snmp_version' => 2], false, false);
// there is nothing to test against, so the long-standing behaviour stands
expect($ping->ping(AVAIL_SNMP_OR_PING, PING_ICMP))->toBeTrue()
->and($ping->snmp_calls)->toBe(0);
});
test('a v3 device with no community is still probed over SNMP', function () {
ping_needs_sockets();
$ping = ping_probe(['snmp_community' => '', 'snmp_version' => 3], false, true);
expect($ping->ping(AVAIL_SNMP_OR_PING, PING_ICMP))->toBeTrue()
->and($ping->snmp_calls)->toBe(1);
});
test('AND mode still needs both probes to answer', function () {
ping_needs_sockets();
$ping = ping_probe([], true, false);
expect($ping->ping(AVAIL_SNMP_AND_PING, PING_ICMP))->toBeFalse();
$ping = ping_probe([], true, true);
expect($ping->ping(AVAIL_SNMP_AND_PING, PING_ICMP))->toBeTrue();
});
test('AND mode skips the SNMP probe once the ping has already failed', function () {
ping_needs_sockets();
$ping = ping_probe([], false, true);
expect($ping->ping(AVAIL_SNMP_AND_PING, PING_ICMP))->toBeFalse()
->and($ping->snmp_calls)->toBe(0);
});
/**
* cmd.php builds reduced host arrays, so these keys can be absent rather than
* empty. Reading them unguarded raised an undefined-key warning.
*/
test('a host array with no snmp keys at all is handled without warnings', function () {
ping_needs_sockets();
$ping = new RecordingPing();
$ping->host = ['hostname' => 'dev.example.net'];
$ping->icmp_answer = false;
$ping->snmp_answer = false;
expect($ping->ping(AVAIL_SNMP_OR_PING, PING_ICMP))->toBeTrue()
->and($ping->snmp_calls)->toBe(0);
});
test('the no-sockets fallback reports a communityless device down, not up', function () {
ping_needs_no_sockets();
// the fallback forces AVAIL_SNMP on a device that has no SNMP configured,
// so there is nothing to test against and "up" would be a fabricated result
$ping = ping_probe(['snmp_community' => '', 'snmp_version' => 2], false, false);
expect($ping->ping(AVAIL_SNMP_OR_PING, PING_ICMP))->toBeFalse()
->and($ping->snmp_calls)->toBe(0);
});
test('the no-sockets fallback still probes a device that has a community', function () {
ping_needs_no_sockets();
$ping = ping_probe([], false, true);
expect($ping->ping(AVAIL_SNMP_OR_PING, PING_ICMP))->toBeTrue()
->and($ping->snmp_calls)->toBe(1);
});