Files
cacti/tests/HandOff/OidStripHandOffTest.php

209 lines
8.4 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* Hand-off tests for the OID trailing-zero stripping pipeline (issue #6108,
* PR #7032). Unit tests already cover detection and stripping in isolation.
* These tests exercise the *full* pipeline end-to-end:
*
* raw $snmp_indexes -> oid_index_should_strip_trailing_zero_padding()
* -> oid_index_strip_trailing_zero_padding()
* -> downstream consumer (parse last-octet via regex)
*
* The point is to prove that the reason this PR exists -- meaningful index
* resolution after stripping -- actually flows through the contract between
* producer (query_snmp_host walk) and consumer (oid_index_parse_regexp).
*/
if (strpos((string) @file_get_contents(dirname(__DIR__, 2) . '/lib/data_query.php'),
'oid_index_should_strip_trailing_zero_padding') === false) {
test('OID strip hand-off: feature not present on this branch', function () {})
->skip('oid_index_should_strip_trailing_zero_padding absent — feature PR #7032 not merged into develop yet');
return;
}
beforeAll(function () {
require_once dirname(__DIR__, 2) . '/lib/functions.php';
require_once dirname(__DIR__, 2) . '/lib/data_query.php';
});
describe('OID strip hand-off pipeline', function () {
$defaultRegex = '/.*\.([0-9]+)$/';
/*
* Stand-in for the downstream consumer. query_snmp_host hands the
* stripped indexes to oid_index_parse_regexp(), which applies the
* value_regex from the data query XML against each OID and returns
* the captured group as the index value. The capture is the same
* regex shape used by the detection helper, so the inputs the
* consumer sees are exactly the stripped keys.
*/
$parseLastOctet = function (array $oids, string $regex): array {
$indexes = [];
foreach (array_keys($oids) as $oid) {
if (preg_match($regex, $oid, $matches)) {
$indexes[$oid] = $matches[1];
}
}
return $indexes;
};
describe('full pipeline: detect -> strip -> consume', function () use ($defaultRegex, $parseLastOctet) {
it('Netscreen multi-zero-padded walk resolves to meaningful indexes', function () use ($defaultRegex, $parseLastOctet) {
// Realistic Netscreen zoneTable walk: ASCII-encoded zone names
// followed by three trailing .0 padding octets.
$snmp_indexes = [
'.1.3.6.1.4.1.2636.3.39.1.8.1.1.1.1.1.84.114.117.115.116.0.0.0' => 'Trust',
'.1.3.6.1.4.1.2636.3.39.1.8.1.1.1.1.1.85.110.116.114.117.115.117.0.0.0' => 'Untrust',
'.1.3.6.1.4.1.2636.3.39.1.8.1.1.1.1.1.68.77.90.0.0.0' => 'DMZ',
];
// Stage 1: detection gate must fire for this input.
expect(oid_index_should_strip_trailing_zero_padding($snmp_indexes, $defaultRegex))->toBeTrue();
// Stage 2: strip helper produces keys with no trailing .0.
$stripped = oid_index_strip_trailing_zero_padding($snmp_indexes);
foreach (array_keys($stripped) as $oid) {
expect($oid)->not->toEndWith('.0');
}
// Values must be preserved 1:1 in the original order.
expect(array_values($stripped))->toBe(['Trust', 'Untrust', 'DMZ']);
// Row count must be preserved -- a silent merge would drop rows.
expect($stripped)->toHaveCount(count($snmp_indexes));
// Stage 3: consumer applies the value_regex to the stripped keys.
$indexes = $parseLastOctet($stripped, $defaultRegex);
// Trust -> last ASCII octet 116 ('t')
// Untrust -> last ASCII octet 117 ('u')
// DMZ -> last ASCII octet 90 ('Z')
expect(array_values($indexes))->toBe(['116', '117', '90']);
// The whole reason the PR exists: indexes are no longer all '0'.
expect(array_unique(array_values($indexes)))->toHaveCount(3);
});
});
describe('collision detection blocks the pipeline', function () use ($defaultRegex, $parseLastOctet) {
it('returns the original array unchanged when stripped keys would collide', function () use ($defaultRegex, $parseLastOctet) {
// Both OIDs strip to '.1.2.3' -- a silent merge would lose a row.
$snmp_indexes = [
'.1.2.3.0' => 'first',
'.1.2.3.0.0' => 'second',
];
// Detection helper must reject the collision.
expect(oid_index_should_strip_trailing_zero_padding($snmp_indexes, $defaultRegex))->toBeFalse();
// Strip helper, called directly, must also be identity here.
$stripped = oid_index_strip_trailing_zero_padding($snmp_indexes);
expect($stripped)->toBe($snmp_indexes)
->and($stripped)->toHaveCount(count($snmp_indexes));
// Consumer still parses the unmodified keys -- both end in .0,
// so this is the legacy (broken) behaviour, but no row is lost.
$indexes = $parseLastOctet($stripped, $defaultRegex);
expect(array_keys($indexes))->toBe(array_keys($snmp_indexes));
});
});
describe('single-.0 boundary: gated on unique stripped indexes', function () use ($defaultRegex, $parseLastOctet) {
it('detection returns true when single-.0 padding resolves to unique indexes', function () use ($defaultRegex, $parseLastOctet) {
// Every OID ends in exactly one .0, so the default regex captures
// '0' for every row -- all three rows collapse onto one index.
// That collapse is the bug, and a single pad octet is enough to
// cause it, so the gate must repair this walk too.
$snmp_indexes = [
'.1.3.6.1.2.1.2.2.1.1.1.0' => 'eth0',
'.1.3.6.1.2.1.2.2.1.1.2.0' => 'eth1',
'.1.3.6.1.2.1.2.2.1.1.3.0' => 'lo0',
];
expect(oid_index_should_strip_trailing_zero_padding($snmp_indexes, $defaultRegex))->toBeTrue();
$stripped = oid_index_strip_trailing_zero_padding($snmp_indexes);
// No row may be merged or dropped by the rewrite.
expect($stripped)->toHaveCount(count($snmp_indexes))
->and(array_values($stripped))->toBe(['eth0', 'eth1', 'lo0']);
$indexes = $parseLastOctet($stripped, $defaultRegex);
expect(array_values($indexes))->toBe(['1', '2', '3'])
->and(array_unique(array_values($indexes)))->toHaveCount(3);
});
it('detection returns false when single-.0 stripping leaves duplicate indexes', function () use ($defaultRegex) {
// Stripping produces two distinct keys, so the helper's own
// collision guard does not fire, but both stripped OIDs parse to
// index '5'. Rewriting here would cross-wire two rows onto one
// data query index, so detection has to refuse.
$snmp_indexes = [
'.1.2.3.5.0' => 'first',
'.1.9.9.5.0' => 'second',
];
expect(oid_index_strip_trailing_zero_padding($snmp_indexes))->not->toBe($snmp_indexes);
expect(oid_index_should_strip_trailing_zero_padding($snmp_indexes, $defaultRegex))->toBeFalse();
});
it('strip helper, called directly with single-.0 input, strips and keeps every row', function () {
$snmp_indexes = [
'.1.3.6.1.2.1.2.2.1.1.1.0' => 'eth0',
'.1.3.6.1.2.1.2.2.1.1.2.0' => 'eth1',
];
$stripped = oid_index_strip_trailing_zero_padding($snmp_indexes);
expect($stripped)->toBe([
'.1.3.6.1.2.1.2.2.1.1.1' => 'eth0',
'.1.3.6.1.2.1.2.2.1.1.2' => 'eth1',
]);
});
});
describe('consumer hand-off: stripped keys parse to meaningful indexes', function () use ($defaultRegex, $parseLastOctet) {
it('downstream regex parse against stripped keys yields non-zero last octets', function () use ($defaultRegex, $parseLastOctet) {
$snmp_indexes = [
'.1.3.6.1.4.1.2636.3.39.1.8.1.1.1.1.1.84.114.117.115.116.0.0.0' => 'Trust',
'.1.3.6.1.4.1.2636.3.39.1.8.1.1.1.1.1.85.110.116.114.117.115.117.0.0.0' => 'Untrust',
];
// Pre-strip: every captured index is '0' -- the bug.
$preStrip = $parseLastOctet($snmp_indexes, $defaultRegex);
expect(array_unique(array_values($preStrip)))->toBe(['0']);
// Run the gated pipeline.
expect(oid_index_should_strip_trailing_zero_padding($snmp_indexes, $defaultRegex))->toBeTrue();
$stripped = oid_index_strip_trailing_zero_padding($snmp_indexes);
// Post-strip: captured indexes are meaningful and unique.
$postStrip = $parseLastOctet($stripped, $defaultRegex);
expect(array_values($postStrip))->toBe(['116', '117'])
->and(array_unique(array_values($postStrip)))->toHaveCount(2);
// And, critically, none of them is '0'.
foreach ($postStrip as $oid => $index) {
expect($index)->not->toBe('0');
}
});
});
});