UNPKG

matterbridge-roborock-vacuum-plugin

Version:
357 lines 18.1 kB
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { RoborockMatterbridgePlatform } from '../module.js'; import { asPartial, createMockLocalStorage, createMockLogger, createMockMatterbridge } from './helpers/testUtils.js'; function createMockConfig(overrides = {}) { return { name: 'TestPlatform', authentication: { username: 'test', region: 'US', forceAuthentication: false, password: 'test', authenticationMethod: 'Password', }, pluginConfiguration: { whiteList: [], sanitizeSensitiveLogs: false, enableMultipleMap: false, unregisterOnShutdown: false, refreshInterval: 60, debug: false, enableServerMode: false, }, advancedFeature: { enableAdvancedFeature: false, settings: { clearStorageOnStartup: false, showRoutinesAsRoom: false, includeDockStationStatus: false, includeVacuumErrorStatus: false, forceRunAtDefault: false, useVacationModeToSendVacuumToDock: false, enableCleanModeMapping: false, cleanModeSettings: { vacuuming: { fanMode: 'Silent', mopRouteMode: 'Standard' }, mopping: { waterFlowMode: 'Low', mopRouteMode: 'Standard', distanceOff: 0 }, vacmop: { fanMode: 'Silent', waterFlowMode: 'Low', mopRouteMode: 'Standard', distanceOff: 0 }, }, overrideMatterConfiguration: false, matterOverrideSettings: { matterVendorName: 'xxx', matterVendorId: 123, matterProductName: 'yy', matterProductId: 456, }, enableEmailNotification: false, emailNotificationSettings: {}, }, }, ...overrides, }; } describe('RoborockMatterbridgePlatform - orchestration', () => { let mockLogger; let mockMatterbridge; let mockPersist; let platform; beforeEach(() => { vi.useFakeTimers(); mockLogger = createMockLogger(); mockMatterbridge = createMockMatterbridge(); mockPersist = createMockLocalStorage({ init: vi.fn().mockResolvedValue(undefined), clear: vi.fn().mockResolvedValue(undefined), }); platform = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig()); // Replace the FilterLogger-wrapped log with the mock logger so spy assertions work platform.log = mockLogger; platform.ready = Promise.resolve(); platform.persist = mockPersist; vi.spyOn(platform.configManager, 'validateConfig'); Object.defineProperty(platform, 'clearSelect', { value: vi.fn().mockResolvedValue(undefined), configurable: true }); Object.defineProperty(platform, 'unregisterAllDevices', { value: vi.fn().mockResolvedValue(undefined), configurable: true, }); }); afterEach(() => { vi.clearAllMocks(); vi.clearAllTimers(); vi.useRealTimers(); }); describe('onStart', () => { it('should complete startup successfully', async () => { vi.spyOn(platform.discovery, 'discoverDevices').mockImplementation(async () => { platform.discovery.roborockService = asPartial({}); return true; }); const onConfigureSpy = vi.spyOn(platform.configurator, 'onConfigureDevice').mockResolvedValue(undefined); await platform.onStart('test reason'); expect(platform.log.notice).toHaveBeenCalledWith('onStart called with reason:', 'test reason'); expect(platform.configManager.validateConfig).toHaveBeenCalled(); expect(onConfigureSpy).toHaveBeenCalled(); expect(platform.log.notice).toHaveBeenCalledWith('onStart finished'); expect(platform.state.isStartupCompleted).toBe(true); }); it('should handle missing reason parameter', async () => { vi.spyOn(platform.discovery, 'discoverDevices').mockImplementation(async () => { platform.discovery.roborockService = asPartial({}); return true; }); vi.spyOn(platform.configurator, 'onConfigureDevice').mockResolvedValue(undefined); await platform.onStart(); expect(platform.log.notice).toHaveBeenCalledWith('onStart called with reason:', 'none'); }); it('should fail when config validation fails', async () => { platform.configManager.validateConfig = vi.fn().mockReturnValue(false); await platform.onStart('test'); expect(platform.log.error).toHaveBeenCalledWith('"username" (email address) is required in the config'); expect(platform.state.isStartupCompleted).toBe(false); }); it('should fail when device discovery fails', async () => { vi.spyOn(platform.discovery, 'discoverDevices').mockResolvedValue(false); await platform.onStart('test'); expect(platform.log.error).toHaveBeenCalledWith('Device discovery failed to start.'); expect(platform.state.isStartupCompleted).toBe(false); }); it('should wait for platform to be ready', async () => { let readyResolver = () => { }; const readyPromise = new Promise((resolve) => { readyResolver = resolve; }); platform.ready = readyPromise; vi.spyOn(platform.discovery, 'discoverDevices').mockImplementation(async () => { platform.discovery.roborockService = asPartial({}); return true; }); vi.spyOn(platform.configurator, 'onConfigureDevice').mockResolvedValue(undefined); const clearSelectSpy = platform.clearSelect; const startPromise = platform.onStart('test'); expect(clearSelectSpy).not.toHaveBeenCalled(); readyResolver(); await startPromise; expect(clearSelectSpy).toHaveBeenCalled(); }); it('should initialize persistence storage', async () => { vi.spyOn(platform.discovery, 'discoverDevices').mockImplementation(async () => { platform.discovery.roborockService = asPartial({}); return true; }); vi.spyOn(platform.configurator, 'onConfigureDevice').mockResolvedValue(undefined); await platform.onStart('test'); expect(mockPersist.init).toHaveBeenCalled(); }); it('should return early when clearStorageOnStartup is enabled', async () => { Object.defineProperty(platform.configManager, 'isClearStorageOnStartupEnabled', { get: () => true, configurable: true, }); await platform.onStart('test'); expect(mockPersist.init).toHaveBeenCalled(); expect(platform.configManager.validateConfig).not.toHaveBeenCalled(); }); it('should clear persistence when alwaysExecuteAuthentication is true', async () => { Object.defineProperty(platform.configManager, 'alwaysExecuteAuthentication', { get: () => true, configurable: true, }); vi.spyOn(platform.discovery, 'discoverDevices').mockImplementation(async () => { platform.discovery.roborockService = asPartial({}); return true; }); vi.spyOn(platform.configurator, 'onConfigureDevice').mockResolvedValue(undefined); await platform.onStart('test'); expect(mockPersist.clear).toHaveBeenCalled(); }); it('should fail when roborockService is undefined after discovery', async () => { vi.spyOn(platform.discovery, 'discoverDevices').mockImplementation(async () => { platform.discovery.roborockService = undefined; return true; }); await platform.onStart('test'); expect(platform.log.error).toHaveBeenCalledWith('Initializing: RoborockService is undefined'); expect(platform.state.isStartupCompleted).toBe(false); }); }); describe('onConfigure', () => { it('should set up polling interval when startup is completed', async () => { platform.state.setStartupCompleted(true); platform.platformRunner = asPartial({ requestHomeData: vi.fn().mockResolvedValue(undefined) }); await platform.onConfigure(); expect(platform.log.notice).toHaveBeenCalledWith('onConfigure called'); vi.advanceTimersByTime(61000); expect(platform.platformRunner.requestHomeData).toHaveBeenCalled(); }); it('should not set up interval when startup is not completed', async () => { platform.state.setStartupCompleted(false); platform.platformRunner = asPartial({ requestHomeData: vi.fn().mockResolvedValue(undefined) }); await platform.onConfigure(); expect(platform.log.notice).toHaveBeenCalledWith('onConfigure called'); vi.advanceTimersByTime(61000); expect(platform.platformRunner.requestHomeData).not.toHaveBeenCalled(); }); it('should handle requestHomeData errors gracefully', async () => { platform.state.setStartupCompleted(true); platform.platformRunner = asPartial({ requestHomeData: vi.fn().mockRejectedValue(new Error('Network error')), }); await platform.onConfigure(); await vi.advanceTimersByTimeAsync(61000); expect(platform.log.error).toHaveBeenCalledWith('requestHomeData (interval) failed: Network error'); }); it('should handle non-Error exceptions in requestHomeData', async () => { platform.state.setStartupCompleted(true); platform.platformRunner = asPartial({ requestHomeData: vi.fn().mockRejectedValue('string error'), }); await platform.onConfigure(); await vi.advanceTimersByTimeAsync(61000); expect(platform.log.error).toHaveBeenCalledWith('requestHomeData (interval) failed: string error'); }); it('should clear storage and unregister when clearStorageOnStartup is enabled', async () => { Object.defineProperty(platform.configManager, 'isClearStorageOnStartupEnabled', { get: () => true, configurable: true, }); Object.defineProperty(platform.configManager, 'rawConfig', { get: () => ({ authentication: { verificationCode: 'abc' }, advancedFeature: { settings: { clearStorageOnStartup: true } }, }), configurable: true, }); Object.defineProperty(platform, 'onConfigChanged', { value: vi.fn().mockResolvedValue(undefined), configurable: true, }); await platform.onConfigure(); expect(platform.log.warn).toHaveBeenCalledWith('Clearing persistence storage as per configuration.'); expect(mockPersist.clear).toHaveBeenCalled(); expect(platform.unregisterAllDevices).toHaveBeenCalled(); }); it('should handle error during clearStorage flow', async () => { Object.defineProperty(platform.configManager, 'isClearStorageOnStartupEnabled', { get: () => true, configurable: true, }); mockPersist.clear = vi.fn().mockRejectedValue(new Error('Storage error')); await platform.onConfigure(); expect(platform.log.error).toHaveBeenCalledWith(expect.stringContaining('Error clearing persistence storage')); }); }); describe('onShutdown', () => { it('should clean up interval and reset state', async () => { platform.state.setStartupCompleted(true); await platform.onShutdown('test reason'); expect(platform.log.notice).toHaveBeenCalledWith('onShutdown called with reason:', 'test reason'); expect(platform.state.isStartupCompleted).toBe(false); }); it('should handle missing reason parameter', async () => { await platform.onShutdown(); expect(platform.log.notice).toHaveBeenCalledWith('onShutdown called with reason:', 'none'); }); it('should clear polling interval if it exists', async () => { platform.state.setStartupCompleted(true); platform.platformRunner = asPartial({ requestHomeData: vi.fn().mockResolvedValue(undefined), burstPolling: asPartial({ stopAllBurstPolling: vi.fn() }), }); await platform.onConfigure(); await platform.onShutdown('test'); vi.advanceTimersByTime(61000); expect(platform.platformRunner.requestHomeData).not.toHaveBeenCalled(); }); it('should stop roborock service when available', async () => { const mockService = asPartial({ stopService: vi.fn(), }); platform.roborockService = mockService; await platform.onShutdown('test'); expect(mockService.stopService).toHaveBeenCalled(); expect(platform.roborockService).toBeUndefined(); }); it('should handle undefined roborock service', async () => { platform.roborockService = undefined; await platform.onShutdown('test'); expect(platform.roborockService).toBeUndefined(); }); it('should unregister devices when configured', async () => { const p = new RoborockMatterbridgePlatform(mockMatterbridge, mockLogger, createMockConfig({ pluginConfiguration: { whiteList: [], sanitizeSensitiveLogs: false, enableMultipleMap: false, unregisterOnShutdown: true, refreshInterval: 60, debug: false, enableServerMode: false, }, })); p.ready = Promise.resolve(); Object.defineProperty(p, 'unregisterAllDevices', { value: vi.fn().mockResolvedValue(undefined), configurable: true, }); await p.onShutdown('test'); expect(p.unregisterAllDevices).toHaveBeenCalledWith(500); }); it('should not unregister devices when not configured', async () => { await platform.onShutdown('test'); expect(platform.unregisterAllDevices).not.toHaveBeenCalled(); }); it('should handle shutdown when no interval was set', async () => { await platform.onShutdown('test'); expect(platform.state.isStartupCompleted).toBe(false); }); it('should call stopAllBurstPolling on shutdown when platformRunner is set', async () => { platform.platformRunner = asPartial({ requestHomeData: vi.fn().mockResolvedValue(undefined), burstPolling: asPartial({ stopAllBurstPolling: vi.fn() }), }); await platform.onShutdown('test'); expect(platform.platformRunner.burstPolling.stopAllBurstPolling).toHaveBeenCalled(); }); }); describe('onConfigure - email notification', () => { it('should send test email notification when enabled and roborockService is available', async () => { platform.state.setStartupCompleted(true); platform.platformRunner = asPartial({ requestHomeData: vi.fn().mockResolvedValue(undefined) }); const mockSendTestEmail = vi.fn().mockResolvedValue(undefined); platform.discovery.roborockService = asPartial({ sendTestEmailNotification: mockSendTestEmail, }); Object.defineProperty(platform.configManager, 'isEmailNotificationEnabled', { get: () => true, configurable: true, }); await platform.onConfigure(); expect(mockSendTestEmail).toHaveBeenCalledOnce(); }); it('should not send test email when email notification is disabled', async () => { platform.state.setStartupCompleted(true); platform.platformRunner = asPartial({ requestHomeData: vi.fn().mockResolvedValue(undefined) }); const mockSendTestEmail = vi.fn().mockResolvedValue(undefined); platform.discovery.roborockService = asPartial({ sendTestEmailNotification: mockSendTestEmail, }); Object.defineProperty(platform.configManager, 'isEmailNotificationEnabled', { get: () => false, configurable: true, }); await platform.onConfigure(); expect(mockSendTestEmail).not.toHaveBeenCalled(); }); it('should not send test email when roborockService is undefined', async () => { platform.state.setStartupCompleted(true); platform.platformRunner = asPartial({ requestHomeData: vi.fn().mockResolvedValue(undefined) }); platform.discovery.roborockService = undefined; Object.defineProperty(platform.configManager, 'isEmailNotificationEnabled', { get: () => true, configurable: true, }); await platform.onConfigure(); expect(platform.log.notice).toHaveBeenCalledWith('onConfigure called'); }); }); }); //# sourceMappingURL=module.orchestration.test.js.map