collabSupport/tests/igmpSummarizer.test.js
jmcqueen d112e45eb6 Detect and inline-remediate Meraki IGMP snooping in /phonestatus
DECT basestations rely on multicast for handset discovery/registration.
When Meraki switches have IGMP snooping enabled without a querier —
or per-switch overrides that deviate from a DECT-safe policy — those
frames are pruned and DECT handsets silently fail to register.

- integrations/meraki/switches.js: getSwitchMulticastSettings,
  setSwitchMulticastSettings, and pure summarizeMulticast verdict fn.
- services/phoneService.js: fires the multicast fetch as soon as the
  network id is known, overlapping DECT enrichment; attaches
  data.multicast summary to the collector output. Non-fatal on error.
- services/renderers/phoneStatusRenderer.js: emits a single warning
  line inside the DECT Basestations section only when needsFix is
  true, itemizing which parts deviate (default snoop, default flood,
  N overrides).
- commands/igmpFix.js: frozen DECT_SAFE_MULTICAST_PAYLOAD constant
  ({snoop:false, flood:true, overrides:[]}), adaptive-card builder,
  and confirm/cancel handlers.
- commands/phoneStatus.js: appends the adaptive card when needsFix
  and the trigger came from chat (skipped on HTTP path).
- index.js: IGMP_FIX_ACTIONS set + dispatch branch mirroring the
  HOST_ASSIGN pattern (one-shot pending lookup, censorActionCard,
  domain call).
- utils/pendingIgmpFixes.js: 15-min TTL pending-card store.
- tests: 11 summarizer cases (all deviation permutations + malformed
  input), 4 renderer cases (each warning-line shape + silence when
  needsFix false or no DECT), and a frozen-constant regression guard
  on the PUT payload. Full suite: 47/47 passing.
2026-07-02 14:10:33 -04:00

135 lines
5.3 KiB
JavaScript

// Unit tests for summarizeMulticast — the pure verdict function
// under integrations/meraki/switches.js. No Meraki API needed; the
// summarizer takes a fixture-shaped response and computes flags.
//
// The target end-state (per the plan) is:
// defaultSettings.igmpSnoopingEnabled = false
// defaultSettings.floodUnknownMulticastTrafficEnabled = true
// overrides = [] (no per-switch drift)
//
// `needsFix` is true if any layer deviates from that.
import test from 'node:test';
import assert from 'node:assert/strict';
import { summarizeMulticast } from '../integrations/meraki/switches.js';
test('target state → needsFix: false', () => {
const s = summarizeMulticast({
defaultSettings: { igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: true },
overrides: [],
});
assert.equal(s.needsFix, false);
assert.equal(s.defaultOk, true);
assert.equal(s.defaultSnoopOn, false);
assert.equal(s.defaultFloodOff, false);
assert.equal(s.deviatingOverrides.length, 0);
});
test('snoop on at default → needsFix: true', () => {
const s = summarizeMulticast({
defaultSettings: { igmpSnoopingEnabled: true, floodUnknownMulticastTrafficEnabled: true },
overrides: [],
});
assert.equal(s.needsFix, true);
assert.equal(s.defaultSnoopOn, true);
assert.equal(s.defaultFloodOff, false);
assert.equal(s.deviatingOverrides.length, 0);
});
test('flood off at default → needsFix: true', () => {
const s = summarizeMulticast({
defaultSettings: { igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: false },
overrides: [],
});
assert.equal(s.needsFix, true);
assert.equal(s.defaultSnoopOn, false);
assert.equal(s.defaultFloodOff, true);
});
test('both default settings wrong → both flags true', () => {
const s = summarizeMulticast({
defaultSettings: { igmpSnoopingEnabled: true, floodUnknownMulticastTrafficEnabled: false },
overrides: [],
});
assert.equal(s.needsFix, true);
assert.equal(s.defaultSnoopOn, true);
assert.equal(s.defaultFloodOff, true);
});
test('default correct + one override with snoop=true → needsFix: true', () => {
const s = summarizeMulticast({
defaultSettings: { igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: true },
overrides: [
{ switches: ['Q234-ABCD-0001'], igmpSnoopingEnabled: true, floodUnknownMulticastTrafficEnabled: true },
],
});
assert.equal(s.needsFix, true);
assert.equal(s.defaultOk, true);
assert.equal(s.deviatingOverrides.length, 1);
});
test('default correct + override that matches target exactly → needsFix: false', () => {
// An override that duplicates the default is redundant but harmless.
// We don't nag the operator about it because there's no user-visible
// impact — the switch behaves identically to no override.
const s = summarizeMulticast({
defaultSettings: { igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: true },
overrides: [
{ switches: ['Q234-ABCD-0001'], igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: true },
],
});
assert.equal(s.needsFix, false);
assert.equal(s.deviatingOverrides.length, 0);
});
test('default correct + override with flood=false only → needsFix: true', () => {
const s = summarizeMulticast({
defaultSettings: { igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: true },
overrides: [
{ switches: ['Q234-ABCD-0001'], igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: false },
],
});
assert.equal(s.needsFix, true);
assert.equal(s.deviatingOverrides.length, 1);
});
test('default correct + multiple overrides mixed → deviatingOverrides only counts the bad ones', () => {
const s = summarizeMulticast({
defaultSettings: { igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: true },
overrides: [
{ switches: ['A'], igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: true }, // OK
{ switches: ['B'], igmpSnoopingEnabled: true, floodUnknownMulticastTrafficEnabled: true }, // bad
{ stacks: ['1'], igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: false }, // bad
],
});
assert.equal(s.needsFix, true);
assert.equal(s.deviatingOverrides.length, 2);
});
test('malformed / missing defaultSettings → needsFix: false, raw preserved', () => {
// Renderer stays silent on this state — the caller sets
// data.multicast.error separately, so a broken read doesn't produce
// a bogus "everything is fine" warning line.
const s = summarizeMulticast({});
assert.equal(s.needsFix, false);
assert.equal(s.defaultSnoopOn, false);
assert.equal(s.defaultFloodOff, false);
assert.equal(s.deviatingOverrides.length, 0);
assert.deepEqual(s.raw, {});
});
test('null / undefined input → does not throw, needsFix: false', () => {
assert.doesNotThrow(() => summarizeMulticast(null));
assert.doesNotThrow(() => summarizeMulticast(undefined));
assert.equal(summarizeMulticast(null).needsFix, false);
assert.equal(summarizeMulticast(undefined).needsFix, false);
});
test('overrides field missing entirely → treated as empty', () => {
const s = summarizeMulticast({
defaultSettings: { igmpSnoopingEnabled: false, floodUnknownMulticastTrafficEnabled: true },
});
assert.equal(s.needsFix, false);
assert.equal(s.deviatingOverrides.length, 0);
});