jest.mock('../config', () => ({ siw: { baseUrl: 'https://siw.example.com/api', username: 'u', password: 'p' }, meraki: { apiKey: 'k', orgId: 'o' }, mdm: { url: 'https://mdm.example.com', username: 'u', password: 'p', tenantCode: 't' }, logLevel: 'error', })); jest.mock('../services/siw', () => ({ getStoreLocation: jest.fn(), getStoreGeneral: jest.fn(), getStoreRegisters: jest.fn(), getStorePrinters: jest.fn(), getStorePaymentTerminals: jest.fn(), })); jest.mock('../services/meraki', () => ({ findMerakiNetwork: jest.fn(), getMerakiDeviceAvailabilities: jest.fn(), getMerakiClients: jest.fn(), })); jest.mock('../services/mdm', () => ({ getMDMDevices: jest.fn(), })); jest.mock('../services/webexPhone', () => ({ collectPhoneStatus: jest.fn(), })); jest.mock('../services/avService', () => ({ collectAvStatus: jest.fn(), })); const { STORE_MODES, MDM_DEVICE_TYPES, filterMdmByType } = require('../constants'); const siw = require('../services/siw'); const meraki = require('../services/meraki'); const mdm = require('../services/mdm'); const webexPhone = require('../services/webexPhone'); const avService = require('../services/avService'); const { getStoreDetail } = require('../integrations/storeDetail'); describe('filterMdmByType with Customer Display marker', () => { it('matches CD devices and ignores SRV/MR/IPH', () => { const devices = [ { UserName: 'US000782SRV01' }, { UserName: 'US000782MR03' }, { UserName: 'US000782CD01' }, { UserName: 'US000782CD02' }, { UserName: 'US000782IPH04' }, ]; const cds = filterMdmByType(devices, MDM_DEVICE_TYPES.CUSTOMER_DISPLAY); expect(cds).toHaveLength(2); expect(cds.map(d => d.UserName)).toEqual(['US000782CD01', 'US000782CD02']); }); it('does not false-positive when name contains no CD substring', () => { const devices = [ { UserName: 'US000782SRV01' }, { UserName: 'US000782MR03' }, { UserName: 'US000782IPH04' }, ]; expect(filterMdmByType(devices, MDM_DEVICE_TYPES.CUSTOMER_DISPLAY)).toEqual([]); }); }); describe('getStoreDetail POS mode — Customer Displays section', () => { beforeEach(() => { jest.clearAllMocks(); // Minimal Meraki + SIW fixtures so the report can be built. meraki.findMerakiNetwork.mockResolvedValue({ id: 'N_1', name: 'AEO - 00782 - Standalone', url: 'https://n976.dashboard.meraki.com/AEO-00782/n/ABC/manage', }); meraki.getMerakiClients.mockResolvedValue([ // Customer Display advertised as the same hostname Meraki sees. { id: 'cd1-meraki', description: 'US000782CD01', status: 'Online', lastSeen: null }, ]); meraki.getMerakiDeviceAvailabilities.mockResolvedValue([]); siw.getStoreRegisters.mockResolvedValue([]); siw.getStorePrinters.mockResolvedValue([]); siw.getStorePaymentTerminals.mockResolvedValue([]); }); it('includes a Customer Displays section listing every CD device with its Meraki status', async () => { mdm.getMDMDevices.mockResolvedValue([ { UserName: 'US000782SRV01', DeviceFriendlyName: 'Store Server 01' }, { UserName: 'US000782CD01', DeviceFriendlyName: 'Customer Display 01' }, ]); const report = await getStoreDetail('782', STORE_MODES.POS); expect(report).toContain('**📟 Customer Displays (1)**'); expect(report).toContain('US000782CD01'); // The match through Meraki should report Online status. expect(report).toMatch(/US000782CD01.*Online/); }); it('omits the Customer Displays section when no CD devices exist', async () => { mdm.getMDMDevices.mockResolvedValue([ { UserName: 'US000782SRV01', DeviceFriendlyName: 'Store Server 01' }, ]); const report = await getStoreDetail('782', STORE_MODES.POS); expect(report).not.toContain('Customer Displays'); }); it('places Customer Displays after Mobile Registers in the POS report', async () => { mdm.getMDMDevices.mockResolvedValue([ { UserName: 'US000782MR01' }, { UserName: 'US000782CD01' }, ]); const report = await getStoreDetail('782', STORE_MODES.POS); const mrIdx = report.indexOf('Mobile Registers'); const cdIdx = report.indexOf('Customer Displays'); expect(mrIdx).toBeGreaterThan(-1); expect(cdIdx).toBeGreaterThan(-1); expect(cdIdx).toBeGreaterThan(mrIdx); }); }); describe('getStoreDetail PHONE mode', () => { beforeEach(() => { jest.clearAllMocks(); meraki.findMerakiNetwork.mockResolvedValue({ id: 'N_1', name: 'AEO - 00782 - Standalone', url: 'https://n976.dashboard.meraki.com/AEO-00782/n/ABC/manage', }); // The Meraki client list contains the matching MAC for our wired phone. meraki.getMerakiClients.mockResolvedValue([ { id: 'cli-phone', description: 'PHN-7841-FRONT', mac: '11:22:33:44:55:66', status: 'Online', lastSeen: new Date().toISOString(), }, { id: 'cli-base', description: 'DECT-BASE-01', mac: 'BA:5E:01:00:00:01', status: 'Online', lastSeen: new Date().toISOString(), }, ]); }); it('renders the location/main-number header, wired phones (Meraki-matched by MAC, with extension), and a registered handset nested under its base with - naming', async () => { webexPhone.collectPhoneStatus.mockResolvedValue({ phones: [ { mac: '11:22:33:44:55:66', name: 'Front Desk Phone', model: 'Cisco 7841', firmware: '12.0', extension: '50782', status: 'connected', }, ], basestations: [ { id: 'base-1', mac: 'BA:5E:01:00:00:01', name: 'Base 1', model: 'DBS-110', firmware: '1.2.3', linesRegistered: 1, }, ], handsets: [ { id: 'h-1', index: 1, name: '50782', // Webex sometimes returns the extension as the display name. extension: '50782', status: 'unknown', // Webex returns this for DECT handsets; should NOT propagate to the report. baseStationId: 'base-1', lastRegistrationTime: new Date().toISOString(), }, ], dectNetwork: { id: 'dn-1', name: 'Store 0782', locationName: 'Store 0782' }, locationMainNumber: '+14123694426', }); const report = await getStoreDetail('782', STORE_MODES.PHONE); // Header at the top: location + store DID together, no trailing footer. expect(report).toContain('**📍 Store 0782**'); expect(report).toContain('📞 Main: **+14123694426**'); expect(report).not.toContain('Store Main Number'); // The header sits ahead of all other sections. expect(report.indexOf('Store 0782')).toBeLessThan(report.indexOf('Wired Phones')); expect(report).toContain('**📞 Wired Phones (1)**'); expect(report).toContain('Front Desk Phone'); // Extension rendered, firmware suppressed. expect(report).toContain('ext 50782'); expect(report).not.toContain('fw 12.0'); // MAC-strategy match through to Meraki should yield an Online status line. expect(report).toMatch(/Front Desk Phone.*Online/); expect(report).toContain('**📡 DECT Network**'); expect(report).toContain('Base 1'); expect(report).toMatch(/Base 1.*Online/); // Handset name uses the - form, NOT the bare extension. expect(report).toContain('1-50782'); // Handset is nested under its base. const baseIdx = report.indexOf('Base 1'); const handsetIdx = report.indexOf('1-50782'); expect(handsetIdx).toBeGreaterThan(baseIdx); // Handset presence is derived from last-registration recency. expect(report).toMatch(/1-50782.*✅ Registered/); expect(report).not.toMatch(/1-50782.*unknown/); // Registered + assigned handsets should NOT appear in the trailing // "Unregistered Handsets" section. expect(report).not.toContain('Unregistered Handsets'); }); it('moves stale, never-registered, and orphan handsets into the trailing Unregistered Handsets section', async () => { const staleStamp = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString(); webexPhone.collectPhoneStatus.mockResolvedValue({ phones: [], basestations: [{ id: 'base-1', mac: 'BA:5E:01:00:00:01', name: 'Base 1' }], handsets: [ { id: 'h-fresh', index: 1, extension: '50782', baseStationId: 'base-1', lastRegistrationTime: new Date().toISOString(), }, { id: 'h-stale', index: 2, extension: '50782', baseStationId: 'base-1', lastRegistrationTime: staleStamp, }, { id: 'h-never', index: 3, extension: '50782', baseStationId: 'base-1', lastRegistrationTime: null, }, { id: 'h-orphan', index: 4, extension: '50782', baseStationId: 'base-removed', lastRegistrationTime: new Date().toISOString(), }, ], dectNetwork: { id: 'dn-1', name: 'Store 0782', locationName: 'Store 0782' }, locationMainNumber: null, }); const report = await getStoreDetail('782', STORE_MODES.PHONE); expect(report).toContain('**📵 Unregistered Handsets (3)**'); // Fresh handset is nested under its base, BEFORE the unregistered section. const baseIdx = report.indexOf('Base 1'); const unregIdx = report.indexOf('Unregistered Handsets'); const freshIdx = report.indexOf('1-50782'); expect(freshIdx).toBeGreaterThan(baseIdx); expect(freshIdx).toBeLessThan(unregIdx); // The three problem handsets all appear in the trailing section, each with // the right presence label. const trail = report.slice(unregIdx); expect(trail).toMatch(/2-50782.*⚠️ Last registered/); expect(trail).toMatch(/3-50782.*❓ No registration data/); expect(trail).toContain('4-50782'); }); it('renders an unavailable banner with the supplied reason', async () => { webexPhone.collectPhoneStatus.mockResolvedValue({ unavailable: true, reason: 'No Webex person found for ae00782@ae.com.', }); const report = await getStoreDetail('782', STORE_MODES.PHONE); expect(report).toContain('Webex phone data unavailable'); expect(report).toContain('No Webex person found for ae00782@ae.com.'); expect(report).toContain('npm run webex:seed'); }); it('renders the unavailable banner when collectPhoneStatus throws unexpectedly', async () => { webexPhone.collectPhoneStatus.mockRejectedValue(new Error('WEBEX_CLIENT_ID is required')); const report = await getStoreDetail('782', STORE_MODES.PHONE); expect(report).toContain('Webex phone data unavailable'); expect(report).toContain('WEBEX_CLIENT_ID is required'); }); }); describe('getStoreDetail AV mode', () => { beforeEach(() => { jest.clearAllMocks(); meraki.findMerakiNetwork.mockResolvedValue({ id: 'N_1', name: 'AEO - 00782 - Standalone', url: 'https://n976.dashboard.meraki.com/AEO-00782/n/ABC/manage', }); meraki.getMerakiClients.mockResolvedValue([ { id: 'cli-amp', description: 'US000782AMP', mac: 'AA:BB:CC:00:00:01', status: 'Online', lastSeen: new Date().toISOString(), }, { id: 'cli-atv1', description: 'US000782AppleTV01', mac: 'AA:BB:CC:00:00:02', status: 'Online', lastSeen: new Date().toISOString(), }, { id: 'cli-vw', description: 'US000782VW1', mac: 'AA:BB:CC:00:00:03', status: 'Online', lastSeen: new Date().toISOString(), }, ]); meraki.getMerakiDeviceAvailabilities.mockResolvedValue([]); }); it('renders Atlas AMP + every MDM AV subsection in fixed order with correct counts', async () => { avService.collectAvStatus.mockResolvedValue({ devices: [ { id: 'd1', name: 'US000782AMP', model: 'Cisco AMP', mac: 'AA:BB:CC:00:00:01', ip: '10.0.0.5', online: true, lastSeen: new Date().toISOString(), }, ], }); mdm.getMDMDevices.mockResolvedValue([ // Two Apple TVs. { UserName: 'US000782AppleTV01', DeviceFriendlyName: 'US000782AppleTV01', MacAddress: 'AA:BB:CC:00:00:02', Model: 'Apple TV 4K', }, { UserName: 'US000782AppleTV02', DeviceFriendlyName: 'US000782AppleTV02' }, // Video wall. { UserName: 'US000782VW1', DeviceFriendlyName: 'US000782VW1', MacAddress: 'AA:BB:CC:00:00:03', Model: 'Samsung VW', }, // Music + LED. { UserName: 'US000782MSC1', DeviceFriendlyName: 'US000782MSC1' }, { UserName: 'US000782LED1', DeviceFriendlyName: 'US000782LED1' }, // Non-AV devices must be ignored. { UserName: 'US000782IPH04' }, { UserName: 'US000782SRV01' }, ]); const report = await getStoreDetail('782', STORE_MODES.AV); expect(report).toContain('**📡 Atlas AMP (1)**'); expect(report).toContain('**📺 Apple TVs (2)**'); expect(report).toContain('**🖼️ Video Walls (1)**'); expect(report).toContain('**🎵 Music Players (1)**'); expect(report).toContain('**💡 LED Displays (1)**'); // Fixed render order: Atlas first, then Apple TVs, VW, Music, LED. const idxAtlas = report.indexOf('Atlas AMP'); const idxAppleTV = report.indexOf('Apple TVs'); const idxVW = report.indexOf('Video Walls'); const idxMusic = report.indexOf('Music Players'); const idxLED = report.indexOf('LED Displays'); expect(idxAtlas).toBeLessThan(idxAppleTV); expect(idxAppleTV).toBeLessThan(idxVW); expect(idxVW).toBeLessThan(idxMusic); expect(idxMusic).toBeLessThan(idxLED); // Online indicator + Meraki match should appear for the AMP. expect(report).toMatch(/US000782AMP.*Cisco AMP.*✅ Online.*Online.*Meraki Client/); // AppleTV with MAC also matches Meraki. expect(report).toMatch(/US000782AppleTV01.*Apple TV 4K.*Online.*Meraki Client/); // Non-AV devices must NOT leak into the report. expect(report).not.toContain('US000782IPH04'); expect(report).not.toContain('US000782SRV01'); }); it('shows only Atlas when MDM returns no AV devices (no empty subsection headers)', async () => { avService.collectAvStatus.mockResolvedValue({ devices: [ { id: 'd1', name: 'US000782AMP', model: 'Cisco AMP', mac: 'AA:BB:CC:00:00:01', online: true, }, ], }); mdm.getMDMDevices.mockResolvedValue([ { UserName: 'US000782IPH04' }, { UserName: 'US000782SRV01' }, ]); const report = await getStoreDetail('782', STORE_MODES.AV); expect(report).toContain('Atlas AMP (1)'); expect(report).not.toContain('Apple TVs'); expect(report).not.toContain('Video Walls'); expect(report).not.toContain('Music Players'); expect(report).not.toContain('LED Displays'); }); it('shows the Atlas-unavailable banner above the MDM sections when Atlas is down', async () => { avService.collectAvStatus.mockResolvedValue({ devices: [], unavailable: true, reason: 'ATLAS_AUTH_KEY is not set', }); mdm.getMDMDevices.mockResolvedValue([ { UserName: 'US000782AppleTV01', DeviceFriendlyName: 'US000782AppleTV01' }, ]); const report = await getStoreDetail('782', STORE_MODES.AV); expect(report).toContain('Atlas AV data unavailable'); expect(report).toContain('ATLAS_AUTH_KEY is not set'); expect(report).toContain('Apple TVs (1)'); // Atlas section header should NOT render when there are no Atlas devices. expect(report).not.toContain('Atlas AMP ('); // Banner appears above the MDM subsections. expect(report.indexOf('Atlas AV data unavailable')).toBeLessThan(report.indexOf('Apple TVs')); }); it('shows the Atlas-unavailable banner even when collectAvStatus throws unexpectedly', async () => { avService.collectAvStatus.mockRejectedValue(new Error('ECONNRESET')); mdm.getMDMDevices.mockResolvedValue([]); const report = await getStoreDetail('782', STORE_MODES.AV); expect(report).toContain('Atlas AV data unavailable'); expect(report).toContain('ECONNRESET'); }); it('renders the empty-state message when neither source has any devices', async () => { avService.collectAvStatus.mockResolvedValue({ devices: [] }); mdm.getMDMDevices.mockResolvedValue([]); const report = await getStoreDetail('782', STORE_MODES.AV); expect(report).toContain('No AV hardware registered for this store.'); }); it('reflects Atlas online=false as ⚠️ Offline in the prefix', async () => { avService.collectAvStatus.mockResolvedValue({ devices: [{ id: 'd1', name: 'US000782AMP', model: 'Cisco AMP', online: false }], }); mdm.getMDMDevices.mockResolvedValue([]); const report = await getStoreDetail('782', STORE_MODES.AV); expect(report).toMatch(/US000782AMP.*⚠️ Offline/); }); it('renders ❓ Unknown for Atlas devices with no derivable status', async () => { avService.collectAvStatus.mockResolvedValue({ devices: [{ id: 'd1', name: 'US000782AMP', model: 'Cisco AMP', online: null }], }); mdm.getMDMDevices.mockResolvedValue([]); const report = await getStoreDetail('782', STORE_MODES.AV); expect(report).toMatch(/US000782AMP.*❓ Unknown/); }); });