// 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 → warn + compound remediation', async () => { 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, 'warn'); assert.equal(r.remediation.action, 'clear_call_forwarding'); assert.deepEqual(r.remediation.payload.variantsToClear, ['always']); assert.match(r.message, /\+18005551212/); }); 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, 'warn'); 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 → ok', async () => { const ctx = mkCtx({ response: { enabled: false } }); const r = await voicemailCheck.run(ctx); assert.equal(r.status, 'ok'); }); test('voicemail: sendAllCalls on → error', async () => { const ctx = mkCtx({ response: { enabled: true, sendAllCalls: { enabled: true }, messageStorage: { mwiEnabled: true }, }, }); const r = await voicemailCheck.run(ctx); assert.equal(r.status, 'error'); assert.match(r.message, /never ring/); }); test('voicemail: enabled but MWI off → warn', async () => { const ctx = mkCtx({ response: { enabled: true, 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, 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 unusual forwarding → ok', async () => { const ctx = mkCtx({ response: { enabled: true, 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 (info only, no remediation)', async () => { const ctx = mkCtx({ response: { enabled: true } }); const r = await hotelingCheck.run(ctx); assert.equal(r.status, 'warn'); assert.equal(r.remediation, null); }); 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('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); } });