test: add Pest v1 security test infrastructure#42
test: add Pest v1 security test infrastructure#42somethingwithproof wants to merge 2 commits intoCacti:developfrom
Conversation
Add source-scan tests verifying security patterns (prepared statements, output escaping, auth guards, PHP 7.4 compatibility) remain in place across refactors. Tests run with Pest v1 (PHP 7.3+) and stub the Cacti framework so plugins can be tested in isolation. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
There was a problem hiding this comment.
Pull request overview
Adds Pest v1-based test scaffolding to the hmib plugin repo, focusing on security-oriented source scans and PHP 7.4 compatibility checks.
Changes:
- Add Composer dev dependency for Pest v1 and wire up test bootstrap loading.
- Introduce a Pest bootstrap with Cacti framework stub functions/constants for isolated plugin testing.
- Add security/compatibility source-scan tests for setup.php structure, prepared-statement helper usage, and PHP 7.4 compatibility.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
composer.json |
Adds Pest v1 as a dev dependency and autoloads the test bootstrap. |
tests/Pest.php |
Pest configuration entrypoint that loads the bootstrap. |
tests/bootstrap.php |
Provides Cacti framework stubs/constants for running tests without full Cacti. |
tests/Security/SetupStructureTest.php |
Verifies setup.php exports expected plugin hook functions and version keys. |
tests/Security/PreparedStatementConsistencyTest.php |
Scans plugin source for raw db_* calls vs _prepared helpers. |
tests/Security/Php74CompatibilityTest.php |
Scans plugin source for PHP 8+ syntax/functions to preserve PHP 7.4 compatibility. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| $targetFiles = array( | ||
| 'associate_os_type.php', | ||
| 'hmib.php', | ||
| 'hmib_types.php', | ||
| 'poller_graphs.php', | ||
| 'poller_hmib.php', | ||
| 'setup.php', | ||
| ); |
There was a problem hiding this comment.
This test will fail on the current hmib sources: the listed target files contain many raw db_execute/db_fetch_* calls (e.g., setup.php uses db_execute() for table DDL and upgrades, and hmib.php uses db_fetch_assoc()). Either migrate these files in the same PR, or narrow the check (e.g., only enforce for already-migrated files / allowlist known-safe DDL / focus on queries with interpolated variables).
| continue; | ||
| } | ||
|
|
||
| $contents = file_get_contents($path); | ||
|
|
||
| if ($contents === false) { | ||
| continue; |
There was a problem hiding this comment.
Skipping missing/unreadable files via continue can silently pass the test even when the scan didn’t run. Consider failing fast when realpath() or file_get_contents() fails so the test reliably enforces the intended security rule.
| continue; | |
| } | |
| $contents = file_get_contents($path); | |
| if ($contents === false) { | |
| continue; | |
| throw new RuntimeException("Failed to resolve path for file {$relativeFile}"); | |
| } | |
| $contents = file_get_contents($path); | |
| if ($contents === false) { | |
| throw new RuntimeException("Failed to read file {$relativeFile} at path {$path}"); |
| it('does not use str_contains (PHP 8.0)', function () use ($files) { | ||
| foreach ($files as $relativeFile) { | ||
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | ||
|
|
||
| if ($path === false) { | ||
| continue; | ||
| } | ||
|
|
||
| $contents = file_get_contents($path); | ||
|
|
||
| if ($contents === false) { | ||
| continue; | ||
| } | ||
|
|
||
| expect(preg_match('/\bstr_contains\s*\(/', $contents))->toBe(0, | ||
| "{$relativeFile} uses str_contains() which requires PHP 8.0" | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| it('does not use str_starts_with (PHP 8.0)', function () use ($files) { | ||
| foreach ($files as $relativeFile) { | ||
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | ||
|
|
||
| if ($path === false) { | ||
| continue; | ||
| } | ||
|
|
||
| $contents = file_get_contents($path); | ||
|
|
||
| if ($contents === false) { | ||
| continue; | ||
| } | ||
|
|
||
| expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0, | ||
| "{$relativeFile} uses str_starts_with() which requires PHP 8.0" | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| it('does not use str_ends_with (PHP 8.0)', function () use ($files) { | ||
| foreach ($files as $relativeFile) { | ||
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | ||
|
|
||
| if ($path === false) { | ||
| continue; | ||
| } | ||
|
|
||
| $contents = file_get_contents($path); | ||
|
|
||
| if ($contents === false) { | ||
| continue; | ||
| } | ||
|
|
||
| expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0, | ||
| "{$relativeFile} uses str_ends_with() which requires PHP 8.0" | ||
| ); | ||
| } | ||
| }); | ||
|
|
||
| it('does not use nullsafe operator (PHP 8.0)', function () use ($files) { | ||
| foreach ($files as $relativeFile) { | ||
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | ||
|
|
||
| if ($path === false) { | ||
| continue; | ||
| } | ||
|
|
||
| $contents = file_get_contents($path); | ||
|
|
||
| if ($contents === false) { | ||
| continue; | ||
| } | ||
|
|
There was a problem hiding this comment.
These tests continue when a file can’t be resolved/read, which can yield false greens (the compatibility check never ran). Prefer asserting the path exists and contents are readable for each file (or failing the whole test) so missing files don’t mask PHP 8+ syntax usage.
| it('does not use str_contains (PHP 8.0)', function () use ($files) { | |
| foreach ($files as $relativeFile) { | |
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | |
| if ($path === false) { | |
| continue; | |
| } | |
| $contents = file_get_contents($path); | |
| if ($contents === false) { | |
| continue; | |
| } | |
| expect(preg_match('/\bstr_contains\s*\(/', $contents))->toBe(0, | |
| "{$relativeFile} uses str_contains() which requires PHP 8.0" | |
| ); | |
| } | |
| }); | |
| it('does not use str_starts_with (PHP 8.0)', function () use ($files) { | |
| foreach ($files as $relativeFile) { | |
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | |
| if ($path === false) { | |
| continue; | |
| } | |
| $contents = file_get_contents($path); | |
| if ($contents === false) { | |
| continue; | |
| } | |
| expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0, | |
| "{$relativeFile} uses str_starts_with() which requires PHP 8.0" | |
| ); | |
| } | |
| }); | |
| it('does not use str_ends_with (PHP 8.0)', function () use ($files) { | |
| foreach ($files as $relativeFile) { | |
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | |
| if ($path === false) { | |
| continue; | |
| } | |
| $contents = file_get_contents($path); | |
| if ($contents === false) { | |
| continue; | |
| } | |
| expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0, | |
| "{$relativeFile} uses str_ends_with() which requires PHP 8.0" | |
| ); | |
| } | |
| }); | |
| it('does not use nullsafe operator (PHP 8.0)', function () use ($files) { | |
| foreach ($files as $relativeFile) { | |
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | |
| if ($path === false) { | |
| continue; | |
| } | |
| $contents = file_get_contents($path); | |
| if ($contents === false) { | |
| continue; | |
| } | |
| $readFileContents = function (string $relativeFile): string { | |
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | |
| if ($path === false) { | |
| throw new RuntimeException("Failed to resolve path for {$relativeFile}"); | |
| } | |
| $contents = file_get_contents($path); | |
| if ($contents === false) { | |
| throw new RuntimeException("Failed to read contents of {$relativeFile}"); | |
| } | |
| return $contents; | |
| }; | |
| it('does not use str_contains (PHP 8.0)', function () use ($files, $readFileContents) { | |
| foreach ($files as $relativeFile) { | |
| $contents = $readFileContents($relativeFile); | |
| expect(preg_match('/\bstr_contains\s*\(/', $contents))->toBe(0, | |
| "{$relativeFile} uses str_contains() which requires PHP 8.0" | |
| ); | |
| } | |
| }); | |
| it('does not use str_starts_with (PHP 8.0)', function () use ($files, $readFileContents) { | |
| foreach ($files as $relativeFile) { | |
| $contents = $readFileContents($relativeFile); | |
| expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0, | |
| "{$relativeFile} uses str_starts_with() which requires PHP 8.0" | |
| ); | |
| } | |
| }); | |
| it('does not use str_ends_with (PHP 8.0)', function () use ($files, $readFileContents) { | |
| foreach ($files as $relativeFile) { | |
| $contents = $readFileContents($relativeFile); | |
| expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0, | |
| "{$relativeFile} uses str_ends_with() which requires PHP 8.0" | |
| ); | |
| } | |
| }); | |
| it('does not use nullsafe operator (PHP 8.0)', function () use ($files, $readFileContents) { | |
| foreach ($files as $relativeFile) { | |
| $contents = $readFileContents($relativeFile); |
| $source = file_get_contents(realpath(__DIR__ . '/../../setup.php')); | ||
|
|
||
| it('defines plugin_hmib_install function', function () use ($source) { | ||
| expect($source)->toContain('function plugin_hmib_install'); | ||
| }); | ||
|
|
||
| it('defines plugin_hmib_version function', function () use ($source) { | ||
| expect($source)->toContain('function plugin_hmib_version'); | ||
| }); | ||
|
|
||
| it('defines plugin_hmib_uninstall function', function () use ($source) { | ||
| expect($source)->toContain('function plugin_hmib_uninstall'); | ||
| }); | ||
|
|
||
| it('returns version array with name key', function () use ($source) { | ||
| expect($source)->toMatch('/[\'\""]name[\'\""]\s*=>/'); | ||
| }); | ||
|
|
||
| it('returns version array with version key', function () use ($source) { | ||
| expect($source)->toMatch('/[\'\""]version[\'\""]\s*=>/'); | ||
| }); |
There was a problem hiding this comment.
file_get_contents(realpath(...)) can pass false into file_get_contents (warning) and set $source to false. It’s safer to resolve the path and assert it’s not false, then assert the file contents are a string (ideally inside a beforeEach/it block) so failures are reported as test assertions rather than runtime warnings.
| $source = file_get_contents(realpath(__DIR__ . '/../../setup.php')); | |
| it('defines plugin_hmib_install function', function () use ($source) { | |
| expect($source)->toContain('function plugin_hmib_install'); | |
| }); | |
| it('defines plugin_hmib_version function', function () use ($source) { | |
| expect($source)->toContain('function plugin_hmib_version'); | |
| }); | |
| it('defines plugin_hmib_uninstall function', function () use ($source) { | |
| expect($source)->toContain('function plugin_hmib_uninstall'); | |
| }); | |
| it('returns version array with name key', function () use ($source) { | |
| expect($source)->toMatch('/[\'\""]name[\'\""]\s*=>/'); | |
| }); | |
| it('returns version array with version key', function () use ($source) { | |
| expect($source)->toMatch('/[\'\""]version[\'\""]\s*=>/'); | |
| }); | |
| beforeEach(function () { | |
| $setupPath = realpath(__DIR__ . '/../../setup.php'); | |
| expect($setupPath)->not->toBeFalse(); | |
| $source = file_get_contents($setupPath); | |
| expect($source)->toBeString(); | |
| $this->source = $source; | |
| }); | |
| it('defines plugin_hmib_install function', function () { | |
| expect($this->source)->toContain('function plugin_hmib_install'); | |
| }); | |
| it('defines plugin_hmib_version function', function () { | |
| expect($this->source)->toContain('function plugin_hmib_version'); | |
| }); | |
| it('defines plugin_hmib_uninstall function', function () { | |
| expect($this->source)->toContain('function plugin_hmib_uninstall'); | |
| }); | |
| it('returns version array with name key', function () { | |
| expect($this->source)->toMatch('/[\'\""]name[\'\""]\s*=>/'); | |
| }); | |
| it('returns version array with version key', function () { | |
| expect($this->source)->toMatch('/[\'\""]version[\'\""]\s*=>/'); | |
| }); |
| } | ||
|
|
||
| if (!defined('CACTI_PATH_BASE')) { | ||
| define('CACTI_PATH_BASE', '/var/www/html/cacti'); |
There was a problem hiding this comment.
Hard-coding CACTI_PATH_BASE to /var/www/html/cacti makes the test bootstrap environment-dependent and can break attempts to load plugin code in isolation. Prefer deriving this from the repo under test (e.g., realpath(DIR . '/..')) or allowing an override via env var.
| define('CACTI_PATH_BASE', '/var/www/html/cacti'); | |
| $cacti_path_base = getenv('CACTI_PATH_BASE'); | |
| if ($cacti_path_base === false || $cacti_path_base === '') { | |
| $cacti_path_base = realpath(__DIR__ . '/..'); | |
| if ($cacti_path_base === false) { | |
| $cacti_path_base = dirname(__DIR__); | |
| } | |
| } | |
| define('CACTI_PATH_BASE', $cacti_path_base); |
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| +-------------------------------------------------------------------------+ | ||
| | Cacti: The Complete RRDtool-based Graphing Solution | | ||
| +-------------------------------------------------------------------------+ |
There was a problem hiding this comment.
The file header block here is abbreviated compared to the standard header used in existing plugin PHP files (includes GPL notice + project URL, e.g. setup.php:2-23). For consistency (and licensing clarity), consider using the same full header template in new test files.
| +-------------------------------------------------------------------------+ | |
| +-------------------------------------------------------------------------+ | |
| | 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. | | |
| | | | |
| | You should have received a copy of the GNU General Public License | | |
| | along with this program; if not, write to the Free Software | | |
| | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | | |
| | 02110-1301, USA. | | |
| | | | |
| | http://www.cacti.net/ | | |
| +-------------------------------------------------------------------------+ |
| /* | ||
| +-------------------------------------------------------------------------+ | ||
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| +-------------------------------------------------------------------------+ |
There was a problem hiding this comment.
The file header block here is abbreviated compared to the standard header used in existing plugin PHP files (includes GPL notice + project URL, e.g. setup.php:2-23). For consistency (and licensing clarity), consider using the same full header template in new test files.
| +-------------------------------------------------------------------------+ | |
| +-------------------------------------------------------------------------+ | |
| | 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. | | |
| | | | |
| | You should have received a copy of the GNU General Public License | | |
| | along with this program; if not, write to the Free Software | | |
| | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | | |
| | 02110-1301, USA. | | |
| | | | |
| | https://www.cacti.net/ | | |
| +-------------------------------------------------------------------------+ |
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| +-------------------------------------------------------------------------+ | ||
| | Cacti: The Complete RRDtool-based Graphing Solution | | ||
| +-------------------------------------------------------------------------+ |
There was a problem hiding this comment.
The file header block here is abbreviated compared to the standard header used in existing plugin PHP files (includes GPL notice + project URL, e.g. setup.php:2-23). For consistency (and licensing clarity), consider using the same full header template in new test files.
| +-------------------------------------------------------------------------+ | |
| +-------------------------------------------------------------------------+ | |
| | 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. | | |
| | | | |
| | You should have received a copy of the GNU General Public License | | |
| | along with this program; if not, write to the Free Software | | |
| | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | | |
| | 02110-1301, USA. | | |
| | | | |
| | https://www.cacti.net/ | | |
| +-------------------------------------------------------------------------+ |
| <?php | ||
| /* | ||
| +-------------------------------------------------------------------------+ | ||
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| +-------------------------------------------------------------------------+ | ||
| | Cacti: The Complete RRDtool-based Graphing Solution | | ||
| +-------------------------------------------------------------------------+ | ||
| */ |
There was a problem hiding this comment.
The file header block here is abbreviated compared to the standard header used in existing plugin PHP files (includes GPL notice + project URL, e.g. setup.php:2-23). For consistency (and licensing clarity), consider using the same full header template in new test files.
| <?php | ||
| /* | ||
| +-------------------------------------------------------------------------+ | ||
| | Copyright (C) 2004-2026 The Cacti Group | | ||
| +-------------------------------------------------------------------------+ | ||
| | Cacti: The Complete RRDtool-based Graphing Solution | | ||
| +-------------------------------------------------------------------------+ | ||
| */ |
There was a problem hiding this comment.
The file header block here is abbreviated compared to the standard header used in existing plugin PHP files (includes GPL notice + project URL, e.g. setup.php:2-23). For consistency (and licensing clarity), consider using the same full header template in new test files.
…dabot - Throw RuntimeException when realpath/file_get_contents fails (previously silent continue hid unscanned files) - Fix Dependabot ecosystem from npm to composer - Remove committed .omc session artifacts, add .omc/ to .gitignore Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
|
Converted to draft to serialize the stack in this repo. Blocked by #40; will un-draft after that merges to avoid cross-PR merge conflicts. |
Summary
Test plan
composer install && vendor/bin/pestpasses