collabSupport/tests/renderers.test.js
jmcqueen 351f89a9a4 Initial commit: CollabFinder Webex bot
Multi-integration Webex chat/HTTP bot that unifies phone, AV, and
network status for retail store support. Consolidates data from
Webex Calling, Meraki, Workspace ONE (MDM), Atlas AMP, RED digital
signage, and OptiSigns into rich per-store status commands.

Key surfaces:
- /phonestatus, /avstatus — per-store phone & AV device reports with
  clickable Meraki deep-links and per-port detail.
- /webexhost — check/assign Webex Meetings host licenses via the
  Service App; adaptive-card confirmation flow, HTTP-API-gated.
- /offboarduser — full Webex Admin offboarding (auth revoke, device
  wipe, license removal); adaptive-card confirmation.
- /jirapoll — on-demand trigger for the hourly Jira poller.
- /bulkavstatuscsv — bulk store CSV export with concurrency limits.

Automation:
- Hourly Jira poller (node-cron) with an X.AI (Grok) ticket classifier
  that categorizes unassigned tickets as phone/av/skip, extracts store
  numbers from free-text, and enriches Jira with the same detailed
  markdown the chat commands emit (converted to Jira ADF, preserves
  bold + Meraki links). Idempotent via a `bot-enriched` Jira label.

