Skip to content

test: add Pest v1 security test infrastructure#367

Open
somethingwithproof wants to merge 2 commits intoCacti:developfrom
somethingwithproof:test/add-security-test-infrastructure
Open

test: add Pest v1 security test infrastructure#367
somethingwithproof wants to merge 2 commits intoCacti:developfrom
somethingwithproof:test/add-security-test-infrastructure

Conversation

@somethingwithproof
Copy link
Copy Markdown

Summary

  • Add Pest v1 test scaffold with Cacti framework stubs
  • Source-scan tests for prepared statement consistency
  • PHP 7.4 compatibility verification tests
  • Plugin setup.php structure validation

Test plan

  • composer install && vendor/bin/pest passes
  • Tests verify security patterns match hardening PRs

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>
Copilot AI review requested due to automatic review settings April 9, 2026 06:55
xmacan
xmacan previously approved these changes Apr 9, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.php structure, prepared-statement usage, and PHP 7.4 compatibility.
  • Adds composer.json with 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.

Comment on lines +16 to +22
it('uses prepared DB helpers in all plugin files', function () {
$targetFiles = array(
'display.php',
'include/database.php',
'include/functions.php',
'intropage.php',
'setup.php',
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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',

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +39
if ($path === false) {
continue;
}

$contents = file_get_contents($path);

if ($contents === false) {
continue;
}
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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"
);

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +97
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;
}

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +116
function __($text, $domain = '') {
return $text;
}
}

if (!function_exists('__esc')) {
function __esc($text, $domain = '') {
return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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');

Copilot uses AI. Check for mistakes.
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants