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