Files
TheWitness 2e5f294c1f test: add Security baseline and expand Unit/Integration coverage for lifecycle (#246)
This plugin had no tests/Security category at all, and setup.php's
lifecycle/device-hook functions had zero test coverage (only db_functions.php
and the request-output escaping/wiring were tested). Closed both gaps:

- Security: added the standard PHP compatibility scan (8.0-8.4
  removed/added syntax) and a static structure check (required
  functions, INFO keys), matching the baseline used across the rest of
  the plugin fleet.
- Unit: plugin_monitor_version(), the lifecycle contract functions
  (plugin_monitor_uninstall - verifies it drops every table it owns,
  plugin_monitor_check_config - both the out-of-range and valid
  monitor_refresh branches, plugin_monitor_upgrade,
  monitor_check_upgrade()'s page-guard and version-drift branches),
  the device-list hooks (monitor_device_filters, monitor_device_sql_where,
  monitor_device_action_array, monitor_device_remove), monitor_draw_navigation_text(),
  monitor_get_default(), monitor_setup_table() (verifies every table it
  provisions), and monitor_poller_bottom() (primary-poller and
  non-primary-poller branches).
- Integration: plugin_monitor_install() (verifies every hook, the
  realm, the tables it provisions, and the config defaults it sets,
  together, in one pass) - this plugin had no tests/Integration
  coverage of setup.php before (the existing Integration tests cover
  request-output wiring, not the plugin lifecycle).

No new unserialize() hardening was needed: setup.php's one
unserialize() call already goes through
sanitize_unserialize_selected_items().

Also fixed tests/.cacti-version, which was pinned to a stale "1.2.31"
and failed the bootstrap's version check against any other real Cacti
checkout (e.g. a local 1.2.32 checkout); "1.2.x" matches the
moving-branch convention used elsewhere in this fleet.

Intentionally left uncovered: monitor_device_table_bottom()/monitor_show_tab()/
monitor_top_graph_refresh()/plugin_monitor_page_head() (print-heavy UI
rendering), monitor_config_settings()/monitor_config_arrays()/monitor_config_form()
(very large), monitor_device_action_execute()/monitor_device_action_prepare()
(large, print/DB-heavy confirmation dialogs), and monitor_api_device_save()
(moderate complexity, comparatively low value relative to the effort of
safely mocking its dependencies) - consistent with similarly-scoped
functions skipped elsewhere in this coverage-expansion pass.

tests/bootstrap-unit.php gained a fixture-mock DB API
(monitor_test_mock_db/monitor_test_reset_db_mocks/monitor_test_db_result,
matching the convention used elsewhere in the plugin fleet),
api_plugin_register_hook/api_plugin_register_realm call-logging stubs,
db_table_exists, and exec_background (call-logging, safe here since
setup.php's poller include always goes through $config['library_path'],
which points at a throwaway test stub, never Cacti core's real
lib/poller.php); read_config_option/set_config_option were rewired from
hardcoded no-ops to a real backing store - all needed by the new tests.

Verified via a real WSL Cacti install (composer-managed Pest, not a
plugin-local vendor tree): 63 passed (303 assertions), up from 31.
2026-09-23 10:04:23 -04:00

150 lines
4.8 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* Verify plugin source files do not use PHP 8.3+/8.4-only syntax.
* This plugin's floor version is PHP 8.2, matching the shared CI test
* matrix (php: ['8.2', '8.3', '8.4']).
*/
$pluginRoot = realpath(__DIR__ . '/../..');
if ($pluginRoot === false) {
throw new RuntimeException('Unable to resolve the plugin root directory');
}
$files = [];
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($pluginRoot, FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->getExtension() !== 'php') {
continue;
}
$relativeFile = ltrim(str_replace($pluginRoot, '', $file->getPathname()), DIRECTORY_SEPARATOR);
$relativeFile = str_replace(DIRECTORY_SEPARATOR, '/', $relativeFile);
if (strpos($relativeFile, 'tests/') === 0 || strpos($relativeFile, 'vendor/') === 0 || strpos($relativeFile, 'include/vendor/') === 0) {
continue;
}
$files[] = $relativeFile;
}
sort($files);
function plugin_test_read_source_file($relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);
if ($path === false) {
throw new RuntimeException("Unable to resolve required plugin source: {$relativeFile}");
}
$contents = file_get_contents($path);
if ($contents === false) {
throw new RuntimeException("Unable to read required plugin source: {$relativeFile}");
}
return $contents;
}
it('does not use asymmetric visibility (PHP 8.4)', function () use ($files) {
foreach ($files as $relativeFile) {
$contents = plugin_test_read_source_file($relativeFile);
expect(preg_match('/\bprivate\s*\(\s*set\s*\)|\bprotected\s*\(\s*set\s*\)/', $contents))->toBe(0,
"{$relativeFile} uses asymmetric visibility which requires PHP 8.4"
);
}
});
it('does not use the #[Override] attribute (PHP 8.3)', function () use ($files) {
foreach ($files as $relativeFile) {
$contents = plugin_test_read_source_file($relativeFile);
expect(preg_match('/#\[\s*\\\\?Override\s*\]/i', $contents))->toBe(0,
"{$relativeFile} uses #[Override] which requires PHP 8.3"
);
}
});
it('does not use the #[Deprecated] attribute (PHP 8.4)', function () use ($files) {
foreach ($files as $relativeFile) {
$contents = plugin_test_read_source_file($relativeFile);
expect(preg_match('/#\[\s*\\\\?Deprecated\b/i', $contents))->toBe(0,
"{$relativeFile} uses #[Deprecated] which requires PHP 8.4"
);
}
});
it('does not use json_validate() (PHP 8.3)', function () use ($files) {
foreach ($files as $relativeFile) {
$contents = plugin_test_read_source_file($relativeFile);
expect(preg_match('/\bjson_validate\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses json_validate() which requires PHP 8.3"
);
}
});
it('does not use array_find()/array_any()/array_all() (PHP 8.4)', function () use ($files) {
foreach ($files as $relativeFile) {
$contents = plugin_test_read_source_file($relativeFile);
expect(preg_match('/\barray_(find|any|all)\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses array_find()/array_any()/array_all() which requires PHP 8.4"
);
}
});
it('does not use typed class constants (PHP 8.3)', function () use ($files) {
foreach ($files as $relativeFile) {
$contents = plugin_test_read_source_file($relativeFile);
expect(preg_match('/\bconst\s+(?:\?\s*)?[A-Za-z_\\\\][A-Za-z0-9_\\\\]*(?:\s*\|\s*(?:\?\s*)?[A-Za-z_\\\\][A-Za-z0-9_\\\\]*)*\s+[A-Za-z_][A-Za-z0-9_]*\s*=/', $contents))->toBe(0,
"{$relativeFile} uses a typed class constant which requires PHP 8.3"
);
}
});
it('does not use a dynamic class constant fetch (PHP 8.3)', function () use ($files) {
foreach ($files as $relativeFile) {
$contents = plugin_test_read_source_file($relativeFile);
expect(preg_match('/::\s*\{\s*\$/', $contents))->toBe(0,
"{$relativeFile} uses a dynamic class constant fetch (Foo::" . '{$bar}' . ") which requires PHP 8.3"
);
}
});
it('does not use each() (removed in PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$contents = plugin_test_read_source_file($relativeFile);
expect(preg_match('/(?<![.>])\beach\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses each() which was removed in PHP 8.0"
);
}
});
it('does not use create_function() (removed in PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$contents = plugin_test_read_source_file($relativeFile);
expect(preg_match('/(?<![.>])\bcreate_function\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses create_function() which was removed in PHP 8.0"
);
}
});