const { findMatchingClient, getClientStatus, formatLastSeen, buildMerakiClientLink, extractHostname, normalizeMac, } = require('../utils/merakiMatcher'); describe('merakiMatcher', () => { const sampleClients = [ { id: 'c1', description: 'Register 305', status: 'Online', lastSeen: '2025-01-01T10:00:00Z' }, { id: 'c2', description: 'Printer Front', status: 'Offline', lastSeen: Date.now() - 5 * 60 * 1000, }, { id: 'c3', description: '192.168.10.45 - Terminal', status: 'Online', lastSeen: null }, { id: 'c4', description: 'SRV-042', status: 'Online' }, ]; describe('findMatchingClient', () => { it('matches by exact or substring on description', () => { expect(findMatchingClient(sampleClients, { name: 'Register 305' })).toBe(sampleClients[0]); expect(findMatchingClient(sampleClients, { name: 'printer' })).toBe(sampleClients[1]); }); it('matches using multiple identifier fields', () => { const match = findMatchingClient(sampleClients, { deviceName: 'Terminal', ip_address: '192.168.10.45', }); expect(match).toBe(sampleClients[2]); }); it('matches MDM style UserName / DeviceFriendlyName', () => { const match = findMatchingClient(sampleClients, { UserName: 'SRV-042', DeviceFriendlyName: 'Server 042', }); expect(match).toBe(sampleClients[3]); }); it('returns null when no clients or no match', () => { expect(findMatchingClient([], { name: 'foo' })).toBeNull(); expect(findMatchingClient(sampleClients, { name: 'nonexistent' })).toBeNull(); }); // Payment-terminal regression: SIW reports the FQDN in `ip_address` // (e.g. "VFI-807-005-168.us000782.stores.ae.com") and Meraki advertises // only the lowercase hostname before the first dot. Before the fix the // matcher compared an upper-case ipPrefix against a lower-cased desc and // missed every payment terminal. it('matches a Meraki client when SIW gives an FQDN ip_address (case-insensitive)', () => { const clients = [ { id: 'm1', description: 'vfi-807-005-168', status: 'Online', lastSeen: null }, ]; const match = findMatchingClient(clients, { deviceName: 'Terminal 11', adyenName: 'P400Plus-807005168', ip_address: 'VFI-807-005-168.us000782.stores.ae.com', }); expect(match).toBe(clients[0]); }); }); describe('normalizeMac', () => { it('strips common separators and lowercases', () => { expect(normalizeMac('AA:BB:CC:11:22:33')).toBe('aabbcc112233'); expect(normalizeMac('aa-bb-cc-11-22-33')).toBe('aabbcc112233'); expect(normalizeMac('aabb.cc11.2233')).toBe('aabbcc112233'); expect(normalizeMac('AABBCC112233')).toBe('aabbcc112233'); }); it('returns null for non-12-hex input', () => { expect(normalizeMac('')).toBeNull(); expect(normalizeMac(null)).toBeNull(); expect(normalizeMac('aa:bb:cc')).toBeNull(); expect(normalizeMac('not a mac at all')).toBeNull(); expect(normalizeMac('aabbcc1122334455')).toBeNull(); }); }); describe('findMatchingClient — MAC strategy', () => { const clients = [ { id: 'client-name-match', description: 'VFI-807-005-168', mac: '11:22:33:44:55:66', status: 'Offline', }, { id: 'client-mac-match', description: 'something-totally-different', mac: 'AA-BB-CC-DD-EE-FF', status: 'Online', }, ]; it('matches by MAC when present (deterministic, case/separator insensitive)', () => { const match = findMatchingClient(clients, { mac: 'aabb.ccdd.eeff' }); expect(match?.id).toBe('client-mac-match'); }); it('lets MAC win even when a name strategy could also match', () => { // The first client has a description that would match by name, but we // pass a MAC for the second one — MAC should take priority. const match = findMatchingClient(clients, { mac: 'aabbccddeeff', name: 'VFI-807-005-168', }); expect(match?.id).toBe('client-mac-match'); }); it('falls back to name strategy when MAC is missing or unmatched', () => { const match = findMatchingClient(clients, { mac: 'ffffffffffff', name: 'VFI-807-005-168', }); expect(match?.id).toBe('client-name-match'); }); it('returns null when nothing matches and MAC is invalid', () => { const match = findMatchingClient(clients, { mac: 'not-a-mac' }); expect(match).toBeNull(); }); }); describe('extractHostname', () => { it('returns the lowercase short hostname from an FQDN', () => { expect(extractHostname('VFI-807-005-168.us000782.stores.ae.com')).toBe('vfi-807-005-168'); }); it('passes a bare IP through (no dot-split semantics for our matching)', () => { // Bare IPs still split on the first dot — that's fine because the // matcher only uses this as a "starts-with" hint anyway. expect(extractHostname('192.168.10.45')).toBe('192'); }); it('returns null for falsy input', () => { expect(extractHostname(null)).toBeNull(); expect(extractHostname(undefined)).toBeNull(); expect(extractHostname('')).toBeNull(); }); }); describe('getClientStatus', () => { it('returns correct emojis and fallback', () => { expect(getClientStatus({ status: 'Online' })).toBe('✅ Online'); expect(getClientStatus({ status: 'Offline' })).toBe('❌ Offline'); expect(getClientStatus(null)).toBe('❓ Unknown'); expect(getClientStatus(undefined)).toBe('❓ Unknown'); }); }); describe('formatLastSeen', () => { it('handles missing value', () => { expect(formatLastSeen(null)).toBe('N/A'); expect(formatLastSeen(undefined)).toBe('N/A'); }); it('formats recent times', () => { const justNow = new Date(); expect(formatLastSeen(justNow)).toBe('Just now'); const tenMin = new Date(Date.now() - 10 * 60 * 1000); expect(formatLastSeen(tenMin)).toMatch(/10 min ago/); }); }); describe('buildMerakiClientLink', () => { const network = { id: 'N_123', name: 'Store 305', url: 'https://example.com/n/ABC123' }; it('builds a client link when possible', () => { const client = { id: 'c99' }; const link = buildMerakiClientLink(network, client); expect(link).toContain('/manage/clients/c99/overview'); }); it('returns empty string on bad input', () => { expect(buildMerakiClientLink(null, {})).toBe(''); expect(buildMerakiClientLink(network, null)).toBe(''); }); }); });