test: add Pest v1 security test infrastructure#367
test: add Pest v1 security test infrastructure#367somethingwithproof 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 scaffold to the intropage plugin repo to enforce basic security/compatibility invariants via source-scanning tests.
Changes:
- Introduces Pest configuration + bootstrap with Cacti framework stubs.
- Adds security-oriented tests for
setup.phpstructure, prepared-statement usage, and PHP 7.4 compatibility. - Adds
composer.jsonwith Pest as a dev dependency and dev autoload for the test bootstrap.
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 v1 as a dev dependency and wires in test bootstrap via autoload-dev. |
tests/Pest.php |
Pest entry/config file that loads the test bootstrap. |
tests/bootstrap.php |
Provides stubs for Cacti DB/i18n/framework functions for isolated testing. |
tests/Security/SetupStructureTest.php |
Source-scan checks that required setup functions/keys exist. |
tests/Security/PreparedStatementConsistencyTest.php |
Source-scan intended to enforce exclusive use of prepared DB helpers. |
tests/Security/Php74CompatibilityTest.php |
Source-scan checks to avoid a small set of PHP 8.0+ syntax/APIs. |
| it('uses prepared DB helpers in all plugin files', function () { | ||
| $targetFiles = array( | ||
| 'display.php', | ||
| 'include/database.php', | ||
| 'include/functions.php', | ||
| 'intropage.php', | ||
| 'setup.php', |
There was a problem hiding this comment.
This test will currently fail because the target list includes files that still contain raw db_execute/db_fetch_* calls (e.g., include/database.php has multiple db_execute() calls, and include/functions.php has db_fetch_assoc()/db_execute()). Either migrate those files to *_prepared helpers first, or narrow/adjust the scan to only enforce this rule on the files that have actually been migrated (or explicitly allow raw calls for DDL/static SQL where prepared statements aren’t applicable).
| it('uses prepared DB helpers in all plugin files', function () { | |
| $targetFiles = array( | |
| 'display.php', | |
| 'include/database.php', | |
| 'include/functions.php', | |
| 'intropage.php', | |
| 'setup.php', | |
| it('uses prepared DB helpers in migrated plugin files', function () { | |
| $targetFiles = array( | |
| 'display.php', | |
| 'intropage.php', | |
| 'setup.php', |
| if ($path === false) { | ||
| continue; | ||
| } | ||
|
|
||
| $contents = file_get_contents($path); | ||
|
|
||
| if ($contents === false) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
The test currently continues when a target file is missing or unreadable, which can let regressions slip through (the assertion still passes). Consider failing the test when realpath()/file_get_contents() fails for an expected file so the scan is always enforced.
| if ($path === false) { | |
| continue; | |
| } | |
| $contents = file_get_contents($path); | |
| if ($contents === false) { | |
| continue; | |
| } | |
| expect($path)->not->toBeFalse( | |
| "Expected file {$relativeFile} to exist and be resolvable for prepared statement scanning" | |
| ); | |
| $contents = file_get_contents($path); | |
| expect($contents)->not->toBeFalse( | |
| "Expected file {$relativeFile} to be readable for prepared statement scanning" | |
| ); |
| 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 compatibility checks silently skip files when realpath()/file_get_contents() fails. That means the test suite can pass even if expected source files go missing (or paths change). Consider asserting the file exists and is readable (and ideally factor the repeated “resolve + read” logic into a small helper to avoid duplicating it across each it() block).
| 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 ($relativeFile) { | |
| $fullPath = __DIR__ . '/../../' . $relativeFile; | |
| expect(is_file($fullPath))->toBeTrue( | |
| "Expected source file {$relativeFile} to exist" | |
| ); | |
| expect(is_readable($fullPath))->toBeTrue( | |
| "Expected source file {$relativeFile} to be readable" | |
| ); | |
| $path = realpath($fullPath); | |
| expect($path)->not->toBeFalse( | |
| "Failed to resolve real path for {$relativeFile}" | |
| ); | |
| $contents = file_get_contents($path); | |
| expect($contents)->not->toBeFalse( | |
| "Failed to read source file {$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); |
| function __($text, $domain = '') { | ||
| return $text; | ||
| } | ||
| } | ||
|
|
||
| if (!function_exists('__esc')) { | ||
| function __esc($text, $domain = '') { | ||
| return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); |
There was a problem hiding this comment.
The test bootstrap stubs for __() / __esc() only accept 2 parameters, but this plugin calls __() with additional formatting arguments (e.g. __('%s Sec', $value, 'intropage')). On PHP 8+ this will be a fatal “too many arguments” if/when tests include plugin files. Update these stubs to accept variadic args (and ideally apply vsprintf-style formatting) so the plugin can be safely loaded under test.
| function __($text, $domain = '') { | |
| return $text; | |
| } | |
| } | |
| if (!function_exists('__esc')) { | |
| function __esc($text, $domain = '') { | |
| return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); | |
| function __($text, ...$args) { | |
| if (!empty($args) && is_string($args[count($args) - 1])) { | |
| array_pop($args); | |
| } | |
| if (!empty($args)) { | |
| $formatted = @vsprintf($text, $args); | |
| if ($formatted !== false) { | |
| return $formatted; | |
| } | |
| } | |
| return $text; | |
| } | |
| } | |
| if (!function_exists('__esc')) { | |
| function __esc($text, ...$args) { | |
| return htmlspecialchars(__($text, ...$args), ENT_QUOTES | ENT_HTML5, 'UTF-8'); |
…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>
Summary
Test plan
composer install && vendor/bin/pestpasses