mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
fix(automation): bind template IDs and enforce 100% coverage (#7612)
* Harden automation template ID filters * Match automation test size semantics * Test API automation tools at 100% coverage * Make API coverage fixture load-order safe * fix(tests): isolate API coverage stubs from unit suite * ci(coverage): isolate API automation coverage workflow * perf(automation): eliminate tree display N+1 queries --------- Co-authored-by: TheWitness <thewitness@cacti.net>
This commit is contained in:
1 parent
eaa1f022a9
commit
ee3ea262d8
6 files changed
+520
-78
No files matched your search
@@ -0,0 +1,74 @@
|
||||
name: API Automation Tools Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- '.github/workflows/api-automation-tools-coverage.yml'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'lib/api_automation_tools.php'
|
||||
- 'phpunit-api-automation-tools.xml'
|
||||
- 'tests/Coverage/ApiAutomationToolsCoverageTest.php'
|
||||
- 'tests/Unit/AutomationPreparedInClauseTest.php'
|
||||
- 'tests/fixtures/automation_prepared_in_clause_probe.php'
|
||||
pull_request:
|
||||
branches: [develop]
|
||||
paths:
|
||||
- '.github/workflows/api-automation-tools-coverage.yml'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
- 'lib/api_automation_tools.php'
|
||||
- 'phpunit-api-automation-tools.xml'
|
||||
- 'tests/Coverage/ApiAutomationToolsCoverageTest.php'
|
||||
- 'tests/Unit/AutomationPreparedInClauseTest.php'
|
||||
- 'tests/fixtures/automation_prepared_in_clause_probe.php'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: api-automation-tools-coverage-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPOSER_ALLOW_SUPERUSER: 1
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: API automation tools 100% coverage
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
|
||||
|
||||
- name: Setup PHP 8.4 with Xdebug
|
||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
coverage: xdebug
|
||||
extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring
|
||||
|
||||
- name: Cache Composer packages
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6
|
||||
with:
|
||||
path: include/vendor
|
||||
key: ${{ runner.os }}-php-api-automation-coverage-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-php-api-automation-coverage-
|
||||
|
||||
- name: Install dependencies
|
||||
run: composer install --prefer-dist --no-progress --no-interaction
|
||||
|
||||
- name: Enforce line coverage
|
||||
env:
|
||||
XDEBUG_MODE: coverage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
include/vendor/bin/pest \
|
||||
--configuration=phpunit-api-automation-tools.xml \
|
||||
--coverage \
|
||||
--coverage-text \
|
||||
--min=100
|
||||
+113
-78
@@ -41,6 +41,55 @@ function getHostTemplates() : array {
|
||||
return $host_templates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes database IDs and builds placeholders for a prepared IN clause.
|
||||
*
|
||||
* @param mixed $ids A positive integer ID, an array of positive integer IDs, or false/an empty array for no filter.
|
||||
*
|
||||
* @return array{placeholders: string, params: array<int, int>}|false Prepared placeholders and integer parameters,
|
||||
* or false when validation fails.
|
||||
*/
|
||||
function automation_prepare_id_list(mixed $ids) : array|false {
|
||||
if ($ids === false || $ids === []) {
|
||||
return [
|
||||
'placeholders' => '',
|
||||
'params' => []
|
||||
];
|
||||
}
|
||||
|
||||
if (!is_array($ids)) {
|
||||
$ids = [$ids];
|
||||
}
|
||||
|
||||
$params = [];
|
||||
|
||||
foreach ($ids as $id) {
|
||||
if (is_string($id) && ctype_digit($id)) {
|
||||
$normalized_id = ltrim($id, '0');
|
||||
$normalized_id = $normalized_id === '' ? '0' : $normalized_id;
|
||||
$max_id = (string) PHP_INT_MAX;
|
||||
|
||||
if (strlen($normalized_id) > strlen($max_id) ||
|
||||
(strlen($normalized_id) === strlen($max_id) && strcmp($normalized_id, $max_id) > 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!(is_int($id) || (is_string($id) && ctype_digit($id))) || (int) $id < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$params[(int) $id] = (int) $id;
|
||||
}
|
||||
|
||||
$params = array_values($params);
|
||||
|
||||
return [
|
||||
'placeholders' => implode(', ', array_fill(0, count($params), '?')),
|
||||
'params' => $params
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves hosts based on their description.
|
||||
*
|
||||
@@ -49,32 +98,23 @@ function getHostTemplates() : array {
|
||||
* @return mixed - Returns an array of hosts that match the given description, or false on failure.
|
||||
*/
|
||||
function getHostsByDescription(mixed $hostTemplateIds = false) : mixed {
|
||||
$hosts = [];
|
||||
$hosts = [];
|
||||
$filter = automation_prepare_id_list($hostTemplateIds);
|
||||
|
||||
if ($hostTemplateIds !== false) {
|
||||
if (!is_array($hostTemplateIds)) {
|
||||
$hostTemplateIds = [$hostTemplateIds];
|
||||
}
|
||||
if ($filter === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($hostTemplateIds !== false && cacti_sizeof($hostTemplateIds)) {
|
||||
foreach ($hostTemplateIds as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$sql_where = 'WHERE ht.id IN (' . implode(',', $hostTemplateIds) . ')';
|
||||
if ($filter['placeholders'] !== '') {
|
||||
$sql_where = 'WHERE h.host_template_id IN (' . $filter['placeholders'] . ')';
|
||||
} else {
|
||||
$sql_where = '';
|
||||
}
|
||||
|
||||
$tmpArray = db_fetch_assoc("SELECT h.id, h.description
|
||||
$tmpArray = db_fetch_assoc_prepared("SELECT h.id, h.description
|
||||
FROM host AS h
|
||||
INNER JOIN host_template AS ht
|
||||
ON h.host_template_id = ht.id
|
||||
$sql_where
|
||||
ORDER BY h.description");
|
||||
ORDER BY h.description", $filter['params']);
|
||||
|
||||
if ($tmpArray !== false && cacti_sizeof($tmpArray)) {
|
||||
foreach ($tmpArray as $tmp) {
|
||||
@@ -112,32 +152,23 @@ function getSites() : array {
|
||||
* @return mixed - Returns an array of hosts if successful, or false on failure.
|
||||
*/
|
||||
function getHosts(mixed $hostTemplateIds = false) : mixed {
|
||||
$hosts = [];
|
||||
$hosts = [];
|
||||
$filter = automation_prepare_id_list($hostTemplateIds);
|
||||
|
||||
if ($hostTemplateIds !== false) {
|
||||
if (!is_array($hostTemplateIds)) {
|
||||
$hostTemplateIds = [$hostTemplateIds];
|
||||
}
|
||||
if ($filter === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($hostTemplateIds !== false && cacti_sizeof($hostTemplateIds)) {
|
||||
foreach ($hostTemplateIds as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$sql_where = 'WHERE ht.id IN (' . implode(',', $hostTemplateIds) . ')';
|
||||
if ($filter['placeholders'] !== '') {
|
||||
$sql_where = 'WHERE h.host_template_id IN (' . $filter['placeholders'] . ')';
|
||||
} else {
|
||||
$sql_where = '';
|
||||
}
|
||||
|
||||
$tmpArray = db_fetch_assoc("SELECT h.id, h.hostname, h.description, h.host_template_id
|
||||
$tmpArray = db_fetch_assoc_prepared("SELECT h.id, h.hostname, h.description, h.host_template_id
|
||||
FROM host AS h
|
||||
LEFT JOIN host_template AS ht
|
||||
ON h.host_template_id = ht.id
|
||||
$sql_where
|
||||
ORDER BY h.id");
|
||||
ORDER BY h.id", $filter['params']);
|
||||
|
||||
if ($tmpArray !== false && cacti_sizeof($tmpArray)) {
|
||||
foreach ($tmpArray as $host) {
|
||||
@@ -359,31 +390,24 @@ function getGraphTemplates() : array {
|
||||
*/
|
||||
function getGraphTemplatesByHostTemplate(mixed $host_template_ids = false) : mixed {
|
||||
$graph_templates = [];
|
||||
$filter = automation_prepare_id_list($host_template_ids);
|
||||
|
||||
if ($host_template_ids !== false) {
|
||||
if (!is_array($host_template_ids)) {
|
||||
$host_template_ids = [$host_template_ids];
|
||||
}
|
||||
if ($filter === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($host_template_ids !== false && cacti_sizeof($host_template_ids)) {
|
||||
foreach ($host_template_ids as $id) {
|
||||
if (!is_numeric($id)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$sql_where = 'WHERE htg.host_template_id IN (' . implode(',', $host_template_ids) . ')';
|
||||
if ($filter['placeholders'] !== '') {
|
||||
$sql_where = 'WHERE htg.host_template_id IN (' . $filter['placeholders'] . ')';
|
||||
} else {
|
||||
$sql_where = '';
|
||||
}
|
||||
|
||||
$tmpArray = db_fetch_assoc_prepared("SELECT htg.graph_template_id AS id, gt.name AS name
|
||||
$tmpArray = db_fetch_assoc_prepared("SELECT DISTINCT htg.graph_template_id AS id, gt.name AS name
|
||||
FROM host_template_graph AS htg
|
||||
LEFT JOIN graph_templates AS gt
|
||||
INNER JOIN graph_templates AS gt
|
||||
ON htg.graph_template_id = gt.id
|
||||
$sql_where
|
||||
ORDER by gt.name ASC");
|
||||
ORDER by gt.name ASC", $filter['params']);
|
||||
|
||||
if (cacti_sizeof($tmpArray)) {
|
||||
foreach ($tmpArray as $t) {
|
||||
@@ -684,23 +708,41 @@ function displayTrees(bool $quietMode = false) : void {
|
||||
function displayTreeNodes(int $tree_id, string $nodeType = '', int $parentNode = 0, bool $quietMode = false) : void {
|
||||
global $tree_sort_types, $tree_item_types, $host_group_types;
|
||||
|
||||
$nodes = db_fetch_assoc_prepared('SELECT gti.id, gti.parent, gti.local_graph_id, gti.title,
|
||||
gti.host_id, gti.host_grouping_type, gti.sort_children_type,
|
||||
gtg.title_cache AS graph_title, h.hostname AS host_name
|
||||
FROM graph_tree_items AS gti
|
||||
LEFT JOIN graph_templates_graph AS gtg
|
||||
ON gtg.local_graph_id = gti.local_graph_id
|
||||
LEFT JOIN host AS h
|
||||
ON h.id = gti.host_id
|
||||
WHERE gti.graph_tree_id = ?
|
||||
ORDER BY gti.parent, gti.position, gti.id', [$tree_id]);
|
||||
|
||||
$nodesByParent = [];
|
||||
|
||||
foreach ($nodes ?: [] as $node) {
|
||||
$nodesByParent[(int) ($node['parent'] ?? 0)][] = $node;
|
||||
}
|
||||
|
||||
$visited = [];
|
||||
|
||||
if ($parentNode == 0) {
|
||||
if (!$quietMode) {
|
||||
print 'Known Tree Nodes: (type, id, parentid, title, attribs)' . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
$parentID = 0;
|
||||
$render = function (int $parent) use (&$render, &$visited, $nodesByParent, $nodeType, $tree_sort_types, $tree_item_types, $host_group_types) : void {
|
||||
foreach ($nodesByParent[$parent] ?? [] as $node) {
|
||||
$nodeId = (int) $node['id'];
|
||||
|
||||
$nodes = db_fetch_assoc_prepared('SELECT id, local_graph_id, title,
|
||||
host_id, host_grouping_type, sort_children_type
|
||||
FROM graph_tree_items
|
||||
WHERE graph_tree_id = ?
|
||||
AND parent = ?
|
||||
ORDER BY position', [$tree_id, $parentNode]);
|
||||
if (isset($visited[$nodeId])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$visited[$nodeId] = true;
|
||||
|
||||
if (cacti_sizeof($nodes)) {
|
||||
foreach ($nodes as $node) {
|
||||
// taken from tree.php, function item_edit()
|
||||
$current_type = TREE_ITEM_TYPE_HEADER;
|
||||
|
||||
@@ -718,10 +760,10 @@ function displayTreeNodes(int $tree_id, string $nodeType = '', int $parentNode =
|
||||
print $tree_item_types[$current_type] . "\t";
|
||||
print $node['id'] . "\t";
|
||||
|
||||
if ($parentNode == 0) {
|
||||
if ($parent == 0) {
|
||||
print "N/A\t";
|
||||
} else {
|
||||
print $parentNode . "\t";
|
||||
print $parent . "\t";
|
||||
}
|
||||
|
||||
print $node['title'] . "\t";
|
||||
@@ -729,7 +771,7 @@ function displayTreeNodes(int $tree_id, string $nodeType = '', int $parentNode =
|
||||
print PHP_EOL;
|
||||
}
|
||||
|
||||
displayTreeNodes($tree_id, $nodeType, $node['id'], $quietMode);
|
||||
$render($nodeId);
|
||||
|
||||
break;
|
||||
case TREE_ITEM_TYPE_GRAPH:
|
||||
@@ -737,18 +779,13 @@ function displayTreeNodes(int $tree_id, string $nodeType = '', int $parentNode =
|
||||
print $tree_item_types[$current_type] . "\t";
|
||||
print $node['id'] . "\t";
|
||||
|
||||
if ($parentNode == 0) {
|
||||
if ($parent == 0) {
|
||||
print "N/A\t";
|
||||
} else {
|
||||
print $parentNode . "\t";
|
||||
print $parent . "\t";
|
||||
}
|
||||
|
||||
// fetch the title for that graph
|
||||
$graph_title = db_fetch_cell_prepared('SELECT gtg.title_cache AS name
|
||||
FROM graph_templates_graph AS gtg
|
||||
WHERE gtg.local_graph_id = ?', [$node['local_graph_id']]);
|
||||
|
||||
print $graph_title . "\t";
|
||||
print $node['graph_title'] . "\t";
|
||||
print PHP_EOL;
|
||||
}
|
||||
|
||||
@@ -758,15 +795,13 @@ function displayTreeNodes(int $tree_id, string $nodeType = '', int $parentNode =
|
||||
print $tree_item_types[$current_type] . "\t";
|
||||
print $node['id'] . "\t";
|
||||
|
||||
if ($parentNode == 0) {
|
||||
if ($parent == 0) {
|
||||
print "N/A\t";
|
||||
} else {
|
||||
print $parentNode . "\t";
|
||||
print $parent . "\t";
|
||||
}
|
||||
|
||||
$name = db_fetch_cell_prepared('SELECT hostname FROM host WHERE id = ?', [$node['host_id']]);
|
||||
|
||||
print $name . "\t";
|
||||
print $node['host_name'] . "\t";
|
||||
print $host_group_types[$node['host_grouping_type']] . "\t";
|
||||
print PHP_EOL;
|
||||
}
|
||||
@@ -774,7 +809,9 @@ function displayTreeNodes(int $tree_id, string $nodeType = '', int $parentNode =
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$render($parentNode);
|
||||
|
||||
if ($parentNode == 0) {
|
||||
if (!$quietMode) {
|
||||
@@ -885,7 +922,7 @@ function displayUsers(bool $quietMode = false) : void {
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function displayGroups(bool $quietMode = false) {
|
||||
function displayGroups(bool $quietMode = false) : void {
|
||||
if (!$quietMode) {
|
||||
print 'Known Groups: (id, name, description)' . PHP_EOL;
|
||||
}
|
||||
@@ -905,6 +942,4 @@ function displayGroups(bool $quietMode = false) {
|
||||
if (!$quietMode) {
|
||||
print PHP_EOL;
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<phpunit bootstrap="include/vendor/autoload.php" beStrictAboutOutputDuringTests="false">
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1"/>
|
||||
<env name="CACTI_TEST_BOOTSTRAP" value="1"/>
|
||||
</php>
|
||||
<source>
|
||||
<include>
|
||||
<file>lib/api_automation_tools.php</file>
|
||||
</include>
|
||||
</source>
|
||||
<testsuites>
|
||||
<testsuite name="API automation tools coverage">
|
||||
<file>tests/Coverage/ApiAutomationToolsCoverageTest.php</file>
|
||||
<file>tests/Unit/AutomationPreparedInClauseTest.php</file>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| 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. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
if (!defined('TREE_ITEM_TYPE_HEADER')) {
|
||||
define('TREE_ITEM_TYPE_HEADER', 1);
|
||||
}
|
||||
|
||||
if (!defined('TREE_ITEM_TYPE_GRAPH')) {
|
||||
define('TREE_ITEM_TYPE_GRAPH', 2);
|
||||
}
|
||||
|
||||
if (!defined('TREE_ITEM_TYPE_HOST')) {
|
||||
define('TREE_ITEM_TYPE_HOST', 3);
|
||||
}
|
||||
|
||||
$GLOBALS['automation_assoc_results'] = [];
|
||||
$GLOBALS['automation_cell_results'] = [];
|
||||
$GLOBALS['automation_queries'] = [];
|
||||
$GLOBALS['tree_sort_types'] = [1 => 'Manual'];
|
||||
$GLOBALS['tree_item_types'] = [1 => 'Header', 2 => 'Graph', 3 => 'Host'];
|
||||
$GLOBALS['host_group_types'] = [1 => 'Graph Template'];
|
||||
|
||||
if (!function_exists('cacti_sizeof')) {
|
||||
function cacti_sizeof(mixed $value) : int {
|
||||
return is_array($value) ? count($value) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
function db_fetch_assoc(string $sql) : array|false {
|
||||
$GLOBALS['automation_queries'][] = [$sql, []];
|
||||
|
||||
return array_shift($GLOBALS['automation_assoc_results']);
|
||||
}
|
||||
|
||||
function db_fetch_assoc_prepared(string $sql, array $params = []) : array|false {
|
||||
$GLOBALS['automation_queries'][] = [$sql, $params];
|
||||
|
||||
return array_shift($GLOBALS['automation_assoc_results']);
|
||||
}
|
||||
|
||||
function db_fetch_cell_prepared(string $sql, array $params = []) : mixed {
|
||||
$GLOBALS['automation_queries'][] = [$sql, $params];
|
||||
|
||||
return array_shift($GLOBALS['automation_cell_results']);
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/lib/api_automation_tools.php';
|
||||
|
||||
beforeEach(function () : void {
|
||||
$GLOBALS['automation_assoc_results'] = [];
|
||||
$GLOBALS['automation_cell_results'] = [];
|
||||
$GLOBALS['automation_queries'] = [];
|
||||
});
|
||||
|
||||
test('automation lookup helpers map database rows and bind optional filters', function () : void {
|
||||
$GLOBALS['automation_assoc_results'] = [
|
||||
[['id' => 2, 'name' => 'Linux']],
|
||||
[['id' => 3, 'description' => 'router-a']],
|
||||
[['id' => 4, 'name' => 'DC']],
|
||||
[['id' => 5, 'hostname' => 'host-a', 'description' => 'Host A', 'host_template_id' => 2]],
|
||||
[['data_template_id' => 6, 'name' => 'community', 'default' => 'public', 'description' => 'Community']],
|
||||
[['id' => 7, 'hostname' => '192.0.2.7']],
|
||||
[['field_name' => 'ifName']],
|
||||
[['field_value' => 'eth0']],
|
||||
[['id' => 8, 'name' => 'Interfaces']],
|
||||
[['id' => 9, 'name' => 'Interface Statistics']],
|
||||
[['id' => 10, 'name' => 'Traffic']],
|
||||
[['id' => 11, 'name' => 'Traffic']],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[]
|
||||
];
|
||||
|
||||
expect(getHostTemplates())->toBe([0 => 'None', 2 => 'Linux'])
|
||||
->and(getHostsByDescription([2]))->toBe(['router-a' => 3])
|
||||
->and(getSites())->toBe([4 => ['id' => 4, 'name' => 'DC']])
|
||||
->and(getHosts([2]))->toBe([5 => ['id' => 5, 'hostname' => 'host-a', 'description' => 'Host A', 'host_template_id' => 2]])
|
||||
->and(getInputFields(10))->toHaveKey('6:community')
|
||||
->and(getAddresses())->toBe(['192.0.2.7' => 7])
|
||||
->and(getSNMPFields(5, 8))->toBe(['ifName' => 1])
|
||||
->and(getSNMPValues(5, 'ifName', 8))->toBe(['eth0' => 1])
|
||||
->and(getSNMPQueries())->toBe([8 => 'Interfaces'])
|
||||
->and(getSNMPQueryTypes(8))->toBe([9 => 'Interface Statistics'])
|
||||
->and(getGraphTemplates())->toBe([10 => 'Traffic'])
|
||||
->and(getGraphTemplatesByHostTemplate([2]))->toBe([11 => 'Traffic'])
|
||||
->and(getHostsByDescription())->toBe([])
|
||||
->and(getHosts())->toBe([])
|
||||
->and(getGraphTemplatesByHostTemplate())->toBe([])
|
||||
->and(getSNMPFields(5))->toBe([])
|
||||
->and(getSNMPValues(5, 'ifName'))->toBe([])
|
||||
->and(getHostsByDescription(['invalid']))->toBeFalse()
|
||||
->and(getHosts(['invalid']))->toBeFalse()
|
||||
->and(getGraphTemplatesByHostTemplate(['invalid']))->toBeFalse();
|
||||
|
||||
expect($GLOBALS['automation_queries'][1][1])->toBe([2])
|
||||
->and($GLOBALS['automation_queries'][6][1])->toBe([5, 8])
|
||||
->and($GLOBALS['automation_queries'][7][1])->toBe([5, 'ifName', 8]);
|
||||
|
||||
expect(automation_prepare_id_list(2))->toBe(['placeholders' => '?', 'params' => [2]])
|
||||
->and(automation_prepare_id_list('0002'))->toBe(['placeholders' => '?', 'params' => [2]])
|
||||
->and(automation_prepare_id_list([2, '02', 3]))->toBe(['placeholders' => '?, ?', 'params' => [2, 3]])
|
||||
->and(automation_prepare_id_list((string) PHP_INT_MAX . '0'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('automation display helpers render every supported record type', function () : void {
|
||||
$GLOBALS['automation_assoc_results'] = [
|
||||
[['snmp_community' => 'public']],
|
||||
[['id' => 1, 'sort_type' => 1, 'name' => 'Main']],
|
||||
[
|
||||
['id' => 1, 'parent' => 0, 'local_graph_id' => 0, 'title' => 'Network', 'host_id' => 0, 'host_grouping_type' => 1, 'sort_children_type' => 1, 'graph_title' => null, 'host_name' => null],
|
||||
['id' => 2, 'parent' => 0, 'local_graph_id' => 22, 'title' => '', 'host_id' => 0, 'host_grouping_type' => 1, 'sort_children_type' => 1, 'graph_title' => 'Root Graph', 'host_name' => null],
|
||||
['id' => 3, 'parent' => 0, 'local_graph_id' => 0, 'title' => '', 'host_id' => 33, 'host_grouping_type' => 1, 'sort_children_type' => 1, 'graph_title' => null, 'host_name' => 'router-a'],
|
||||
['id' => 4, 'parent' => 1, 'local_graph_id' => 0, 'title' => 'Subnet', 'host_id' => 0, 'host_grouping_type' => 1, 'sort_children_type' => 1, 'graph_title' => null, 'host_name' => null],
|
||||
['id' => 5, 'parent' => 1, 'local_graph_id' => 44, 'title' => '', 'host_id' => 0, 'host_grouping_type' => 1, 'sort_children_type' => 1, 'graph_title' => 'Nested Graph', 'host_name' => null],
|
||||
['id' => 6, 'parent' => 1, 'local_graph_id' => 0, 'title' => '', 'host_id' => 66, 'host_grouping_type' => 1, 'sort_children_type' => 1, 'graph_title' => null, 'host_name' => 'router-b'],
|
||||
['id' => 1, 'parent' => 1, 'local_graph_id' => 0, 'title' => 'Cycle', 'host_id' => 0, 'host_grouping_type' => 1, 'sort_children_type' => 1, 'graph_title' => null, 'host_name' => null]
|
||||
],
|
||||
[['id' => 1, 'steps' => 1, 'rows' => 600, 'name' => 'Daily']],
|
||||
[['id' => 1, 'name' => 'Traffic', 'template_name' => 'Interface']],
|
||||
[['id' => 1, 'username' => 'admin', 'full_name' => 'Administrator']],
|
||||
[['id' => 1, 'name' => 'Operators', 'description' => 'Ops']]
|
||||
];
|
||||
ob_start();
|
||||
displayQueryTypes([1 => 'Interface']);
|
||||
displayHostTemplates([2 => 'Linux']);
|
||||
displayCommunities();
|
||||
displaySNMPFields(['ifName' => 1], 5);
|
||||
displaySNMPValues(['eth0' => 1], 5, 'ifName');
|
||||
displaySNMPQueries([8 => 'Interfaces']);
|
||||
displayInputFields([['data_template_id' => 6, 'name' => 'community', 'default' => 'public', 'description' => 'Community']]);
|
||||
displayGraphTemplates([10 => 'Traffic']);
|
||||
displayHosts([['id' => 5, 'hostname' => 'host-a', 'host_template_id' => 2, 'description' => 'Host A']]);
|
||||
displaySites([['id' => 4, 'name' => 'DC']]);
|
||||
displayTrees();
|
||||
displayTreeNodes(1);
|
||||
displayRRAs();
|
||||
displayHostGraphs(5);
|
||||
displayUsers();
|
||||
displayGroups();
|
||||
$output = ob_get_clean();
|
||||
|
||||
expect($output)->toContain('Known SNMP Query Types')
|
||||
->toContain('public')
|
||||
->toContain('Nested Graph')
|
||||
->toContain('Root Graph')
|
||||
->toContain('router-a')
|
||||
->toContain('Daily')
|
||||
->toContain('Administrator')
|
||||
->toContain('Operators');
|
||||
});
|
||||
|
||||
test('quiet and empty display inputs produce no output', function () : void {
|
||||
$GLOBALS['automation_assoc_results'] = [false, [], [], [], [], [], []];
|
||||
|
||||
ob_start();
|
||||
displayQueryTypes(false, true);
|
||||
displayHostTemplates(false, true);
|
||||
displayCommunities(true);
|
||||
displaySNMPFields(false, 1, true);
|
||||
displaySNMPValues(false, 1, 'field', true);
|
||||
displaySNMPQueries(false, true);
|
||||
displayInputFields(false, true);
|
||||
displayGraphTemplates(false, true);
|
||||
displayHosts(false, true);
|
||||
displaySites(false, true);
|
||||
displayTrees(true);
|
||||
displayTreeNodes(1, 'graph', 1, true);
|
||||
displayRRAs(true);
|
||||
displayHostGraphs(1, true);
|
||||
displayUsers(true);
|
||||
displayGroups(true);
|
||||
$output = ob_get_clean();
|
||||
|
||||
expect($output)->toBe('');
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| 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. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
if (!function_exists('cacti_sizeof')) {
|
||||
/**
|
||||
* Returns the size of an array test value.
|
||||
*
|
||||
* @param mixed $value Value to count.
|
||||
*
|
||||
* @return int Number of elements.
|
||||
*/
|
||||
function cacti_sizeof(mixed $value) : int {
|
||||
return is_array($value) ? count($value) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/lib/api_automation_tools.php';
|
||||
require_once dirname(__DIR__) . '/Helpers/IsolatedProbe.php';
|
||||
|
||||
test('ID list normalization is strict and fail closed', function () : void {
|
||||
expect(cacti_sizeof(new ArrayObject([1])))->toBe(0)
|
||||
->and(automation_prepare_id_list(false))->toBe(['placeholders' => '', 'params' => []])
|
||||
->and(automation_prepare_id_list([]))->toBe(['placeholders' => '', 'params' => []])
|
||||
->and(automation_prepare_id_list('7'))->toBe(['placeholders' => '?', 'params' => [7]])
|
||||
->and(automation_prepare_id_list([2, '9']))->toBe(['placeholders' => '?, ?', 'params' => [2, 9]])
|
||||
->and(automation_prepare_id_list('1e3'))->toBeFalse()
|
||||
->and(automation_prepare_id_list('1 OR 1=1'))->toBeFalse()
|
||||
->and(automation_prepare_id_list((string) PHP_INT_MAX))->toBe(['placeholders' => '?', 'params' => [PHP_INT_MAX]])
|
||||
->and(automation_prepare_id_list((string) PHP_INT_MAX . '0'))->toBeFalse()
|
||||
->and(automation_prepare_id_list(str_repeat('9', strlen((string) PHP_INT_MAX) + 1)))->toBeFalse()
|
||||
->and(automation_prepare_id_list(0))->toBeFalse()
|
||||
->and(automation_prepare_id_list(-1))->toBeFalse()
|
||||
->and(automation_prepare_id_list(1.5))->toBeFalse();
|
||||
});
|
||||
|
||||
test('all three host-template filters bind dynamic IN clause values', function () : void {
|
||||
$verdict = cacti_test_isolated_probe(dirname(__DIR__) . '/fixtures/automation_prepared_in_clause_probe.php');
|
||||
$queries = $verdict['queries'];
|
||||
|
||||
expect($queries)->toHaveCount(3)
|
||||
->and($queries[0][0])->toContain('h.host_template_id IN (?, ?)')
|
||||
->and($queries[0][1])->toBe([1, 2])
|
||||
->and($queries[1][0])->toContain('h.host_template_id IN (?)')
|
||||
->and($queries[1][1])->toBe([3])
|
||||
->and($queries[2][0])->toContain('htg.host_template_id IN (?, ?)')
|
||||
->and($queries[2][1])->toBe([4, 5]);
|
||||
});
|
||||
|
||||
test('invalid ID lists never reach the database', function () : void {
|
||||
$verdict = cacti_test_isolated_probe(dirname(__DIR__) . '/fixtures/automation_prepared_in_clause_probe.php');
|
||||
|
||||
expect($verdict['invalid_results'])->toBe([false, false, false])
|
||||
->and($verdict['invalid_query_count'])->toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/*
|
||||
+-------------------------------------------------------------------------+
|
||||
| Copyright (C) 2004-2026 The Cacti Group |
|
||||
| |
|
||||
| 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. |
|
||||
+-------------------------------------------------------------------------+
|
||||
| Cacti: The Complete RRDtool-based Graphing Solution |
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
$queries = [];
|
||||
|
||||
/**
|
||||
* Captures a prepared automation query without requiring a database.
|
||||
*
|
||||
* @param string $sql Prepared SQL text.
|
||||
* @param array $params Bound query parameters.
|
||||
*
|
||||
* @return array Empty result set.
|
||||
*/
|
||||
function db_fetch_assoc_prepared(string $sql, array $params = []) : array {
|
||||
global $queries;
|
||||
|
||||
$queries[] = [$sql, $params];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the size of an array probe value.
|
||||
*
|
||||
* @param mixed $value Value to count.
|
||||
*
|
||||
* @return int Number of elements.
|
||||
*/
|
||||
function cacti_sizeof(mixed $value) : int {
|
||||
return is_array($value) ? count($value) : 0;
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/lib/api_automation_tools.php';
|
||||
|
||||
getHostsByDescription([1, '2']);
|
||||
getHosts(3);
|
||||
getGraphTemplatesByHostTemplate(['4', 5]);
|
||||
|
||||
$valid_queries = $queries;
|
||||
$queries = [];
|
||||
|
||||
$invalid_results = [
|
||||
getHostsByDescription([1, 'bad']),
|
||||
getHosts('1e3'),
|
||||
getGraphTemplatesByHostTemplate('1 OR 1=1'),
|
||||
];
|
||||
|
||||
print json_encode([
|
||||
'queries' => $valid_queries,
|
||||
'invalid_results' => $invalid_results,
|
||||
'invalid_query_count' => count($queries),
|
||||
], JSON_THROW_ON_ERROR);
|
||||
Reference in new issue
Block a user