UNPKG

matterbridge-roborock-vacuum-plugin

Version:
322 lines 14.1 kB
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { DeviceError, DeviceNotFoundError } from '../../errors/index.js'; import { ProtocolVersion } from '../../roborockCommunication/enums/index.js'; import { DeviceCategory } from '../../roborockCommunication/models/deviceCategory.js'; import { DeviceModel, Protocol } from '../../roborockCommunication/models/index.js'; import { DeviceManagementService } from '../../services/deviceManagementService.js'; import { createMockAuthApi, createMockIotApi } from '../helpers/testUtils.js'; import { makeLogger } from '../testUtils.js'; describe('DeviceManagementService', () => { let deviceService; let mockLogger; let mockLoginApi; let mockIotApi; const mockUserData = { username: 'test-user', uid: 'test-uid', tokentype: 'Bearer', token: 'test-token', rruid: 'rr-uid', region: 'us', countrycode: 'US', country: 'United States', nickname: 'Test User', rriot: { r: { r: 'test-r', a: 'test-a', m: 'test-m', l: 'test-l' }, u: 'test-user', s: 'test-secret', h: 'test-host', k: 'test-key', }, }; const mockDevice = { duid: 'device-123', name: 'Test Vacuum', productId: 'prod-456', localKey: 'local-key-789', pv: 'A01', sn: 'SN12345', fv: '1.0.0', rrHomeId: 12345, scenes: [], deviceStatus: { [Protocol.battery]: 85, }, serialNumber: 'SN12345', activeTime: 0, createTime: 0, online: true, schema: [], mapInfos: undefined, specs: { id: 'device-123', firmwareVersion: '1.0.0', serialNumber: 'SN12345', model: DeviceModel.QREVO_EDGE_5V1, protocol: ProtocolVersion.V1, category: DeviceCategory.VacuumCleaner, batteryLevel: 85, hasRealTimeConnection: true, }, store: { userData: mockUserData, localKey: 'local-key-789', pv: 'A01', model: DeviceModel.QREVO_EDGE_5V1, homeData: { id: 12345, name: 'Test Home', products: [], devices: [], receivedDevices: [], rooms: [ { id: 1, name: 'Living Room', }, ], }, }, }; const mockHomeData = { id: 12345, name: 'Test Home', products: [ { id: 'prod-456', name: 'Test Product', model: 's5_max', category: 'vacuum', schema: [], }, ], devices: [mockDevice], receivedDevices: [], rooms: [ { id: 1, name: 'Living Room', }, ], }; beforeEach(() => { vi.clearAllMocks(); mockLogger = makeLogger(); mockIotApi = createMockIotApi({ getHomev2: vi.fn().mockResolvedValue(mockHomeData), getHomev3: vi.fn(), getHome: vi.fn(), getScenes: vi.fn().mockResolvedValue([]), getHomeWithProducts: vi.fn().mockResolvedValue(mockHomeData), }); mockLoginApi = createMockAuthApi({ getBasicHomeInfo: vi.fn().mockResolvedValue({ rrHomeId: 12345 }), }); deviceService = new DeviceManagementService(mockLogger, mockLoginApi, mockUserData); deviceService.setIotApi(mockIotApi); }); afterEach(() => { vi.clearAllMocks(); vi.clearAllTimers(); }); describe('initialization and configuration', () => { it('should set authentication data correctly', () => { const newUserData = { ...mockUserData, uid: 'new-uid' }; deviceService.setAuthentication(newUserData); expect(deviceService).toBeDefined(); }); }); describe('listDevices', () => { it('should throw error when not authenticated', async () => { const unauthenticatedService = new DeviceManagementService(mockLogger, mockLoginApi, undefined); await expect(unauthenticatedService.listDevices()).rejects.toThrow('Not authenticated. Please login first.'); }); it('should throw DeviceNotFoundError when no home found', async () => { mockLoginApi.getBasicHomeInfo = vi.fn().mockResolvedValue(undefined); await expect(deviceService.listDevices()).rejects.toThrow(DeviceNotFoundError); await expect(deviceService.listDevices()).rejects.toThrow('No home found for user'); }); it('should throw error when home details missing rrHomeId', async () => { mockLoginApi.getBasicHomeInfo.mockResolvedValue(undefined); await expect(deviceService.listDevices()).rejects.toThrow(DeviceNotFoundError); }); it('should throw DeviceError when homeData cannot be retrieved', async () => { mockIotApi.getHomeWithProducts = vi.fn().mockResolvedValue(undefined); await expect(deviceService.listDevices()).rejects.toThrow(DeviceError); await expect(deviceService.listDevices()).rejects.toThrow('Failed to retrieve home data'); }); it('should successfully list devices with all data', async () => { const result = await deviceService.listDevices(); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ duid: 'device-123', name: 'Test Vacuum', rrHomeId: 12345, scenes: [], specs: { id: 'device-123', firmwareVersion: '1.0.0', serialNumber: 'SN12345', category: 'vacuum', batteryLevel: 85, }, store: { userData: mockUserData, localKey: 'local-key-789', pv: 'A01', model: 's5_max', }, }); expect(mockLoginApi.getBasicHomeInfo).toHaveBeenCalled(); expect(mockIotApi.getHomeWithProducts).toHaveBeenCalledWith(12345); expect(mockIotApi.getScenes).toHaveBeenCalledWith(12345); expect(mockLogger.notice).toHaveBeenCalledWith('Found 1 devices'); }); it('should use receivedDevices when devices array is empty', async () => { const homeDataWithReceivedDevices = { ...mockHomeData, devices: [], receivedDevices: [mockDevice], }; mockIotApi.getHomev2 = vi.fn().mockResolvedValue(homeDataWithReceivedDevices); const result = await deviceService.listDevices(); expect(result).toHaveLength(1); expect(result[0].duid).toBe('device-123'); }); it('should filter scenes by device entityId', async () => { const mockScenes = [ { id: 1, name: 'Test Scene 1', param: JSON.stringify({ action: { items: [{ entityId: 'device-123' }, { entityId: 'other-device' }], }, }), enabled: true, extra: undefined, type: 'auto', }, { id: 2, name: 'Test Scene 2', param: JSON.stringify({ action: { items: [{ entityId: 'other-device' }], }, }), enabled: true, extra: undefined, type: 'auto', }, ]; mockIotApi.getScenes.mockResolvedValue(mockScenes); const result = await deviceService.listDevices(); expect(result[0].scenes).toHaveLength(1); expect(result[0]?.scenes?.[0]?.id).toBe(1); }); it('should fallback battery level to 100 if not present', async () => { const deviceWithoutBattery = { ...mockDevice, deviceStatus: {} }; const homeDataNoBattery = { ...mockHomeData, devices: [deviceWithoutBattery] }; mockIotApi.getHomeWithProducts = vi.fn().mockResolvedValue(homeDataNoBattery); const result = await deviceService.listDevices(); expect(result[0].specs.batteryLevel).toBe(100); }); it('should handle API errors and wrap them in DeviceError', async () => { mockIotApi.getHomeWithProducts = vi.fn().mockRejectedValue(new Error('API Error')); await expect(deviceService.listDevices()).rejects.toThrow(DeviceError); await expect(deviceService.listDevices()).rejects.toThrow('Failed to retrieve device list'); }); }); it('should return undefined when not authenticated', async () => { const unauthenticatedService = new DeviceManagementService(mockLogger, mockLoginApi, undefined); const result = await unauthenticatedService.getHomeDataForUpdating(12345); expect(result).toBeUndefined(); expect(mockLogger.error).toHaveBeenCalledWith('Not authenticated'); }); it('should return undefined when homeData cannot be retrieved', async () => { mockIotApi.getHomev2 = vi.fn().mockResolvedValue(undefined); const result = await deviceService.getHomeDataForUpdating(12345); expect(result).toBeUndefined(); }); it('should return homeData with enriched devices', async () => { const result = await deviceService.getHomeDataForUpdating(12345); expect(result).toBeDefined(); expect(result?.devices).toBeDefined(); expect(result?.devices).toHaveLength(1); expect(result?.devices?.[0]).toMatchObject({ duid: 'device-123', rrHomeId: 12345, specs: expect.objectContaining({ id: 'device-123', firmwareVersion: '1.0.0', serialNumber: 'SN12345', }), }); }); it('should fallback to v3 API for rooms if v2 rooms are empty', async () => { const homeDataNoRooms = { ...mockHomeData, rooms: [] }; const v3Rooms = [{ id: 2, name: 'Kitchen' }]; mockIotApi.getHomev2.mockResolvedValue(homeDataNoRooms); mockIotApi.getHomev3 = vi.fn().mockResolvedValue({ ...mockHomeData, rooms: v3Rooms }); const result = await deviceService.getHomeDataForUpdating(12345); expect(mockIotApi.getHomev3).toHaveBeenCalledWith(12345); expect(result?.rooms).toEqual(v3Rooms); }); it('should fallback to v1 API for rooms if v2 and v3 rooms are empty', async () => { const homeDataNoRooms = { ...mockHomeData, rooms: [] }; const v1Rooms = [{ id: 3, name: 'Bedroom' }]; mockIotApi.getHomev2.mockResolvedValue(homeDataNoRooms); mockIotApi.getHomev3.mockResolvedValue({ ...mockHomeData, rooms: [] }); mockIotApi.getHome.mockResolvedValue({ ...mockHomeData, rooms: v1Rooms }); const result = await deviceService.getHomeDataForUpdating(12345); expect(mockIotApi.getHome).toHaveBeenCalledWith(12345); expect(result?.rooms).toEqual(v1Rooms); }); it('should keep empty rooms if all API versions fail to provide rooms', async () => { const homeDataNoRooms = { ...mockHomeData, rooms: [] }; mockIotApi.getHomev2.mockResolvedValue(homeDataNoRooms); mockIotApi.getHomev3 = vi.fn().mockResolvedValue(undefined); mockIotApi.getHome.mockResolvedValue(undefined); const result = await deviceService.getHomeDataForUpdating(12345); expect(result?.rooms).toEqual([]); expect(result?.rooms).toEqual([]); expect(result?.rooms).toEqual([]); }); it('should keep empty rooms if v3 returns no rooms and v1 fails', async () => { const homeDataNoRooms = { ...mockHomeData, rooms: [] }; mockIotApi.getHomev2.mockResolvedValue(homeDataNoRooms); mockIotApi.getHomev3.mockResolvedValue({ ...mockHomeData, rooms: [] }); mockIotApi.getHome.mockResolvedValue(undefined); const result = await deviceService.getHomeDataForUpdating(12345); expect(result?.rooms).toEqual([]); }); it('should use receivedDevices when devices array is empty', async () => { const homeDataWithReceived = { ...mockHomeData, devices: [], receivedDevices: [mockDevice], }; mockIotApi.getHomev2.mockResolvedValue(homeDataWithReceived); const result = await deviceService.getHomeDataForUpdating(12345); expect(result?.devices).toHaveLength(1); expect(result?.devices?.[0]?.duid).toBe('device-123'); }); it('should handle API errors gracefully', async () => { mockIotApi.getHomev2.mockRejectedValue(new Error('API Error')); const result = await deviceService.getHomeDataForUpdating(12345); expect(result).toBeUndefined(); expect(mockLogger.error).toHaveBeenCalledWith('Failed to get home data for updating:', expect.any(Error)); }); describe('Authentication data handling', () => { it('should update authentication when setAuthentication is called', () => { const newUserData = { ...mockUserData, uid: 'new-uid' }; // Call setAuthentication deviceService.setAuthentication(newUserData); // Verify the method was called successfully expect(newUserData.uid).toBe('new-uid'); expect(newUserData.rruid).toBe(mockUserData.rruid); }); }); }); //# sourceMappingURL=deviceManagementService.test.js.map