jest.mock('../config', () => ({ webexServiceApp: { clientId: 'c', clientSecret: 's', tokensPath: '/tmp/x.json' }, logLevel: 'error', })); jest.mock('../services/webexService', () => ({ request: jest.fn(), })); const webex = require('../services/webexService'); const phone = require('../services/webexPhone'); function setRouteResponses(routes) { webex.request.mockReset(); webex.request.mockImplementation(async (method, pathSuffix, _body, _params) => { const key = `${method} ${pathSuffix}`; if (key in routes) { const v = routes[key]; return typeof v === 'function' ? v() : v; } throw new Error(`Unmocked Webex request: ${key}`); }); } describe('webexPhone.storeEmail', () => { it('pads to 5 digits and uses the @ae.com domain', () => { expect(phone.storeEmail(782)).toBe('ae00782@ae.com'); expect(phone.storeEmail('00305')).toBe('ae00305@ae.com'); expect(phone.storeEmail(' 12345 ')).toBe('ae12345@ae.com'); }); }); describe('webexPhone.WIRED_PHONE_PATTERN', () => { it('matches 7821 / 7841 and rejects everything else', () => { expect(phone.WIRED_PHONE_PATTERN.test('Cisco IP Phone 7821')).toBe(true); expect(phone.WIRED_PHONE_PATTERN.test('CP-7841-K9')).toBe(true); expect(phone.WIRED_PHONE_PATTERN.test('Cisco 8851')).toBe(false); expect(phone.WIRED_PHONE_PATTERN.test('Webex Desk Pro')).toBe(false); }); }); describe('collectPhoneStatus', () => { it('returns the full flat shape and associates handsets with their base', async () => { setRouteResponses({ 'GET people': { items: [{ id: 'person-1' }] }, 'GET people/person-1': { id: 'person-1', displayName: 'AE Store 782', extension: '50782', }, 'GET devices': { items: [ { mac: '11:22:33:44:55:66', product: 'Cisco 7841', displayName: 'Front Desk Phone' }, { mac: 'aa:bb:cc:dd:ee:ff', product: 'Cisco 8851', displayName: 'Should be filtered out', }, { mac: '99:88:77:66:55:44', product: 'CP-7821-K9' }, ], }, 'GET telephony/config/people/person-1/dectNetworks': { dectNetworks: [ { id: 'dn-1', name: 'Store 0782', location: { id: 'loc-1', name: 'Store 0782' }, numberOfHandsetsAssigned: 2, }, ], }, 'GET telephony/config/locations/loc-1/dectNetworks/dn-1/baseStations': { items: [ { id: 'base-1', mac: 'BA:5E:01:00:00:01', displayName: 'Base 1', status: 'online' }, { id: 'base-2', mac: 'BA:5E:02:00:00:02', displayName: 'Base 2', status: 'online' }, ], }, 'GET telephony/config/locations/loc-1/dectNetworks/dn-1/handsets': { items: [ { id: 'h-1', index: 1, defaultDisplayName: 'Handset 1', status: 'online' }, { id: 'h-2', index: 2, defaultDisplayName: 'Handset 2', status: 'offline' }, { id: 'h-3', index: 3, defaultDisplayName: 'Handset 3 (orphan)', status: 'unknown' }, ], }, 'GET telephony/config/locations/loc-1/dectNetworks/dn-1/handsets/h-1': { id: 'h-1', index: 1, baseStationId: 'base-1', lines: [{ extension: '1001' }], }, 'GET telephony/config/locations/loc-1/dectNetworks/dn-1/handsets/h-2': { id: 'h-2', index: 2, baseStationId: 'base-2', lines: [{ extension: '1002' }], }, 'GET telephony/config/locations/loc-1/dectNetworks/dn-1/handsets/h-3': { id: 'h-3', index: 3, baseStationId: null, // orphan lines: [], }, 'GET telephony/config/locations/loc-1': { callingLineId: { phoneNumber: '+15555550100' }, }, }); const result = await phone.collectPhoneStatus('782'); expect(result.unavailable).toBeFalsy(); expect(result.phones).toHaveLength(2); // 7841 + 7821, 8851 filtered out expect(result.phones.map(p => p.model)).toEqual(['Cisco 7841', 'CP-7821-K9']); // Every wired phone inherits the store-person extension. expect(result.phones.every(p => p.extension === '50782')).toBe(true); expect(result.basestations).toHaveLength(2); expect(result.handsets).toHaveLength(3); // Handset → base association via the detail endpoint const h1 = result.handsets.find(h => h.id === 'h-1'); const h2 = result.handsets.find(h => h.id === 'h-2'); const h3 = result.handsets.find(h => h.id === 'h-3'); expect(h1.baseStationId).toBe('base-1'); expect(h2.baseStationId).toBe('base-2'); expect(h3.baseStationId).toBeNull(); expect(h1.extension).toBe('1001'); // The handset's slot index flows through from both list and detail // endpoints so the renderer can display "-". expect(h1.index).toBe(1); expect(h2.index).toBe(2); expect(h3.index).toBe(3); expect(result.locationMainNumber).toBe('+15555550100'); expect(result.dectNetwork?.id).toBe('dn-1'); }); it('returns unavailable=true when no person exists for the store email', async () => { setRouteResponses({ 'GET people': { items: [] }, }); const result = await phone.collectPhoneStatus('999'); expect(result.unavailable).toBe(true); expect(result.reason).toMatch(/ae00999@ae\.com/); }); it('returns unavailable=true when the underlying request layer throws on first call', async () => { webex.request.mockReset(); webex.request.mockRejectedValue(new Error('WEBEX_CLIENT_ID is required')); const result = await phone.collectPhoneStatus('782'); // collectPhoneStatus swallows getPersonIdByEmail errors and returns // "no person" rather than crashing — equivalent surface to "unavailable". expect(result.unavailable).toBe(true); }); it('handles a store with no DECT network gracefully (wired phones only)', async () => { setRouteResponses({ 'GET people': { items: [{ id: 'person-2' }] }, 'GET people/person-2': { id: 'person-2', extension: '50305' }, 'GET devices': { items: [{ mac: '11:22:33:44:55:66', product: 'Cisco 7841' }] }, 'GET telephony/config/people/person-2/dectNetworks': { dectNetworks: [] }, }); const result = await phone.collectPhoneStatus('305'); expect(result.phones).toHaveLength(1); expect(result.phones[0].extension).toBe('50305'); expect(result.basestations).toEqual([]); expect(result.handsets).toEqual([]); expect(result.dectNetwork).toBeNull(); expect(result.locationMainNumber).toBeNull(); }); describe('getPersonExtension', () => { it('returns extension from the top-level extension field', async () => { setRouteResponses({ 'GET people/p1': { id: 'p1', extension: '50782' }, }); expect(await phone.getPersonExtension('p1')).toBe('50782'); }); it('falls back to phoneNumbers entry with extension-like type', async () => { setRouteResponses({ 'GET people/p2': { id: 'p2', phoneNumbers: [ { type: 'work', value: '+14123694426' }, { type: 'work_extension', value: '50305' }, ], }, }); expect(await phone.getPersonExtension('p2')).toBe('50305'); }); it('returns null when nothing extension-like is present', async () => { setRouteResponses({ 'GET people/p3': { id: 'p3', phoneNumbers: [{ type: 'work', value: '+1...' }] }, }); expect(await phone.getPersonExtension('p3')).toBeNull(); }); }); });