matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
333 lines • 16.4 kB
JavaScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SCENE_AREA_ID_MIN } from '../../constants/index.js';
import { RoborockAuthenticateApi } from '../../roborockCommunication/api/authClient.js';
import { RoborockService } from '../../services/roborockService.js';
import { asPartial } from '../testUtils.js';
describe('RoborockService - Comprehensive Coverage', () => {
let service;
let mockLogger;
let mockContainer;
let mockAuthCoordinator;
let mockDeviceService;
let mockAreaService;
let mockMessageService;
let mockPollingService;
let mockPersist;
let mockConfigManager;
beforeEach(() => {
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
notice: vi.fn(),
};
mockAuthCoordinator = {
authenticate: vi.fn(),
};
mockDeviceService = {
listDevices: vi.fn(),
getHomeDataForUpdating: vi.fn(),
setIotApi: vi.fn(),
setAuthentication: vi.fn(),
stopService: vi.fn(),
};
mockAreaService = {
setSelectedAreas: vi.fn(),
getSelectedAreas: vi.fn(),
setSupportedAreas: vi.fn(),
setSupportedAreaIndexMap: vi.fn(),
setSupportedRoutines: vi.fn(),
getSupportedAreas: vi.fn(),
getSupportedAreasIndexMap: vi.fn(),
getSupportedRoutines: vi.fn(),
getMapInfo: vi.fn(),
getRoomMap: vi.fn(),
getScenes: vi.fn(),
startScene: vi.fn(),
setMessageClient: vi.fn(),
setIotApi: vi.fn(),
clearAll: vi.fn(),
};
mockMessageService = {
getCleanModeData: vi.fn(),
getRoomIdFromMap: vi.fn(),
changeCleanMode: vi.fn(),
startClean: vi.fn().mockResolvedValue(undefined),
pauseClean: vi.fn(),
stopAndGoHome: vi.fn(),
resumeClean: vi.fn(),
stopClean: vi.fn().mockResolvedValue(undefined),
playSoundToLocate: vi.fn(),
customGet: vi.fn(),
customSend: vi.fn(),
setIotApi: vi.fn(),
getMapInfo: vi.fn(),
clearAll: vi.fn(),
getSerialNumber: vi.fn().mockResolvedValue('SN-ABC'),
};
mockPollingService = {
activateDeviceNotifyOverLocal: vi.fn(),
requestStatusOnce: vi.fn().mockResolvedValue(undefined),
stopPolling: vi.fn(),
shutdown: vi.fn(),
};
mockContainer = {
getAuthenticationCoordinator: vi.fn().mockReturnValue(mockAuthCoordinator),
getDeviceManagementService: vi.fn().mockReturnValue(mockDeviceService),
getAreaManagementService: vi.fn().mockReturnValue(mockAreaService),
getMessageRoutingService: vi.fn().mockReturnValue(mockMessageService),
getPollingService: vi.fn().mockReturnValue(mockPollingService),
getConnectionService: vi.fn().mockReturnValue({
initializeMessageClient: vi.fn(),
initializeMessageClientForLocal: vi.fn().mockResolvedValue(false),
setDeviceNotify: vi.fn(),
}),
setUserData: vi.fn(),
getIotApi: vi.fn(),
synchronizeMessageClients: vi.fn(),
destroy: vi.fn(),
};
mockPersist = {
init: vi.fn().mockResolvedValue(undefined),
getItem: vi.fn().mockResolvedValue(undefined),
setItem: vi.fn().mockResolvedValue(undefined),
};
mockConfigManager = {
username: 'testuser',
password: 'testpass',
verificationCode: undefined,
authenticationMethod: 'Password',
};
service = new RoborockService({
refreshInterval: 10,
baseUrl: 'https://api.roborock.com',
persist: mockPersist,
configManager: mockConfigManager,
container: mockContainer,
toastMessage: vi.fn(),
}, mockLogger, mockConfigManager);
});
describe('Constructor and Initialization', () => {
it('should create service with default factories when not provided', () => {
const defaultService = new RoborockService({
refreshInterval: 10,
baseUrl: 'https://api.roborock.com',
persist: mockPersist,
configManager: mockConfigManager,
container: mockContainer,
toastMessage: vi.fn(),
}, mockLogger, mockConfigManager);
expect(defaultService).toBeInstanceOf(RoborockService);
});
it('should create service without injected container', () => {
const defaultService = new RoborockService({
refreshInterval: 10,
baseUrl: 'https://api.roborock.com',
persist: mockPersist,
configManager: mockConfigManager,
toastMessage: vi.fn(),
}, mockLogger, mockConfigManager);
expect(defaultService).toBeInstanceOf(RoborockService);
});
it('should create service with custom factories', () => {
const customAuthFactory = vi.fn((logger) => new RoborockAuthenticateApi(logger));
const customService = new RoborockService({
authenticateApiFactory: customAuthFactory,
refreshInterval: 20000,
baseUrl: 'https://custom-api.roborock.com',
persist: mockPersist,
configManager: mockConfigManager,
container: mockContainer,
toastMessage: vi.fn(),
}, mockLogger, mockConfigManager);
expect(customService).toBeInstanceOf(RoborockService);
});
it('should create service with custom base URL', () => {
const customService = new RoborockService({
refreshInterval: 15000,
baseUrl: 'https://another-api.roborock.com',
persist: mockPersist,
configManager: mockConfigManager,
container: mockContainer,
toastMessage: vi.fn(),
}, mockLogger, mockConfigManager);
expect(customService).toBeInstanceOf(RoborockService);
});
it('activateDeviceNotify delegates to device service', () => {
const device = { duid: 'test-duid' };
// Test that method exists and doesn't throw with basic call
expect(() => {
service.activateDeviceNotify(device);
}).not.toThrow();
});
});
describe('Authentication', () => {
it('should authenticate with password flow', async () => {
vi.mocked(mockAuthCoordinator.authenticate)?.mockResolvedValue(asPartial({
nickname: 'Test User',
username: 'testuser',
}));
const result = await service.authenticate();
expect(result.shouldContinue).toBe(true);
expect(result.userData).toBeDefined();
expect(mockAuthCoordinator.authenticate).toHaveBeenCalledWith('Password', {
username: 'testuser',
password: 'testpass',
verificationCode: undefined,
});
});
it('should authenticate with verification code flow', async () => {
const mockConfigWith2FA = {
username: 'testuser',
password: 'testpass',
verificationCode: '123456',
authenticationMethod: 'VerificationCode',
};
const serviceWith2FA = new RoborockService({
refreshInterval: 10,
baseUrl: 'https://api.roborock.com',
persist: mockPersist,
configManager: mockConfigWith2FA,
container: mockContainer,
toastMessage: vi.fn(),
}, mockLogger, mockConfigWith2FA);
vi.mocked(mockAuthCoordinator.authenticate)?.mockResolvedValue(asPartial({
nickname: 'Test User',
username: 'testuser',
}));
const result = await serviceWith2FA.authenticate();
expect(result.shouldContinue).toBe(true);
expect(result.userData).toBeDefined();
expect(mockAuthCoordinator.authenticate).toHaveBeenCalledWith('VerificationCode', {
username: 'testuser',
password: 'testpass',
verificationCode: '123456',
});
});
it('should return false when authentication fails', async () => {
vi.mocked(mockAuthCoordinator.authenticate)?.mockRejectedValue(new Error('Auth failed'));
const result = await service.authenticate();
expect(result.shouldContinue).toBe(false);
expect(result.userData).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith('Authentication failed: Auth failed');
});
it('should return false when userData is undefined', async () => {
vi.mocked(mockAuthCoordinator.authenticate)?.mockResolvedValue(undefined);
const result = await service.authenticate();
expect(result.shouldContinue).toBe(false);
expect(result.userData).toBeUndefined();
expect(mockLogger.info).toHaveBeenCalledWith('Authentication incomplete. Further action required (e.g., 2FA).');
});
});
describe('getCustomAPI', () => {
it('should throw error when iotApi is not initialized', async () => {
vi.mocked(mockContainer.getIotApi)?.mockReturnValue(undefined);
await expect(service.getCustomAPI('/test')).rejects.toThrow('IoT API not initialized. Please login first.');
});
it('should call iotApi.getCustom when iotApi is initialized', async () => {
const mockIotApi = { getCustom: vi.fn().mockResolvedValue({ data: 'test' }) };
vi.mocked(mockContainer.getIotApi)?.mockReturnValue(asPartial(mockIotApi));
const result = await service.getCustomAPI('/test');
expect(result).toEqual({ data: 'test' });
expect(mockIotApi.getCustom).toHaveBeenCalledWith('/test');
});
});
describe('stopClean', () => {
it('should delegate to messageRoutingService.stopClean', async () => {
await service.stopClean('duid-1');
expect(mockMessageService.stopClean).toHaveBeenCalledWith('duid-1');
});
});
describe('getSerialNumber', () => {
it('should delegate to messageRoutingService.getSerialNumber', async () => {
vi.mocked(mockMessageService.getSerialNumber)?.mockResolvedValue('SN-ABC');
const result = await service.getSerialNumber('duid-1');
expect(result).toBe('SN-ABC');
expect(mockMessageService.getSerialNumber).toHaveBeenCalledWith('duid-1');
});
});
describe('requestDeviceStatusOnce', () => {
it('should delegate to pollingService.requestStatusOnce', async () => {
vi.mocked(mockPollingService.requestStatusOnce)?.mockResolvedValue(undefined);
await service.requestDeviceStatusOnce('duid-1');
expect(mockPollingService.requestStatusOnce).toHaveBeenCalledWith('duid-1');
});
});
describe('setDeviceNotify', () => {
it('should set deviceNotify and delegate to pollingService and connectionService', () => {
const cb = vi.fn();
service.setDeviceNotify(cb);
expect(service.deviceNotify).toBe(cb);
});
});
describe('initializeMessageClientForLocal', () => {
it('should delegate to connectionService and return its result', async () => {
const mockConnectionService = vi.mocked(mockContainer.getConnectionService)?.();
vi.mocked(mockConnectionService?.initializeMessageClientForLocal)?.mockResolvedValue(true);
const result = await service.initializeMessageClientForLocal(asPartial({ duid: 'duid-1' }));
expect(result).toBe(true);
});
});
describe('startClean - buildCleanCommand branches', () => {
const duid = 'test-duid';
function makeArea(areaId, mapId, name = '') {
return asPartial({
areaId,
mapId,
areaInfo: { locationInfo: { locationName: name, floorNumber: 0, areaType: 1 }, landmarkInfo: null },
});
}
it('should send routine command when selected area matches a routine', async () => {
const routineAreaId = SCENE_AREA_ID_MIN + 10;
vi.mocked(mockAreaService.getSelectedAreas)?.mockReturnValue([routineAreaId]);
vi.mocked(mockAreaService.getSupportedRoutines)?.mockReturnValue([makeArea(routineAreaId, 0)]);
vi.mocked(mockAreaService.getSupportedAreas)?.mockReturnValue([]);
vi.mocked(mockAreaService.getSupportedAreasIndexMap)?.mockReturnValue(undefined);
await service.startClean(duid);
expect(mockMessageService.startClean).toHaveBeenCalledWith(duid, expect.objectContaining({ type: 'routine', routineId: 10 }));
});
it('should send global command when no areas are selected', async () => {
vi.mocked(mockAreaService.getSelectedAreas)?.mockReturnValue([]);
vi.mocked(mockAreaService.getSupportedRoutines)?.mockReturnValue([]);
vi.mocked(mockAreaService.getSupportedAreas)?.mockReturnValue([makeArea(1, 1), makeArea(2, 1)]);
vi.mocked(mockAreaService.getSupportedAreasIndexMap)?.mockReturnValue(undefined);
await service.startClean(duid);
expect(mockMessageService.startClean).toHaveBeenCalledWith(duid, { type: 'global' });
});
it('should send global command when all rooms in active map are selected', async () => {
const room1 = makeArea(1, 1);
const room2 = makeArea(2, 1);
const indexMap = asPartial({ getRoomId: vi.fn().mockImplementation((id) => id + 100) });
vi.mocked(mockAreaService.getSelectedAreas)?.mockReturnValue([1, 2]);
vi.mocked(mockAreaService.getSupportedRoutines)?.mockReturnValue([]);
vi.mocked(mockAreaService.getSupportedAreas)?.mockReturnValue([room1, room2]);
vi.mocked(mockAreaService.getSupportedAreasIndexMap)?.mockReturnValue(indexMap);
await service.startClean(duid);
expect(mockMessageService.startClean).toHaveBeenCalledWith(duid, { type: 'global' });
});
it('should send room command when specific rooms are selected (not all)', async () => {
const room1 = makeArea(1, 1);
const room2 = makeArea(2, 1);
const room3 = makeArea(3, 1);
const indexMap = asPartial({ getRoomId: vi.fn().mockImplementation((id) => id + 100) });
vi.mocked(mockAreaService.getSelectedAreas)?.mockReturnValue([1]);
vi.mocked(mockAreaService.getSupportedRoutines)?.mockReturnValue([]);
vi.mocked(mockAreaService.getSupportedAreas)?.mockReturnValue([room1, room2, room3]);
vi.mocked(mockAreaService.getSupportedAreasIndexMap)?.mockReturnValue(indexMap);
await service.startClean(duid);
expect(mockMessageService.startClean).toHaveBeenCalledWith(duid, expect.objectContaining({ type: 'room', roomIds: [101] }));
});
it('should send global command when selected area has no matching room in indexMap', async () => {
const room1 = makeArea(1, 1);
const indexMap = asPartial({ getRoomId: vi.fn().mockReturnValue(undefined) });
vi.mocked(mockAreaService.getSelectedAreas)?.mockReturnValue([1]);
vi.mocked(mockAreaService.getSupportedRoutines)?.mockReturnValue([]);
vi.mocked(mockAreaService.getSupportedAreas)?.mockReturnValue([room1]);
vi.mocked(mockAreaService.getSupportedAreasIndexMap)?.mockReturnValue(indexMap);
await service.startClean(duid);
expect(mockMessageService.startClean).toHaveBeenCalledWith(duid, { type: 'global' });
});
});
});
//# sourceMappingURL=roborockService.test.js.map