Architecture:
- Node.js 20+, ESM, Express 5, webex-node-bot-framework.
- Layered integrations (integrations/*), services (services/*),
  commands (commands/*), utils (utils/*).
- Shared markdown renderers (services/renderers/*) feed both chat
  handlers and the Jira poller so the two surfaces stay in sync.
- Hand-rolled markdown-to-ADF converter (utils/markdownToAdf.js) —
  no new npm dependency.
- Node built-in test runner (`node --test tests/*.test.js`), 30 tests
  covering the converter, renderers, and poller ADF assembly.

Docker + docker-compose deployment. Config via .env
(see .env.example for the full option surface).
2026-07-01 16:55:03 -04:00

167 lines
6.3 KiB
JavaScript

// Golden-ish tests for services/renderers/{phone,av}StatusRenderer.js
//
// True byte-for-byte golden strings aren't practical because both
// renderers call `simpleTimeAgo` (which uses `Date.now()`). Instead we
// assert on structural properties AND on the exact assembly of key
// lines — enough to catch a regression like "we lost the Meraki
// `[Meraki↗](url)` link" or "the header format changed" but robust to
// time-of-day drift.
import test from 'node:test';
import assert from 'node:assert/strict';
import { renderPhoneStatusMarkdown } from '../services/renderers/phoneStatusRenderer.js';
import { renderAvStatusMarkdown } from '../services/renderers/avStatusRenderer.js';
// Timestamp exactly 3 hours in the past — makes `simpleTimeAgo`
// deterministic to "3 hours ago" for the duration of this test run.
const threeHrAgo = () => new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString();
test('phone renderer: header and empty-store message', () => {
const md = renderPhoneStatusMarkdown({}, { storeNum: '782' });
assert.match(md, /^\*\*Phone Status - Store 782\*\*/);
assert.match(md, /No phones or DECT basestations found/);
});
test('phone renderer: full desk-phone entry preserves Meraki link', () => {
const data = {
phones: {
data: [{
displayName: 'PHONE 782-1',
status: 'connected',
lastSeen: threeHrAgo(),
firmware: '12.0.4',
serial: 'ABC12345',
meraki: {
switchName: 'STORE-782-SW1',
status: 'Online',
port: '17',
vlan: '20',
ip: '10.1.1.5',
lastSeen: threeHrAgo(),
clientUrl: 'https://n123.meraki.com/example/manage/clients/abc/overview',
usage: { sent: 1024, recv: 4096 },
},
}],
},
telephonyProfile: { timeZone: 'America/New_York' },
locationMainNumber: '+14125550100',
};
const md = renderPhoneStatusMarkdown(data, { storeNum: '782', footer: false });
// Header lines the poller cares about
assert.match(md, /\*\*Timezone:\*\* America\/New_York/);
assert.match(md, /\*\*PhoneNumber:\*\* \+14125550100/);
// Bold device name
assert.match(md, /\*\*PHONE 782-1\*\*/);
// Firmware / serial line
assert.match(md, /FW: 12\.0\.4 • Serial: ABC12345/);
// Meraki port info + link — the whole reason we ripped out the codeBlock
assert.match(md, /\*\*STORE-782-SW1\*\*/);
assert.match(md, /\[Meraki↗\]\(https:\/\/n123\.meraki\.com\/example\/manage\/clients\/abc\/overview\)/);
// Data usage
assert.match(md, /Data \(recent\): 1 KB sent \/ 4 KB recv/);
// No footer when opts.footer is false — Jira uses this mode
assert.doesNotMatch(md, /Last checked/);
});
test('phone renderer: footer appears when opts.footer=true (default) on non-empty data', () => {
// The empty-store branch legitimately returns early with no footer
// — that matches the chat handler's historical behavior. Feed a
// minimal populated fixture to exercise the footer path.
const data = {
phones: {
data: [{ displayName: 'X', status: 'connected', lastSeen: threeHrAgo() }],
},
};
const md = renderPhoneStatusMarkdown(data, { storeNum: '782' });
assert.match(md, /Last checked:/);
});
test('phone renderer: detailed mode reveals SIP details', () => {
const data = {
phones: {
data: [{
displayName: 'PHONE 782-2',
status: 'connected',
lastSeen: threeHrAgo(),
primarySipUrl: 'sip:782-2@aeo2go.webex.com',
sipUrls: ['sip:a@x', 'sip:b@x', 'sip:c@x'],
}],
},
};
const compact = renderPhoneStatusMarkdown(data, { storeNum: '782', detailed: false, footer: false });
const detailed = renderPhoneStatusMarkdown(data, { storeNum: '782', detailed: true, footer: false });
assert.doesNotMatch(compact, /SIP: sip:782-2/);
assert.match(detailed, /SIP: sip:782-2@aeo2go\.webex\.com/);
assert.match(detailed, /Alt SIPs: sip:a@x, sip:b@x…/);
});
test('av renderer: header always says (Mode: detailed)', () => {
const md = renderAvStatusMarkdown({}, { storeNum: '782', footer: false });
assert.match(md, /^\*\*Device Status - Store 782\*\* \(Mode: detailed\)/);
});
test('av renderer: MDM entry with Meraki wired client preserves link', () => {
const data = {
mdm: {
data: [{
friendlyName: 'STORE-782-KIOSK-1',
lastSeen: threeHrAgo(),
meraki: {
deviceName: 'STORE-782-SW2',
recentDeviceConnection: 'Wired',
status: 'Online',
clientStatus: 'Online',
port: '4',
vlan: '30',
ip: '10.1.2.20',
mac: 'aa:bb:cc:dd:ee:ff',
lastSeen: threeHrAgo(),
clientUrl: 'https://n123.meraki.com/example/manage/clients/xyz/overview',
},
}],
},
};
const md = renderAvStatusMarkdown(data, { storeNum: '782', footer: false });
assert.match(md, /\*\*STORE-782-KIOSK-1\*\*/);
assert.match(md, /\*\*STORE-782-SW2 \(Wired - Online\)\*\*/);
assert.match(md, /\[Meraki↗\]\(https:\/\/n123\.meraki\.com\/example\/manage\/clients\/xyz\/overview\)/);
// Port line with double-arrow indent
assert.match(md, / → → Port: \*\*4\*\*/);
});
test('av renderer: absent MDM devices prints friendly message', () => {
const md = renderAvStatusMarkdown({ mdm: { data: [] } }, { storeNum: '782', footer: false });
assert.match(md, /No MDM devices found for this store/);
});
test('av renderer: Atlas AMP with vitals renders temps + fan + amps', () => {
const data = {
mdm: { data: [] },
atlas: {
data: [{
name: 'US000782AMP',
status: 'online',
last_seen_at: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
model: { name: 'Atlas-4M' },
firmware: { version: '1.9.3' },
sn: 'SN-782-AMP',
state: {
voltageMonitor: '120.4',
faultStatus: 0,
tempCpu: '40', tempPsu: '35', tempIo: '38',
fanSpeed: 45.7,
ampStatus_1: 'Active',
ampStatus_2: 'Ready',
IpAddress: '10.1.9.9',
},
}],
},
};
const md = renderAvStatusMarkdown(data, { storeNum: '782', footer: false });
assert.match(md, /\*\*Atlas Devices:\*\*/);
assert.match(md, /\*\*US000782AMP\*\* \(online\)/);
assert.match(md, /Atlas-4M • FW 1\.9\.3 • SN SN-782-AMP • IP 10\.1\.9\.9/);
assert.match(md, /CPU: 104°F • PSU: 95°F • Io: 100°F • Voltage: 120\.4V • Fan: 46%/);
assert.match(md, /Amps: Amp1: Active, Amp2: Ready/);
});