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