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.
135 lines
5.3 KiB
JavaScript
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);
|
|
});
|