// Unit tests for services/voiceDiag/checks/*. Every check gets a // happy-path, an actionable-finding path, and a scope-missing path // (403 or 404 surfaced by the runner as `status: 'skipped'`). // // Remediation functions are exercised in tests/voiceDiag.remediations.test.js // where we can stub the WebexClient import. This file only covers the // `run(ctx)` shape — ctx.webex.request is trivially stubbed. import test from 'node:test'; import assert from 'node:assert/strict'; import { dndCheck } from '../services/voiceDiag/checks/dnd.js'; import { callForwardingCheck } from '../services/voiceDiag/checks/callForwarding.js'; import { callWaitingCheck } from '../services/voiceDiag/checks/callWaiting.js'; import { voicemailCheck } from '../services/voiceDiag/checks/voicemail.js'; import { callInterceptCheck } from '../services/voiceDiag/checks/callIntercept.js'; import { hotelingCheck } from '../services/voiceDiag/checks/hoteling.js'; import { executiveAssistantCheck } from '../services/voiceDiag/checks/executiveAssistant.js'; import { outgoingPermissionCheck } from '../services/voiceDiag/checks/outgoingPermission.js'; import { phoneOnlineCheck } from '../services/voiceDiag/checks/phoneOnline.js'; import { runVoiceDiag, buildRemediationRegistry, } from '../services/voiceDiag/voiceDiagService.js'; import { CHECKS } from '../services/voiceDiag/checks/index.js'; // ─── Helpers ──────────────────────────────────────────────────────── /** Build a stub context with a webex.request that returns whatever * callback returns for its (method, endpoint) signature. */ function mkCtx({ response, error } = {}) { return { storeNum: '12345', email: 'ae12345@ae.com', personId: 'PID_TEST', personLabel: 'Store 12345', telephonyProfile: {}, phoneStatus: null, person: null, webex: { async request(_method, _endpoint) { if (error) throw error; return response; }, }, }; } /** Build an http error that mirrors what axios throws. */ function httpError(status, message = 'Boom') { const err = new Error(message); err.response = { status, data: { message } }; return err; } // ─── dnd ──────────────────────────────────────────────────────────── test('dnd: disabled → ok', async () => { const ctx = mkCtx({ response: { enabled: false, ringSplashEnabled: false } }); const r = await dndCheck.run(ctx); assert.equal(r.status, 'ok'); assert.equal(r.remediation, null); }); test('dnd: enabled → warn + disable_dnd remediation', async () => { const ctx = mkCtx({ response: { enabled: true, ringSplashEnabled: true } }); const r = await dndCheck.run(ctx); assert.equal(r.status, 'warn'); assert.equal(r.remediation.action, 'disable_dnd'); assert.equal(r.remediation.payload.personId, 'PID_TEST'); assert.equal(r.remediation.payload.before.enabled, true); }); test('dnd: 403 flows through runner as skipped with scope hint', async () => { const ctx = mkCtx({ error: httpError(403) }); const results = await runVoiceDiag(ctx, { only: ['dnd'] }); assert.equal(results.length, 1); assert.equal(results[0].status, 'skipped'); assert.match(results[0].message, /missing/); assert.match(results[0].message, /spark-admin:people_read/); }); test('dnd: 404 flows through runner as skipped ("not applicable" wording)', async () => { const ctx = mkCtx({ error: httpError(404) }); const results = await runVoiceDiag(ctx, { only: ['dnd'] }); assert.equal(results[0].status, 'skipped'); assert.match(results[0].message, /Webex Calling license|feature isn't applicable/); }); test('dnd: 404 with "no static resource" body → skipped with routing-miss diagnostic', async () => { const ctx = mkCtx({ error: httpError(404, 'No static resource hydra/v1/people/.../features/doNotDisturb.'), }); const results = await runVoiceDiag(ctx, { only: ['dnd'] }); assert.equal(results[0].status, 'skipped'); assert.match(results[0].message, /URL for this feature has moved/); assert.equal(results[0].details.routingMiss, true); }); // ─── callForwarding ───────────────────────────────────────────────── test('callForwarding: nothing enabled → ok', async () => { const ctx = mkCtx({ response: { callForwarding: { always: { enabled: false }, busy: { enabled: false }, noAnswer: { enabled: false }, }, }, }); const r = await callForwardingCheck.run(ctx); assert.equal(r.status, 'ok'); assert.equal(r.remediation, null); }); test('callForwarding: always-forwarding enabled → error + compound remediation', async () => { // Severity upgraded from warn → error per store-line standard // (any forwarding active on a store line = missed customer calls). const ctx = mkCtx({ response: { callForwarding: { always: { enabled: true, destination: '+18005551212' }, busy: { enabled: false }, noAnswer: { enabled: false }, }, }, }); const r = await callForwardingCheck.run(ctx); assert.equal(r.status, 'error'); assert.equal(r.remediation.action, 'clear_call_forwarding'); assert.deepEqual(r.remediation.payload.variantsToClear, ['always']); assert.match(r.message, /\+18005551212/); assert.match(r.message, /Store phone standard/); }); test('callForwarding: multiple variants active → single compound remediation', async () => { const ctx = mkCtx({ response: { callForwarding: { always: { enabled: true, destination: '+15551111111' }, busy: { enabled: true, destination: '+15552222222' }, noAnswer: { enabled: false }, }, }, }); const r = await callForwardingCheck.run(ctx); assert.equal(r.status, 'error'); assert.deepEqual(r.remediation.payload.variantsToClear, ['always', 'busy']); assert.match(r.remediation.title, /2 forwarding variants/); }); test('callForwarding: 403 → skipped via runner', async () => { const ctx = mkCtx({ error: httpError(403) }); const results = await runVoiceDiag(ctx, { only: ['callForwarding'] }); assert.equal(results[0].status, 'skipped'); }); // ─── callWaiting ──────────────────────────────────────────────────── test('callWaiting: enabled → ok', async () => { const ctx = mkCtx({ response: { enabled: true } }); const r = await callWaitingCheck.run(ctx); assert.equal(r.status, 'ok'); }); test('callWaiting: disabled → warn + enable_call_waiting remediation', async () => { const ctx = mkCtx({ response: { enabled: false } }); const r = await callWaitingCheck.run(ctx); assert.equal(r.status, 'warn'); assert.equal(r.remediation.action, 'enable_call_waiting'); }); test('callWaiting: 404 → skipped via runner', async () => { const ctx = mkCtx({ error: httpError(404) }); const results = await runVoiceDiag(ctx, { only: ['callWaiting'] }); assert.equal(results[0].status, 'skipped'); }); // ─── voicemail ────────────────────────────────────────────────────── test('voicemail: disabled → warn (callers can\'t leave messages)', async () => { // Standard is enabled=true; disabled surfaces as warn, no // auto-remediation (some sites intentionally disable it). const ctx = mkCtx({ response: { enabled: false } }); const r = await voicemailCheck.run(ctx); assert.equal(r.status, 'warn'); assert.equal(r.remediation, null); assert.match(r.message, /Voicemail is disabled/); }); test('voicemail: sendAllCalls on → error + stop_sending_to_voicemail remediation', async () => { const ctx = mkCtx({ response: { enabled: true, sendAllCalls: { enabled: true }, sendBusyCalls: { enabled: false }, sendUnansweredCalls: { enabled: false }, messageStorage: { mwiEnabled: true }, }, }); const r = await voicemailCheck.run(ctx); assert.equal(r.status, 'error'); assert.equal(r.remediation.action, 'stop_sending_to_voicemail'); assert.deepEqual(r.remediation.payload.activeSendTriggers, ['sendAllCalls']); assert.equal(r.remediation.payload.before.sendAllCallsEnabled, true); assert.match(r.message, /Voicemail is receiving calls/); }); test('voicemail: sendBusyCalls + sendUnansweredCalls both on → error, all triggers listed', async () => { const ctx = mkCtx({ response: { enabled: true, sendAllCalls: { enabled: false }, sendBusyCalls: { enabled: true }, sendUnansweredCalls: { enabled: true }, messageStorage: { mwiEnabled: true }, }, }); const r = await voicemailCheck.run(ctx); assert.equal(r.status, 'error'); assert.deepEqual( r.remediation.payload.activeSendTriggers.sort(), ['sendBusyCalls', 'sendUnansweredCalls'], ); assert.match(r.message, /2 triggers are active|triggers are active/); }); test('voicemail: enabled but MWI off → warn (no remediation, still soft signal)', async () => { const ctx = mkCtx({ response: { enabled: true, sendAllCalls: { enabled: false }, sendBusyCalls: { enabled: false }, sendUnansweredCalls: { enabled: false }, messageStorage: { mwiEnabled: false }, }, }); const r = await voicemailCheck.run(ctx); assert.equal(r.status, 'warn'); assert.match(r.message, /MWI/); }); test('voicemail: enabled, email forward to off-org → warn', async () => { const ctx = mkCtx({ response: { enabled: true, sendAllCalls: { enabled: false }, sendBusyCalls: { enabled: false }, sendUnansweredCalls: { enabled: false }, messageStorage: { mwiEnabled: true }, emailCopyOfMessage: { enabled: true, emailId: 'random@gmail.com' }, }, }); const r = await voicemailCheck.run(ctx); assert.equal(r.status, 'warn'); assert.match(r.message, /off-org/); }); test('voicemail: enabled + MWI + no send-to-VM + no unusual forwarding → ok', async () => { const ctx = mkCtx({ response: { enabled: true, sendAllCalls: { enabled: false }, sendBusyCalls: { enabled: false }, sendUnansweredCalls: { enabled: false }, messageStorage: { mwiEnabled: true }, emailCopyOfMessage: { enabled: false, emailId: '' }, }, }); const r = await voicemailCheck.run(ctx); assert.equal(r.status, 'ok'); }); test('voicemail: 401 → skipped via runner', async () => { const ctx = mkCtx({ error: httpError(401) }); const results = await runVoiceDiag(ctx, { only: ['voicemail'] }); assert.equal(results[0].status, 'skipped'); }); // ─── callIntercept ────────────────────────────────────────────────── test('callIntercept: disabled → ok', async () => { const ctx = mkCtx({ response: { enabled: false } }); const r = await callInterceptCheck.run(ctx); assert.equal(r.status, 'ok'); }); test('callIntercept: enabled → error + disable_call_intercept remediation', async () => { const ctx = mkCtx({ response: { enabled: true, incoming: { type: 'INTERCEPT_ALL' }, outgoing: { type: 'ALLOW_ALL' }, }, }); const r = await callInterceptCheck.run(ctx); assert.equal(r.status, 'error'); assert.equal(r.remediation.action, 'disable_call_intercept'); }); test('callIntercept: 403 → skipped via runner', async () => { const ctx = mkCtx({ error: httpError(403) }); const results = await runVoiceDiag(ctx, { only: ['callIntercept'] }); assert.equal(results[0].status, 'skipped'); }); // ─── hoteling (info-only) ─────────────────────────────────────────── test('hoteling: disabled → ok', async () => { const ctx = mkCtx({ response: { enabled: false } }); const r = await hotelingCheck.run(ctx); assert.equal(r.status, 'ok'); assert.equal(r.remediation, null); }); test('hoteling: enabled → warn + disable_hoteling remediation', async () => { const ctx = mkCtx({ response: { enabled: true } }); const r = await hotelingCheck.run(ctx); assert.equal(r.status, 'warn'); assert.equal(r.remediation.action, 'disable_hoteling'); assert.equal(r.remediation.payload.personId, 'PID_TEST'); assert.equal(r.remediation.payload.before.enabled, true); }); test('hoteling: 404 → skipped via runner', async () => { const ctx = mkCtx({ error: httpError(404) }); const results = await runVoiceDiag(ctx, { only: ['hoteling'] }); assert.equal(results[0].status, 'skipped'); }); // ─── executiveAssistant (info-only) ───────────────────────────────── test('executiveAssistant: UNASSIGNED → ok', async () => { const ctx = mkCtx({ response: { type: 'UNASSIGNED' } }); const r = await executiveAssistantCheck.run(ctx); assert.equal(r.status, 'ok'); }); test('executiveAssistant: EXECUTIVE_ASSISTANT → warn (no remediation)', async () => { const ctx = mkCtx({ response: { type: 'EXECUTIVE_ASSISTANT' } }); const r = await executiveAssistantCheck.run(ctx); assert.equal(r.status, 'warn'); assert.equal(r.remediation, null); assert.match(r.message, /EXECUTIVE_ASSISTANT/); }); test('executiveAssistant: 403 → skipped via runner', async () => { const ctx = mkCtx({ error: httpError(403) }); const results = await runVoiceDiag(ctx, { only: ['executiveAssistant'] }); assert.equal(results[0].status, 'skipped'); }); // ─── outgoingPermission (info-only) ───────────────────────────────── test('outgoingPermission: useCustomEnabled=false → ok (defers to location)', async () => { const ctx = mkCtx({ response: { useCustomEnabled: false, callingPermissions: [] }, }); const r = await outgoingPermissionCheck.run(ctx); assert.equal(r.status, 'ok'); assert.match(r.message, /location default/); }); test('outgoingPermission: NATIONAL BLOCK → warn', async () => { const ctx = mkCtx({ response: { useCustomEnabled: true, callingPermissions: [ { callType: 'NATIONAL', action: 'BLOCK' }, { callType: 'INTERNAL_CALL', action: 'ALLOW' }, ], }, }); const r = await outgoingPermissionCheck.run(ctx); assert.equal(r.status, 'warn'); assert.deepEqual(r.details.highImpactBlocked, ['NATIONAL']); }); test('outgoingPermission: only premium blocked → ok', async () => { const ctx = mkCtx({ response: { useCustomEnabled: true, callingPermissions: [ { callType: 'PREMIUM_SERVICES_I', action: 'BLOCK' }, ], }, }); const r = await outgoingPermissionCheck.run(ctx); assert.equal(r.status, 'ok'); assert.deepEqual(r.details.highImpactBlocked, []); }); test('outgoingPermission: 404 → skipped via runner', async () => { const ctx = mkCtx({ error: httpError(404) }); const results = await runVoiceDiag(ctx, { only: ['outgoingPermission'] }); assert.equal(results[0].status, 'skipped'); }); // ─── phoneOnline (no API — reads phoneStatus snapshot) ────────────── test('phoneOnline: no phoneStatus → skipped by runner (requires guard)', async () => { const ctx = { ...mkCtx(), phoneStatus: null }; const results = await runVoiceDiag(ctx, { only: ['phoneOnline'] }); assert.equal(results[0].status, 'skipped'); assert.match(results[0].message, /missing phoneStatus/); }); test('phoneOnline: no devices → warn', async () => { const ctx = { ...mkCtx(), phoneStatus: { phones: { data: [] }, dectBasestations: [] }, }; const r = await phoneOnlineCheck.run(ctx); assert.equal(r.status, 'warn'); assert.match(r.message, /No desk phones/); }); test('phoneOnline: all connected → ok', async () => { const ctx = { ...mkCtx(), phoneStatus: { phones: { data: [{ mac: 'aa:bb', status: 'connected', name: 'A' }] }, dectBasestations: [{ mac: 'cc:dd', status: 'connected', name: 'DB' }], }, }; const r = await phoneOnlineCheck.run(ctx); assert.equal(r.status, 'ok'); }); test('phoneOnline: one offline phone → error, offlineDevices populated', async () => { const ctx = { ...mkCtx(), phoneStatus: { phones: { data: [ { mac: 'aa:bb', status: 'connected', name: 'A' }, { mac: 'ee:ff', status: 'disconnected', name: 'B', lastSeen: 'now' }, ] }, dectBasestations: [], }, }; const r = await phoneOnlineCheck.run(ctx); assert.equal(r.status, 'error'); assert.equal(r.details.offlineDevices.length, 1); assert.equal(r.details.offlineDevices[0].name, 'B'); }); // ─── runner-level integration ─────────────────────────────────────── test('runVoiceDiag: missing personId → every personId-requiring check is skipped', async () => { const ctx = mkCtx(); ctx.personId = null; const results = await runVoiceDiag(ctx); const personRequired = results.filter((r) => ['dnd', 'callForwarding', 'callWaiting', 'voicemail', 'callIntercept', 'hoteling', 'executiveAssistant', 'outgoingPermission'].includes(r.id), ); for (const r of personRequired) { assert.equal(r.status, 'skipped', `expected ${r.id} skipped, got ${r.status}`); } }); test('runVoiceDiag: --only filter honours case-insensitive names', async () => { const ctx = mkCtx({ response: { enabled: false } }); const results = await runVoiceDiag(ctx, { only: ['DND'] }); assert.equal(results.length, 1); assert.equal(results[0].id, 'dnd'); }); test('runVoiceDiag: check runner isolates thrown errors as error status', async () => { const ctx = mkCtx({ error: new Error('random blowup') }); const results = await runVoiceDiag(ctx, { only: ['dnd'] }); assert.equal(results[0].status, 'error'); assert.match(results[0].message, /random blowup/); }); test('runVoiceDiag: registry order preserved in results', async () => { const ctx = mkCtx({ response: { enabled: false } }); ctx.phoneStatus = { phones: { data: [] }, dectBasestations: [] }; const results = await runVoiceDiag(ctx); const orderedIds = CHECKS.map((c) => c.id); assert.deepEqual(results.map((r) => r.id), orderedIds); }); // ─── command-arg normalization ───────────────────────────────────── test('normalizeArg: em-dash converted to double-hyphen', async () => { const { normalizeArg } = await import('../commands/voiceDiag.js'); assert.equal(normalizeArg('\u2014detailed'), '--detailed'); assert.equal(normalizeArg('\u2014only'), '--only'); }); test('normalizeArg: en-dash and hyphen-like Unicode range all collapse to --', async () => { const { normalizeArg } = await import('../commands/voiceDiag.js'); assert.equal(normalizeArg('\u2013detailed'), '--detailed'); // en dash assert.equal(normalizeArg('\u2015detailed'), '--detailed'); // horizontal bar assert.equal(normalizeArg('\u2212detailed'), '--detailed'); // minus sign }); test('normalizeArg: run of 3+ ASCII hyphens collapses to --', async () => { const { normalizeArg } = await import('../commands/voiceDiag.js'); assert.equal(normalizeArg('---detailed'), '--detailed'); assert.equal(normalizeArg('----only'), '--only'); }); test('normalizeArg: leaves plain --flag and bare word untouched', async () => { const { normalizeArg } = await import('../commands/voiceDiag.js'); assert.equal(normalizeArg('--detailed'), '--detailed'); assert.equal(normalizeArg('detailed'), 'detailed'); assert.equal(normalizeArg('782'), '782'); }); test('normalizeArg: nullish input passed through', async () => { const { normalizeArg } = await import('../commands/voiceDiag.js'); assert.equal(normalizeArg(null), null); assert.equal(normalizeArg(undefined), undefined); }); test('parseWindowMinutes: unit-suffixed values (m/h/d) resolve to minutes', async () => { const { parseWindowMinutes } = await import('../commands/voiceDiag.js'); assert.equal(parseWindowMinutes('15m'), 15); assert.equal(parseWindowMinutes('15min'), 15); assert.equal(parseWindowMinutes('1h'), 60); assert.equal(parseWindowMinutes('6h'), 360); assert.equal(parseWindowMinutes('24h'), 1440); assert.equal(parseWindowMinutes('1d'), 1440); assert.equal(parseWindowMinutes('1day'), 1440); assert.equal(parseWindowMinutes('7d'), 10080, '7d resolves to 10080 min — matches the default window and the hard cap'); assert.equal(parseWindowMinutes('7days'), 10080); assert.equal(parseWindowMinutes('2 hours'), 120); }); test('parseWindowMinutes: bare integer treated as minutes', async () => { const { parseWindowMinutes } = await import('../commands/voiceDiag.js'); assert.equal(parseWindowMinutes('30'), 30); assert.equal(parseWindowMinutes('1440'), 1440); assert.equal(parseWindowMinutes('10080'), 10080); }); test('parseWindowMinutes: empty / null → undefined (falls to env default)', async () => { const { parseWindowMinutes } = await import('../commands/voiceDiag.js'); assert.equal(parseWindowMinutes(null), undefined); assert.equal(parseWindowMinutes(undefined), undefined); assert.equal(parseWindowMinutes(''), undefined); }); test('parseWindowMinutes: unrecognised token → null (caller shows friendly error)', async () => { const { parseWindowMinutes } = await import('../commands/voiceDiag.js'); assert.equal(parseWindowMinutes('bogus'), null); assert.equal(parseWindowMinutes('15x'), null); assert.equal(parseWindowMinutes('h1'), null); }); test('buildRemediationRegistry: contains every declared remediation exactly once', () => { const registry = buildRemediationRegistry(); const declared = new Map(); for (const c of CHECKS) { for (const rid of Object.keys(c.remediations || {})) { if (declared.has(rid)) { throw new Error(`duplicate remediation id in fixture: ${rid}`); } declared.set(rid, c.id); } } assert.equal(registry.size, declared.size); for (const [rid, checkId] of declared.entries()) { assert.ok(registry.has(rid), `missing remediation ${rid}`); assert.equal(registry.get(rid).check.id, checkId); } }); // ─── standards regression guards ─────────────────────────────────── // The "voice standards" reference table in README.md is generated // off the `standards` field on each check descriptor. If a check // forgets to declare it, we lose visibility of what the desired // state actually is. Fail loud instead of silently dropping the row. test('every registered check exposes a standards object', () => { const missing = CHECKS.filter((c) => !c.standards || typeof c.standards !== 'object'); assert.deepEqual( missing.map((c) => c.id), [], `checks without .standards: ${missing.map((c) => c.id).join(', ')}`, ); }); test('standards for DND / callWaiting / callIntercept encode the expected boolean shape', () => { const byId = new Map(CHECKS.map((c) => [c.id, c])); assert.equal(byId.get('dnd').standards.enabled, false); assert.equal(byId.get('callWaiting').standards.enabled, true); assert.equal(byId.get('callIntercept').standards.enabled, false); }); test('standards for callForwarding declare all three variants off', () => { const byId = new Map(CHECKS.map((c) => [c.id, c])); const cf = byId.get('callForwarding').standards; assert.equal(cf.always.enabled, false); assert.equal(cf.busy.enabled, false); assert.equal(cf.noAnswer.enabled, false); }); test('standards for voicemail encode enabled + no send-to-VM triggers + MWI on', () => { const byId = new Map(CHECKS.map((c) => [c.id, c])); const vm = byId.get('voicemail').standards; assert.equal(vm.enabled, true); assert.equal(vm.sendAllCalls.enabled, false); assert.equal(vm.sendBusyCalls.enabled, false); assert.equal(vm.sendUnansweredCalls.enabled, false); assert.equal(vm.messageStorage.mwiEnabled, true); }); // ─── port-hygiene bucket ─────────────────────────────────────────── // Each of these tests injects a phoneStatus snapshot shaped like // collectPhoneStatus() output, focussed on the fields the port check // consumes. async function loadPortChecks() { const [ { portTypeCheck, PORT_TYPE_STANDARDS }, { portVlanCheck, PORT_VLAN_STANDARDS }, { portPoeCheck, PORT_POE_STANDARDS }, { portEnabledCheck, PORT_ENABLED_STANDARDS }, ] = await Promise.all([ import('../services/voiceDiag/checks/port/portType.js'), import('../services/voiceDiag/checks/port/portVlan.js'), import('../services/voiceDiag/checks/port/portPoe.js'), import('../services/voiceDiag/checks/port/portEnabled.js'), ]); return { portTypeCheck, PORT_TYPE_STANDARDS, portVlanCheck, PORT_VLAN_STANDARDS, portPoeCheck, PORT_POE_STANDARDS, portEnabledCheck, PORT_ENABLED_STANDARDS, }; } function mkPortCtx(phones = [], dectBasestations = []) { return { ...mkCtx(), phoneStatus: { phones: { data: phones }, dectBasestations, }, }; } test('portType: all access ports → ok', async () => { const { portTypeCheck } = await loadPortChecks(); const ctx = mkPortCtx( [{ mac: 'aa:aa', name: 'P1', meraki: { portType: 'access', switchName: 'sw1', portName: 'p1', connectionType: 'Wired' } }], [{ mac: 'bb:bb', name: 'B1', meraki: { portType: 'access', switchName: 'sw1', portName: 'p3', connectionType: 'Wired' } }], ); const r = await portTypeCheck.run(ctx); assert.equal(r.status, 'ok'); assert.equal(r.details.compliant, 2); assert.equal(r.details.trunks, 0); }); test('portType: trunk uplink → warn, offender named', async () => { const { portTypeCheck } = await loadPortChecks(); const ctx = mkPortCtx([ { mac: 'aa:aa', name: 'P1', meraki: { portType: 'access', switchName: 'sw1', portName: 'p1', connectionType: 'Wired' } }, { mac: 'cc:cc', name: 'P2', meraki: { portType: 'trunk', switchName: 'sw1', portName: 'p2', connectionType: 'Wired' } }, ]); const r = await portTypeCheck.run(ctx); assert.equal(r.status, 'warn'); assert.equal(r.details.trunks, 1); assert.match(r.message, /TRUNK/); assert.equal(r.details.offenders[0].deviceLabel, 'P2'); }); test('portType: wireless phone → skipped from compliance count (no switchport)', async () => { const { portTypeCheck } = await loadPortChecks(); const ctx = mkPortCtx([ { mac: 'aa:aa', name: 'P1', meraki: { connectionType: 'Wireless' } }, ]); const r = await portTypeCheck.run(ctx); // With one device that's wireless, no compliant / trunks / unknown // → we land in the "all compliant" branch with compliant=0. assert.equal(r.status, 'ok'); assert.equal(r.details.wireless, 1); }); test('portType: no devices at all → skipped', async () => { const { portTypeCheck } = await loadPortChecks(); const ctx = mkPortCtx([], []); const r = await portTypeCheck.run(ctx); assert.equal(r.status, 'skipped'); }); test('portVlan: expected VLAN from env, mismatch → warn', async () => { const prev = process.env.VOICE_STANDARD_PHONE_VLAN; process.env.VOICE_STANDARD_PHONE_VLAN = '102'; try { const { portVlanCheck } = await loadPortChecks(); const ctx = mkPortCtx([ { mac: 'aa:aa', name: 'P1', meraki: { portType: 'access', vlan: 102, switchName: 'sw1', portName: 'p1', connectionType: 'Wired' } }, { mac: 'bb:bb', name: 'P2', meraki: { portType: 'access', vlan: 1, switchName: 'sw1', portName: 'p2', connectionType: 'Wired' } }, ]); const r = await portVlanCheck.run(ctx); assert.equal(r.status, 'warn'); assert.equal(r.details.expectedVlan, 102); assert.equal(r.details.wrong, 1); assert.match(r.message, /expected 102/); assert.match(r.message, /P2.*VLAN 1/); } finally { if (prev === undefined) delete process.env.VOICE_STANDARD_PHONE_VLAN; else process.env.VOICE_STANDARD_PHONE_VLAN = prev; } }); test('portVlan: env override — VOICE_STANDARD_PHONE_VLAN=200 makes 200 compliant', async () => { const prev = process.env.VOICE_STANDARD_PHONE_VLAN; process.env.VOICE_STANDARD_PHONE_VLAN = '200'; try { const { portVlanCheck } = await loadPortChecks(); const ctx = mkPortCtx([ { mac: 'aa:aa', name: 'P1', meraki: { portType: 'access', vlan: 200, switchName: 'sw', portName: 'p1', connectionType: 'Wired' } }, ]); const r = await portVlanCheck.run(ctx); assert.equal(r.status, 'ok'); assert.equal(r.details.expectedVlan, 200); assert.equal(r.details.compliant, 1); } finally { if (prev === undefined) delete process.env.VOICE_STANDARD_PHONE_VLAN; else process.env.VOICE_STANDARD_PHONE_VLAN = prev; } }); test('portVlan: trunk ports get skipped (owned by portType check)', async () => { const { portVlanCheck } = await loadPortChecks(); const ctx = mkPortCtx([ { mac: 'aa:aa', name: 'P1', meraki: { portType: 'access', vlan: 102, switchName: 'sw', portName: 'p1', connectionType: 'Wired' } }, { mac: 'bb:bb', name: 'P2', meraki: { portType: 'trunk', vlan: 999, switchName: 'sw', portName: 'p2', connectionType: 'Wired' } }, ]); const r = await portVlanCheck.run(ctx); assert.equal(r.status, 'ok'); assert.equal(r.details.trunkSkipped, 1); }); test('portPoe: PoE off on an access port → warn', async () => { const { portPoeCheck } = await loadPortChecks(); const ctx = mkPortCtx([ { mac: 'aa:aa', name: 'P1', meraki: { portType: 'access', poeEnabled: true, switchName: 'sw', portName: 'p1', connectionType: 'Wired' } }, { mac: 'bb:bb', name: 'P2', meraki: { portType: 'access', poeEnabled: false, switchName: 'sw', portName: 'p2', connectionType: 'Wired' } }, ]); const r = await portPoeCheck.run(ctx); assert.equal(r.status, 'warn'); assert.equal(r.details.off, 1); assert.match(r.message, /PoE DISABLED/); }); test('portEnabled: admin-disabled port → error (phone won\'t work)', async () => { const { portEnabledCheck } = await loadPortChecks(); const ctx = mkPortCtx([ { mac: 'aa:aa', name: 'P1', meraki: { portType: 'access', portEnabled: true, switchName: 'sw', portName: 'p1', connectionType: 'Wired' } }, { mac: 'bb:bb', name: 'P2', meraki: { portType: 'access', portEnabled: false, switchName: 'sw', portName: 'p2', connectionType: 'Wired' } }, ]); const r = await portEnabledCheck.run(ctx); assert.equal(r.status, 'error'); assert.equal(r.details.disabled, 1); }); test('port kill-switch: VOICE_STANDARD_ENABLED=false → all four port checks return skipped', async () => { const prev = process.env.VOICE_STANDARD_ENABLED; process.env.VOICE_STANDARD_ENABLED = 'false'; try { const { portTypeCheck, portVlanCheck, portPoeCheck, portEnabledCheck } = await loadPortChecks(); const ctx = mkPortCtx([ { mac: 'aa:aa', meraki: { portType: 'access', vlan: 102, poeEnabled: true, portEnabled: true, switchName: 'sw', portName: 'p1', connectionType: 'Wired' } }, ]); for (const chk of [portTypeCheck, portVlanCheck, portPoeCheck, portEnabledCheck]) { const r = await chk.run(ctx); assert.equal(r.status, 'skipped', `${chk.id} should be skipped by kill switch`); assert.match(r.message, /VOICE_STANDARD_ENABLED/); } } finally { if (prev === undefined) delete process.env.VOICE_STANDARD_ENABLED; else process.env.VOICE_STANDARD_ENABLED = prev; } }); // ─── combined "apply all N fixes" card ───────────────────────────── // bot.say has two supported signatures — `bot.say('markdown', md)` // (used by simple text posts) and `bot.say({ markdown, attachments })` // (used by adaptive-card posts). The mock captures the *message // body* from either shape so per-test assertions can match on // content rather than mimicking the framework calling convention. function mkBotMock() { return { messages: [], async say(a, b) { if (typeof a === 'string' && typeof b === 'string') this.messages.push(b); else if (a && typeof a === 'object') this.messages.push(a.markdown || JSON.stringify(a)); else this.messages.push(String(a ?? '')); }, }; } test('applyAllVoiceDiagRemediations: iterates entries in sequence, tolerates individual failure', async () => { const { applyAllVoiceDiagRemediations } = await import('../commands/voiceDiag.js'); const bot = mkBotMock(); const requester = { displayName: 'Op Test', email: 'op@example.com' }; // Two entries: one whose handler is real (disable_dnd via the // registry — but that would talk to WebexClient which we can't // in a test). We rely on the fact that the registered handler is // called with a payload we can inspect via a mock — but the actual // handler will throw on WebexClient import. So we test the // "unknown handler" path (safe) and the audit summary. const data = { combined: true, storeNum: '782', personLabel: 'Test Store', entries: [ { remediationId: 'nonexistent_handler_1', remediationPayload: {} }, { remediationId: 'nonexistent_handler_2', remediationPayload: {} }, ], }; await applyAllVoiceDiagRemediations(bot, data, null, requester); // Final summary posted regardless assert.ok(bot.messages.length >= 1, 'should post final summary'); const summary = bot.messages[bot.messages.length - 1]; assert.match(summary, /Apply-all complete/); assert.match(summary, /Applied: 0 \/ 2/); assert.match(summary, /Failed: 2/); assert.match(summary, /nonexistent_handler_1/); assert.match(summary, /no handler registered/); }); test('applyAllVoiceDiagRemediations: no entries → posts "no pending" hint', async () => { const { applyAllVoiceDiagRemediations } = await import('../commands/voiceDiag.js'); const bot = mkBotMock(); await applyAllVoiceDiagRemediations(bot, { combined: true, storeNum: '782', entries: [] }, null, {}); assert.equal(bot.messages.length, 1); assert.match(bot.messages[0], /No pending remediations/); }); test('cancelAllVoiceDiagRemediations: acknowledges the batch was cancelled', async () => { const { cancelAllVoiceDiagRemediations } = await import('../commands/voiceDiag.js'); const bot = mkBotMock(); await cancelAllVoiceDiagRemediations(bot, { combined: true, storeNum: '782', personLabel: 'Store 782', entries: [{ remediationId: 'a' }, { remediationId: 'b' }, { remediationId: 'c' }], }, null, { displayName: 'Op' }); assert.equal(bot.messages.length, 1); assert.match(bot.messages[0], /Cancelled all pending remediations/); assert.match(bot.messages[0], /3 fixes not applied/); });