/** * Tests for OnlineStatusService */ import { OnlineStatusService, calculateOnlineStatus, formatLastSeen, getStatusColor, getStatusLabel, calculateOverallStatus, DEFAULT_THRESHOLDS, DeviceStatusInfo, } from '../onlineStatusService'; import { api } from '../api'; // Mock the api module jest.mock('../api', () => ({ api: { getDevicesForBeneficiary: jest.fn(), }, })); const mockApi = api as jest.Mocked; describe('OnlineStatusService', () => { describe('calculateOnlineStatus', () => { it('should return online for recent timestamps (< 5 minutes)', () => { const now = new Date(); const twoMinutesAgo = new Date(now.getTime() - 2 * 60 * 1000); expect(calculateOnlineStatus(twoMinutesAgo)).toBe('online'); }); it('should return online for just now', () => { const now = new Date(); expect(calculateOnlineStatus(now)).toBe('online'); }); it('should return warning for timestamps between 5 and 60 minutes', () => { const now = new Date(); const thirtyMinutesAgo = new Date(now.getTime() - 30 * 60 * 1000); expect(calculateOnlineStatus(thirtyMinutesAgo)).toBe('warning'); }); it('should return offline for timestamps older than 60 minutes', () => { const now = new Date(); const twoHoursAgo = new Date(now.getTime() - 120 * 60 * 1000); expect(calculateOnlineStatus(twoHoursAgo)).toBe('offline'); }); it('should respect custom thresholds', () => { const now = new Date(); const tenMinutesAgo = new Date(now.getTime() - 10 * 60 * 1000); // With default thresholds, 10 minutes should be warning expect(calculateOnlineStatus(tenMinutesAgo, DEFAULT_THRESHOLDS)).toBe('warning'); // With custom thresholds, 10 minutes could be online expect(calculateOnlineStatus(tenMinutesAgo, { onlineMaxMinutes: 15, warningMaxMinutes: 60 })).toBe('online'); }); }); describe('formatLastSeen', () => { it('should return "Just now" for timestamps less than 1 minute ago', () => { const now = new Date(); expect(formatLastSeen(now)).toBe('Just now'); const thirtySecondsAgo = new Date(now.getTime() - 30 * 1000); expect(formatLastSeen(thirtySecondsAgo)).toBe('Just now'); }); it('should return minutes for timestamps less than 1 hour ago', () => { const now = new Date(); const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); expect(formatLastSeen(fiveMinutesAgo)).toBe('5 min ago'); const fortyFiveMinutesAgo = new Date(now.getTime() - 45 * 60 * 1000); expect(formatLastSeen(fortyFiveMinutesAgo)).toBe('45 min ago'); }); it('should return hours for timestamps less than 24 hours ago', () => { const now = new Date(); const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); expect(formatLastSeen(oneHourAgo)).toBe('1 hour ago'); const threeHoursAgo = new Date(now.getTime() - 3 * 60 * 60 * 1000); expect(formatLastSeen(threeHoursAgo)).toBe('3 hours ago'); }); it('should return days for timestamps older than 24 hours', () => { const now = new Date(); const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); expect(formatLastSeen(oneDayAgo)).toBe('1 day ago'); const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000); expect(formatLastSeen(threeDaysAgo)).toBe('3 days ago'); }); }); describe('getStatusColor', () => { it('should return green for online', () => { expect(getStatusColor('online')).toBe('#22c55e'); }); it('should return amber for warning', () => { expect(getStatusColor('warning')).toBe('#f59e0b'); }); it('should return red for offline', () => { expect(getStatusColor('offline')).toBe('#ef4444'); }); }); describe('getStatusLabel', () => { it('should return proper labels', () => { expect(getStatusLabel('online')).toBe('Online'); expect(getStatusLabel('warning')).toBe('Warning'); expect(getStatusLabel('offline')).toBe('Offline'); }); }); describe('calculateOverallStatus', () => { const createDevice = (status: 'online' | 'warning' | 'offline'): DeviceStatusInfo => ({ deviceId: `device-${Math.random()}`, deviceName: 'Test Device', status, lastSeen: new Date(), lastSeenMinutesAgo: 0, lastSeenFormatted: 'Just now', }); it('should return offline for empty array', () => { expect(calculateOverallStatus([])).toBe('offline'); }); it('should return online when all devices are online', () => { const devices = [ createDevice('online'), createDevice('online'), createDevice('online'), ]; expect(calculateOverallStatus(devices)).toBe('online'); }); it('should return warning when at least one device has warning (but none offline)', () => { const devices = [ createDevice('online'), createDevice('warning'), createDevice('online'), ]; expect(calculateOverallStatus(devices)).toBe('warning'); }); it('should return offline when at least one device is offline', () => { const devices = [ createDevice('online'), createDevice('warning'), createDevice('offline'), ]; expect(calculateOverallStatus(devices)).toBe('offline'); }); it('should prioritize offline over warning', () => { const devices = [ createDevice('offline'), createDevice('warning'), ]; expect(calculateOverallStatus(devices)).toBe('offline'); }); }); describe('OnlineStatusService class', () => { let service: OnlineStatusService; beforeEach(() => { service = new OnlineStatusService(); jest.clearAllMocks(); }); describe('setThresholds / getThresholds', () => { it('should update thresholds', () => { service.setThresholds({ onlineMaxMinutes: 10 }); const thresholds = service.getThresholds(); expect(thresholds.onlineMaxMinutes).toBe(10); expect(thresholds.warningMaxMinutes).toBe(60); // Default }); }); describe('checkBeneficiaryStatus', () => { it('should return error when API fails', async () => { mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({ ok: false, error: { message: 'Network error' }, } as any); const result = await service.checkBeneficiaryStatus('ben-123'); expect(result.ok).toBe(false); expect(result.error).toBe('Network error'); }); it('should return empty status when no devices', async () => { mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({ ok: true, data: [], } as any); const result = await service.checkBeneficiaryStatus('ben-123'); expect(result.ok).toBe(true); expect(result.data?.totalDevices).toBe(0); expect(result.data?.devices).toHaveLength(0); expect(result.data?.overallStatus).toBe('offline'); // No devices = offline }); it('should correctly transform devices', async () => { const now = new Date(); const mockDevices = [ { deviceId: 'dev-1', wellId: 123, mac: 'AABBCCDDEEFF', name: 'WP_123_ddeeff', status: 'online', lastSeen: now, beneficiaryId: 'ben-123', deploymentId: 456, source: 'api', }, { deviceId: 'dev-2', wellId: 456, mac: '112233445566', name: 'WP_456_445566', status: 'offline', lastSeen: new Date(now.getTime() - 2 * 60 * 60 * 1000), // 2 hours ago beneficiaryId: 'ben-123', deploymentId: 456, source: 'api', }, ]; mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({ ok: true, data: mockDevices, } as any); const result = await service.checkBeneficiaryStatus('ben-123'); expect(result.ok).toBe(true); expect(result.data?.totalDevices).toBe(2); expect(result.data?.onlineCount).toBe(1); expect(result.data?.offlineCount).toBe(1); expect(result.data?.overallStatus).toBe('offline'); // One offline = overall offline }); it('should use cached data when available and valid', async () => { const mockDevices = [ { deviceId: 'dev-1', name: 'WP_123_ddeeff', status: 'online', lastSeen: new Date(), wellId: 123, mac: 'AABBCCDDEEFF', }, ]; mockApi.getDevicesForBeneficiary.mockResolvedValue({ ok: true, data: mockDevices, } as any); // First call - should hit API await service.checkBeneficiaryStatus('ben-123'); expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(1); // Second call - should use cache await service.checkBeneficiaryStatus('ben-123'); expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(1); // Force refresh - should hit API again await service.checkBeneficiaryStatus('ben-123', true); expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(2); }); }); describe('checkMultipleBeneficiaries', () => { it('should check multiple beneficiaries', async () => { mockApi.getDevicesForBeneficiary.mockResolvedValue({ ok: true, data: [], } as any); const results = await service.checkMultipleBeneficiaries(['ben-1', 'ben-2', 'ben-3']); expect(results.size).toBe(3); expect(results.get('ben-1')?.ok).toBe(true); expect(results.get('ben-2')?.ok).toBe(true); expect(results.get('ben-3')?.ok).toBe(true); }); }); describe('isDeviceOnline', () => { it('should return true for online device', async () => { mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({ ok: true, data: [ { deviceId: 'dev-1', name: 'Test', status: 'online', lastSeen: new Date(), }, ], } as any); const isOnline = await service.isDeviceOnline('ben-123', 'dev-1'); expect(isOnline).toBe(true); }); it('should return false for offline device', async () => { const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000); mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({ ok: true, data: [ { deviceId: 'dev-1', name: 'Test', status: 'offline', lastSeen: twoHoursAgo, }, ], } as any); const isOnline = await service.isDeviceOnline('ben-123', 'dev-1'); expect(isOnline).toBe(false); }); it('should return false for non-existent device', async () => { mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({ ok: true, data: [], } as any); const isOnline = await service.isDeviceOnline('ben-123', 'non-existent'); expect(isOnline).toBe(false); }); }); describe('getStatusSummary', () => { it('should return correct summary', async () => { const now = new Date(); mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({ ok: true, data: [ { deviceId: 'd1', name: 'D1', status: 'online', lastSeen: now }, { deviceId: 'd2', name: 'D2', status: 'online', lastSeen: now }, { deviceId: 'd3', name: 'D3', status: 'warning', lastSeen: new Date(now.getTime() - 30 * 60 * 1000) }, { deviceId: 'd4', name: 'D4', status: 'offline', lastSeen: new Date(now.getTime() - 2 * 60 * 60 * 1000) }, ], } as any); const summary = await service.getStatusSummary('ben-123'); expect(summary).not.toBeNull(); expect(summary?.total).toBe(4); expect(summary?.online).toBe(2); expect(summary?.warning).toBe(1); expect(summary?.offline).toBe(1); }); it('should return null when API fails', async () => { mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({ ok: false, error: 'Failed', } as any); const summary = await service.getStatusSummary('ben-123'); expect(summary).toBeNull(); }); }); describe('clearCache', () => { it('should clear all cached data', async () => { mockApi.getDevicesForBeneficiary.mockResolvedValue({ ok: true, data: [], } as any); // Populate cache await service.checkBeneficiaryStatus('ben-1'); await service.checkBeneficiaryStatus('ben-2'); expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(2); // Verify cache is being used await service.checkBeneficiaryStatus('ben-1'); expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(2); // Clear cache service.clearCache(); // Now should hit API again await service.checkBeneficiaryStatus('ben-1'); expect(mockApi.getDevicesForBeneficiary).toHaveBeenCalledTimes(3); }); }); describe('getCachedStatus', () => { it('should return cached status if valid', async () => { mockApi.getDevicesForBeneficiary.mockResolvedValueOnce({ ok: true, data: [{ deviceId: 'd1', name: 'D1', status: 'online', lastSeen: new Date() }], } as any); // Populate cache await service.checkBeneficiaryStatus('ben-123'); const cached = service.getCachedStatus('ben-123'); expect(cached).not.toBeUndefined(); expect(cached?.beneficiaryId).toBe('ben-123'); }); it('should return undefined for non-cached beneficiary', () => { const cached = service.getCachedStatus('non-existent'); expect(cached).toBeUndefined(); }); }); }); });