Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "cacti/plugin_mactrack",
"description": "plugin_mactrack plugin for Cacti",
"license": "GPL-2.0-or-later",
"require-dev": {
"pestphp/pest": "^1.23"
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"autoload-dev": {
"files": [
"tests/bootstrap.php"
]
}
}
14 changes: 14 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Pest configuration file.
*/

require_once __DIR__ . '/bootstrap.php';
108 changes: 108 additions & 0 deletions tests/Security/Php74CompatibilityTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify plugin source files do not use PHP 8.0+ syntax.
* Cacti 1.2.x plugins must remain compatible with PHP 7.4.
*/

describe('PHP 7.4 compatibility in mactrack', function () {
$files = 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('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;
}

Comment on lines +29 to +102
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 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.

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 (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);

Copilot uses AI. Check for mistakes.
expect(preg_match('/\?->/', $contents))->toBe(0,
"{$relativeFile} uses nullsafe operator which requires PHP 8.0"
);
}
});
});
66 changes: 66 additions & 0 deletions tests/Security/PreparedStatementConsistencyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify migrated files use prepared DB helpers exclusively.
* Catches regressions where raw db_execute/db_fetch_* calls creep back in.
*/

describe('prepared statement consistency in mactrack', function () {
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',
Comment on lines +16 to +27
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.

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.

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

Copilot uses AI. Check for mistakes.
);

$rawPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)\s*\(/';
$preparedPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)_prepared\s*\(/';

foreach ($targetFiles as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

if ($path === false) {
continue;
}

$contents = file_get_contents($path);

if ($contents === false) {
continue;
}
Comment on lines +36 to +44
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 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.

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

Copilot uses AI. Check for mistakes.

$lines = explode("\n", $contents);
$rawCallsOutsideComments = 0;

foreach ($lines as $line) {
$trimmed = ltrim($line);

if (strpos($trimmed, '//') === 0 || strpos($trimmed, '*') === 0 || strpos($trimmed, '#') === 0) {
continue;
}

if (preg_match($rawPattern, $line) && !preg_match($preparedPattern, $line)) {
$rawCallsOutsideComments++;
}
}

expect($rawCallsOutsideComments)->toBe(0,
"File {$relativeFile} contains raw (unprepared) DB calls"
);
}
});
});
36 changes: 36 additions & 0 deletions tests/Security/SetupStructureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify setup.php defines required plugin hooks and info function.
*/

describe('mactrack setup.php structure', function () {
$source = file_get_contents(realpath(__DIR__ . '/../../setup.php'));

Comment on lines +15 to +16
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(...)) 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.

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

Copilot uses AI. Check for mistakes.
it('defines plugin_mactrack_install function', function () use ($source) {
expect($source)->toContain('function plugin_mactrack_install');
});

it('defines plugin_mactrack_version function', function () use ($source) {
expect($source)->toContain('function plugin_mactrack_version');
});

it('defines plugin_mactrack_uninstall function', function () use ($source) {
expect($source)->toContain('function plugin_mactrack_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*=>/');
});
});
Loading
Loading