// Unit tests for integrations/cisco-dect/statusXml.js. // // Pure — no network, no fs. The fixture below is a REDACTED copy of a // real /admin/status.xml pulled from a lab DBS-210 during the DECT // spike. MAC, IP, RFPI, and firmware server URL are all changed to // obviously-fake values so this file is safe to commit and safe to // leave in CI logs. The XML SHAPE (tag nesting, whitespace, encoding // oddities like `text/text` mimetype on the wire) is preserved // verbatim — that's exactly what the parser has to be robust against. import test from 'node:test'; import assert from 'node:assert/strict'; import { xmlToObject, parseStatusXml, parseRebootLine, summarizeBaseHealth, } from '../integrations/cisco-dect/statusXml.js'; const FIXTURE = ` Yes Unchained(TXT_STATE_UNCHAINED) Allowed to Join as Secondary IPDECT-V2 (DBS-210-3PC) Generic SIP (RFC 3261) SME VoIP Base Idx:0 US No Conflict 02-Jul-2026 13:22:48 00:10:20 (H:M:S) ABCDEF12; RPN:00 001122334455 10.0.0.100 0000 IPDECT-V2/05-01-03-0101-09/18-Mar-2026 10:29 https://example.invalid dms/dbS210 2026-07-02 13:11:46 (164) Normal Reboot (21) Firmware Version 05-01-03-0101-09 2026-07-02 13:09:50 (163) Normal Reboot (21) Firmware Version 05-01-03-0101-09 2026-07-02 13:06:28 (162) Normal Reboot (21) Firmware Version 05-01-03-0101-09 2026-07-02 12:54:12 (161) Power Loss (80) Firmware Version 05-01-03-0101-09 2026-07-02 12:49:50 (160) Normal Reboot (21) Firmware Version 05-01-03-0101-09 2026-07-02 12:48:40 (159) Normal Reboot (21) Firmware Version 05-01-03-0101-09 Idle N/A Not Installed Unavailable N/A 2 -1 49710 days 06:28:15 [H:M:S] 0 0 0 0 0 Base type:DBS-210-3PC - Required Version:501 Required Branch:309 Device type:6825 - Required Version:501 Required Branch:308 Language Pack:6825_default Device type:6825-RGD - Required Version:501 Required Branch:308 Language Pack:6825-RGD_default Device type:6823 - Required Version:501 Required Branch:308 Language Pack:6823_default Device type:RPT-110-3PC - Required Version:501 Required Branch:303 Off 911 1911 933 No Number set! No Number set! 1959 0 0 0 0 49318 0 9 0 5834 RPN, MAC-Addr, OP[s], DT[s] `; // ─── Low-level parser ─────────────────────────────────────────────── test('xmlToObject: parses the DBS-210 status.xml shape into a nested object', () => { const tree = xmlToObject(FIXTURE); assert.ok(tree.Status, 'root element is Status'); assert.equal(tree.Status.System_Information.Phone_Type, 'IPDECT-V2 (DBS-210-3PC)'); assert.equal(tree.Status.System_Information.MAC_Address, '001122334455'); // Nested Firmware_URL is a child object, not a string. assert.equal(typeof tree.Status.System_Information.Firmware_URL, 'object'); assert.equal(tree.Status.System_Information.Firmware_URL.Path, 'dms/dbS210'); // Empty \n becomes an empty leaf string — // XML alone can't distinguish empty-container from empty-leaf, so // the parser stays neutral. parseStatusXml() then normalizes both // shapes into `[]` for callers that expect a container. assert.equal(tree.Status.System_Information.RSSI_List, ''); // Statistics has the massive CSV header preserved as a single string. assert.match(tree.Status.Statistics.Header_Line_Idx, /RPN, MAC-Addr/); }); test('xmlToObject: preserves whitespace inside leaf text values', () => { const tree = xmlToObject(FIXTURE); // RFPI has a `; ` separator that must survive intact. assert.equal(tree.Status.System_Information.RFPI_Address, 'ABCDEF12; RPN:00'); }); test('xmlToObject: throws on empty input', () => { assert.throws(() => xmlToObject(''), /empty input/); assert.throws(() => xmlToObject(''), /empty input/); }); test('xmlToObject: throws on non-string input', () => { assert.throws(() => xmlToObject(null), /expected a string/); assert.throws(() => xmlToObject(123), /expected a string/); }); // ─── Reboot line parser ───────────────────────────────────────────── test('parseRebootLine: normal reboot line', () => { const p = parseRebootLine('2026-07-02 13:11:46 (164) Normal Reboot (21) Firmware Version 05-01-03-0101-09'); assert.equal(p.at, '2026-07-02T13:11:46'); assert.equal(p.sequence, 164); assert.equal(p.reasonName, 'Normal Reboot'); assert.equal(p.reasonCode, 21); assert.equal(p.firmwareAtBoot, '05-01-03-0101-09'); }); test('parseRebootLine: power loss line', () => { const p = parseRebootLine('2026-07-02 12:54:12 (161) Power Loss (80) Firmware Version 05-01-03-0101-09'); assert.equal(p.reasonName, 'Power Loss'); assert.equal(p.reasonCode, 80); }); test('parseRebootLine: marks unrecognized shapes without dropping them', () => { const p = parseRebootLine('Something totally different'); assert.equal(p.unrecognized, true); assert.equal(p.raw, 'Something totally different'); }); test('parseRebootLine: empty/null input', () => { assert.equal(parseRebootLine(''), null); assert.equal(parseRebootLine(' '), null); assert.equal(parseRebootLine(null), null); assert.equal(parseRebootLine(undefined), null); }); // ─── High-level parseStatusXml ────────────────────────────────────── test('parseStatusXml: extracts core device identity', () => { const s = parseStatusXml(FIXTURE); assert.equal(s.device.model, 'IPDECT-V2 (DBS-210-3PC)'); assert.equal(s.device.macAddress, '00:11:22:33:44:55'); // normalized colon-form assert.equal(s.device.ipAddress, '10.0.0.100'); assert.equal(s.device.rfBand, 'US'); assert.equal(s.device.rfpiAddress, 'ABCDEF12; RPN:00'); assert.equal(s.device.releasedBuild, true); }); test('parseStatusXml: firmware version and required-per-device map', () => { const s = parseStatusXml(FIXTURE); assert.equal(s.firmware.version, 'IPDECT-V2/05-01-03-0101-09/18-Mar-2026 10:29'); assert.equal(s.firmware.updateServer, 'https://example.invalid'); assert.equal(s.firmware.updatePath, 'dms/dbS210'); // Device_Base_Station → keyed by model assert.deepEqual(s.firmware.requiredFor['DBS-210-3PC'], { requiredVersion: '501', requiredBranch: '309', languagePack: null, _sourceKey: 'Device_Base_Station', }); // Handset lines carry language pack too assert.equal(s.firmware.requiredFor['6825'].languagePack, '6825_default'); }); test('parseStatusXml: operating time gets converted to seconds', () => { const s = parseStatusXml(FIXTURE); assert.equal(s.time.operatingTime, '00:10:20 (H:M:S)'); assert.equal(s.time.operatingTimeSeconds, 620); // 10m 20s }); test('parseStatusXml: multi-cell role parsed to a normalized token', () => { const s = parseStatusXml(FIXTURE); assert.equal(s.multiCell.role, 'unchained'); assert.match(s.multiCell.raw, /TXT_STATE_UNCHAINED/); }); test('parseStatusXml: reboot log is 6 sorted entries with structured fields', () => { const s = parseStatusXml(FIXTURE); assert.equal(s.rebootLog.length, 6); // First entry = the newest by sequence#, matching physical order in the XML. assert.equal(s.rebootLog[0].sequence, 164); assert.equal(s.rebootLog[0].reasonName, 'Normal Reboot'); // The Power Loss event is entry #4 (sequence 161). const powerLoss = s.rebootLog.find((r) => r.reasonCode === 80); assert.ok(powerLoss); assert.equal(powerLoss.reasonName, 'Power Loss'); assert.equal(powerLoss.sequence, 161); }); test('parseStatusXml: network stats decoded as numbers', () => { const s = parseStatusXml(FIXTURE); assert.equal(s.network.txPackets, 1959); assert.equal(s.network.rxPackets, 49318); assert.equal(s.network.rxDropped, 9); assert.equal(s.network.rxErrors, 0); }); test('parseStatusXml: emergency numbers filter out "No Number set" placeholders', () => { const s = parseStatusXml(FIXTURE); assert.deepEqual(s.emergencyNumbers, ['911', '1911', '933']); }); test('parseStatusXml: RTP usage counters', () => { const s = parseStatusXml(FIXTURE); assert.equal(s.rtp.total, 2); assert.equal(s.rtp.current, 0); assert.equal(s.rtp.max, -1); }); test('parseStatusXml: security / dot1x / customCA', () => { const s = parseStatusXml(FIXTURE); assert.equal(s.security.customCa.installed, false); assert.equal(s.security.customCa.info, 'Not Installed'); assert.equal(s.security.dot1x.transactionStatus, 'Unavailable'); }); test('parseStatusXml: pushToTalk feature flag', () => { const s = parseStatusXml(FIXTURE); assert.equal(s.features.pushToTalk, false); }); // ─── Health verdict ───────────────────────────────────────────────── test('summarizeBaseHealth: fixture flags power-loss reboot and short uptime', () => { const s = parseStatusXml(FIXTURE); const verdict = summarizeBaseHealth(s); assert.equal(verdict.healthy, false); // Uptime 620s (~10 min) is < 600 → boundary, so no uptime warning. // But we DO have a power loss in the log, so healthy=false via that. assert.ok( verdict.warnings.some((w) => /power-loss/i.test(w)), `expected power-loss warning; got: ${JSON.stringify(verdict.warnings)}`, ); // rxDropped=9 → info, not warning assert.ok( verdict.info.some((i) => /rx dropped/i.test(i)), `expected rx-dropped info; got: ${JSON.stringify(verdict.info)}`, ); }); test('summarizeBaseHealth: RF conflict is flagged as a warning', () => { const s = parseStatusXml(FIXTURE.replace( 'No Conflict', 'Conflict Detected on RPN 00', )); const verdict = summarizeBaseHealth(s); assert.ok(verdict.warnings.some((w) => /RF conflict/i.test(w))); }); test('summarizeBaseHealth: very short uptime is flagged as recent reboot', () => { const s = parseStatusXml(FIXTURE.replace( '00:10:20 (H:M:S)', '00:02:15 (H:M:S)', )); const verdict = summarizeBaseHealth(s); assert.ok(verdict.warnings.some((w) => /rebooted very recently/i.test(w))); }); test('summarizeBaseHealth: healthy base with clean stats returns healthy:true', () => { // Wipe rx_dropped and the power-loss reboot line so nothing warns. const cleaned = FIXTURE .replace(/\d+<\/Rx_Dropped>/, '0') .replace( /[^<]+<\/Reboot_Line_4>/, '2026-07-02 12:54:12 (161) Normal Reboot (21) Firmware Version 05-01-03-0101-09', ) .replace( '00:10:20 (H:M:S)', '24:15:00 (H:M:S)', ); const s = parseStatusXml(cleaned); const verdict = summarizeBaseHealth(s); assert.equal(verdict.healthy, true, `not healthy: ${JSON.stringify(verdict.warnings)}`); assert.equal(verdict.warnings.length, 0); }); test('summarizeBaseHealth: bad input degrades gracefully', () => { assert.equal(summarizeBaseHealth(null).healthy, false); assert.equal(summarizeBaseHealth(undefined).healthy, false); assert.equal(summarizeBaseHealth('nope').healthy, false); });