Skip to content

test: add Pest v1 security test infrastructure#42

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

test: add Pest v1 security test infrastructure#42
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
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 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.

Comment on lines +17 to +24
$targetFiles = array(
'associate_os_type.php',
'hmib.php',
'hmib_types.php',
'poller_graphs.php',
'poller_hmib.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 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).

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +39
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.

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.

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

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +98
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 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.

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;
}
$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);

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +35
$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*=>/');
});
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.

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.

Suggested change
$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*=>/');
});

Copilot uses AI. Check for mistakes.
Comment thread tests/bootstrap.php
}

if (!defined('CACTI_PATH_BASE')) {
define('CACTI_PATH_BASE', '/var/www/html/cacti');
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.

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.

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

Copilot uses AI. Check for mistakes.
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
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 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.

Suggested change
+-------------------------------------------------------------------------+
+-------------------------------------------------------------------------+
| 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/ |
+-------------------------------------------------------------------------+

Copilot uses AI. Check for mistakes.
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
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 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.

Suggested change
+-------------------------------------------------------------------------+
+-------------------------------------------------------------------------+
| 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/ |
+-------------------------------------------------------------------------+

Copilot uses AI. Check for mistakes.
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
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 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.

Suggested change
+-------------------------------------------------------------------------+
+-------------------------------------------------------------------------+
| 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/ |
+-------------------------------------------------------------------------+

Copilot uses AI. Check for mistakes.
Comment thread tests/bootstrap.php
Comment on lines +1 to +8
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
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 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.

Copilot uses AI. Check for mistakes.
Comment thread tests/Pest.php
Comment on lines +1 to +8
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
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 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.

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>
@somethingwithproof somethingwithproof marked this pull request as draft April 11, 2026 00:09
@somethingwithproof
Copy link
Copy Markdown
Author

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.

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.

2 participants