test: add Pest v1 security test infrastructure#326
test: add Pest v1 security test infrastructure#326somethingwithproof 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 a Pest v1-based test harness for the plugin_mactrack repository, focused on security/compatibility guardrails (prepared-statement usage, PHP 7.4 syntax checks, and setup.php structure validation).
Changes:
- Introduces Pest bootstrap/config to run tests without a full Cacti runtime.
- Adds source-scanning security tests for prepared-statement usage and PHP 7.4 compatibility.
- Adds a
setup.phpstructure test to ensure required plugin hooks/metadata are present.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| composer.json | Adds Pest as a dev dependency and loads the test bootstrap via Composer autoload-dev. |
| tests/Pest.php | Pest configuration entrypoint that loads the test bootstrap. |
| tests/bootstrap.php | Stubs common Cacti framework functions/constants for isolated test execution. |
| tests/Security/PreparedStatementConsistencyTest.php | Scans selected plugin files to enforce prepared DB helper usage. |
| tests/Security/Php74CompatibilityTest.php | Scans selected plugin files for PHP 8+ APIs/syntax usage. |
| tests/Security/SetupStructureTest.php | Validates presence of required setup.php hooks and version keys. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| it('uses prepared DB helpers in all plugin files', function () { | ||
| $targetFiles = array( | ||
| 'mactrack_devices.php', | ||
| 'mactrack_device_types.php', | ||
| 'mactrack_interfaces.php', | ||
| 'mactrack_sites.php', | ||
| 'mactrack_snmp.php', | ||
| 'mactrack_utilities.php', | ||
| 'mactrack_view_arp.php', | ||
| 'mactrack_view_macs.php', | ||
| 'mactrack_view_sites.php', | ||
| 'setup.php', |
There was a problem hiding this comment.
PreparedStatementConsistencyTest will fail in the current tree: the listed target files (e.g. setup.php, mactrack_snmp.php, mactrack_utilities.php, mactrack_view_macs.php, etc.) still contain db_execute()/db_fetch_*() calls. Either migrate those files to the _prepared helpers first, or narrow this test to only the files that have actually been migrated (or explicitly allow known exceptions), so vendor/bin/pest can pass.
| it('uses prepared DB helpers in all plugin files', function () { | |
| $targetFiles = array( | |
| 'mactrack_devices.php', | |
| 'mactrack_device_types.php', | |
| 'mactrack_interfaces.php', | |
| 'mactrack_sites.php', | |
| 'mactrack_snmp.php', | |
| 'mactrack_utilities.php', | |
| 'mactrack_view_arp.php', | |
| 'mactrack_view_macs.php', | |
| 'mactrack_view_sites.php', | |
| 'setup.php', | |
| it('uses prepared DB helpers in migrated plugin files', function () { | |
| $targetFiles = array( | |
| 'mactrack_devices.php', | |
| 'mactrack_device_types.php', | |
| 'mactrack_interfaces.php', | |
| 'mactrack_sites.php', | |
| 'mactrack_view_arp.php', | |
| 'mactrack_view_sites.php', |
| if ($path === false) { | ||
| continue; | ||
| } | ||
|
|
||
| $contents = file_get_contents($path); | ||
|
|
||
| if ($contents === false) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
This test silently continues when a target file path can't be resolved or read (realpath(...) === false or file_get_contents(...) === false). That can make the test pass while skipping the checks entirely. Consider failing the test (or at least asserting the file exists/is readable) so missing/renamed files are caught.
| if ($path === false) { | |
| continue; | |
| } | |
| $contents = file_get_contents($path); | |
| if ($contents === false) { | |
| continue; | |
| } | |
| expect($path)->not->toBeFalse("Failed to resolve path for {$relativeFile}"); | |
| $contents = file_get_contents($path); | |
| expect($contents)->not->toBeFalse("Failed to read contents of {$relativeFile}"); |
| $source = file_get_contents(realpath(__DIR__ . '/../../setup.php')); | ||
|
|
There was a problem hiding this comment.
file_get_contents(realpath(...)) will emit warnings and set $source to false if realpath() fails (e.g., if setup.php is missing in a checkout/layout). It would be more robust to resolve the path separately and assert it is not false (and that the read succeeded) so failures are clear and not just runtime warnings/type errors.
| $source = file_get_contents(realpath(__DIR__ . '/../../setup.php')); | |
| $setupPath = realpath(__DIR__ . '/../../setup.php'); | |
| if ($setupPath === false) { | |
| throw new RuntimeException('Failed to resolve setup.php path for mactrack structure test.'); | |
| } | |
| $source = file_get_contents($setupPath); | |
| if ($source === false) { | |
| throw new RuntimeException(sprintf('Failed to read setup.php for mactrack structure test: %s', $setupPath)); | |
| } |
| 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 PHP-compatibility checks continue when a file can't be resolved/read, which can lead to false-green results if a file is renamed or missing. Consider asserting the file exists/is readable (or failing with a clear message) instead of skipping it.
| 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; | |
| } | |
| $readSourceFile = function (string $relativeFile): string { | |
| $path = realpath(__DIR__ . '/../../' . $relativeFile); | |
| if ($path === false) { | |
| throw new \RuntimeException("Unable to resolve source file for PHP 7.4 compatibility check: {$relativeFile}"); | |
| } | |
| $contents = file_get_contents($path); | |
| if ($contents === false) { | |
| throw new \RuntimeException("Unable to read source file for PHP 7.4 compatibility check: {$relativeFile}"); | |
| } | |
| return $contents; | |
| }; | |
| it('does not use str_contains (PHP 8.0)', function () use ($files, $readSourceFile) { | |
| foreach ($files as $relativeFile) { | |
| $contents = $readSourceFile($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, $readSourceFile) { | |
| foreach ($files as $relativeFile) { | |
| $contents = $readSourceFile($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, $readSourceFile) { | |
| foreach ($files as $relativeFile) { | |
| $contents = $readSourceFile($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, $readSourceFile) { | |
| foreach ($files as $relativeFile) { | |
| $contents = $readSourceFile($relativeFile); |
…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 #324; will un-draft after that merges to avoid cross-PR merge conflicts. |
TheWitness
left a comment
There was a problem hiding this comment.
Let's exclude the composer.json and leverage in the base.
Summary
Test plan
composer install && vendor/bin/pestpasses