matterbridge-roborock-vacuum-plugin
Version:
Matterbridge Roborock Vacuum Plugin
358 lines • 17.3 kB
JavaScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('axios-logger', () => ({
requestLogger: (request) => request,
responseLogger: () => undefined,
errorLogger: (error) => Promise.reject(error),
}));
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { RoborockIoTApi } from '../../../roborockCommunication/api/iotClient.js';
import { DeviceModel } from '../../../roborockCommunication/models/deviceModel.js';
import { asPartial, asType, createMockLogger } from '../../helpers/testUtils.js';
const makeUserData = () => ({ rriot: { r: { a: 'https://api.example' }, u: 'uid', s: 's', h: 'hmac' } });
describe('RoborockIoTApi (additional)', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('getHome returns result when API returns a result', async () => {
const userdata = makeUserData();
const logger = createMockLogger();
const expected = { id: 1, products: [], devices: [], receivedDevices: [], rooms: [] };
const mockApi = {
get: vi.fn().mockResolvedValue({ data: { result: expected } }),
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
};
vi.spyOn(axios, 'create').mockImplementation(() => mockApi);
const api = new RoborockIoTApi(asType(userdata), asType(logger));
const res = await api.getHome(1);
expect(res).toEqual(expected);
expect(mockApi.get).toHaveBeenCalledWith('user/homes/1');
});
it('getHomeWithProducts uses v3 API and merges devices when product requires v3', async () => {
const userdata = makeUserData();
const logger = createMockLogger();
const homeV1 = {
id: 2,
products: [{ model: 'roborock.vacuum.ss07' }],
devices: [{ duid: 'a' }],
receivedDevices: [{ duid: 'ra' }],
rooms: [],
};
const homeV3 = {
id: 2,
products: [],
devices: [{ duid: 'b' }],
receivedDevices: [{ duid: 'rb' }],
rooms: [],
};
const mockApi = {
get: vi.fn((url) => {
if (url === 'user/homes/2')
return Promise.resolve({ data: { result: homeV1 } });
if (url === 'v3/user/homes/2')
return Promise.resolve({ data: { result: homeV3 } });
return Promise.resolve({ data: {} });
}),
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
};
vi.spyOn(axios, 'create').mockImplementation(() => mockApi);
const api = new RoborockIoTApi(asType(userdata), asType(logger));
const res = await api.getHomeWithProducts(2);
expect(res).toBeDefined();
expect(res?.devices).toEqual([{ duid: 'a' }, { duid: 'b' }]);
expect(res?.receivedDevices).toEqual([{ duid: 'ra' }, { duid: 'rb' }]);
expect(mockApi.get).toHaveBeenCalledWith('user/homes/2');
expect(mockApi.get).toHaveBeenCalledWith('v3/user/homes/2');
});
it('getHomeWithProducts fills rooms from v2 when v1 has no rooms', async () => {
const userdata = makeUserData();
const logger = createMockLogger();
const homeV1 = {
id: 3,
products: [],
devices: [],
receivedDevices: [],
rooms: [],
};
const homeV2 = {
id: 3,
products: [],
devices: [],
receivedDevices: [],
rooms: [{ id: 11, name: 'Living' }],
};
const mockApi = {
get: vi.fn((url) => {
if (url === 'user/homes/3')
return Promise.resolve({ data: { result: homeV1 } });
if (url === 'v2/user/homes/3')
return Promise.resolve({ data: { result: homeV2 } });
return Promise.resolve({ data: {} });
}),
interceptors: { request: { use: vi.fn() }, response: { use: vi.fn() } },
};
vi.spyOn(axios, 'create').mockImplementation(() => mockApi);
const api = new RoborockIoTApi(asType(userdata), asType(logger));
const res = await api.getHomeWithProducts(3);
expect(res).toBeDefined();
expect(res?.rooms).toEqual(homeV2.rooms);
});
});
describe('RoborockIoTApi', () => {
describe('getHomeWithProducts', () => {
it('returns home data as-is if no v3 or v2 logic triggered', async () => {
const home = asPartial({
id: 1,
name: 'Home1',
products: [asPartial({ model: DeviceModel.S5 })],
devices: [],
receivedDevices: [],
rooms: [asPartial({ id: 1 })],
});
vi.spyOn(api, 'getHome').mockResolvedValue(home);
const result = await api.getHomeWithProducts(1);
expect(result).toBe(home);
});
it('merges v3 devices/receivedDevices if v3 logic triggered', async () => {
const home = {
id: 1,
name: 'Home1',
products: [{ model: 'roborock.vacuum.ss07' }],
devices: [{ duid: 'a' }],
receivedDevices: [{ duid: 'b' }],
rooms: [asPartial({ id: 1 })],
};
const v3 = {
devices: [{ duid: 'c' }],
receivedDevices: [{ duid: 'd' }],
rooms: [asPartial({ id: 2 })],
};
vi.spyOn(api, 'getHome').mockResolvedValue(asPartial({
id: 1,
name: 'Home1',
products: [asPartial({ model: DeviceModel.Q10_S5_PLUS })],
devices: [asPartial({ duid: 'a' })],
receivedDevices: [asPartial({ duid: 'b' })],
rooms: [asPartial({ id: 1 })],
}));
vi.spyOn(api, 'getHomev3').mockResolvedValue(asPartial({
devices: [asPartial({ duid: 'c' })],
receivedDevices: [asPartial({ duid: 'd' })],
rooms: [asPartial({ id: 2 })],
}));
const result = await api.getHomeWithProducts(1);
expect(result?.devices).toContainEqual({ duid: 'a' });
expect(result?.devices).toContainEqual({ duid: 'c' });
expect(result?.receivedDevices).toContainEqual({ duid: 'b' });
expect(result?.receivedDevices).toContainEqual({ duid: 'd' });
});
it('throws if v3 API is needed but fails', async () => {
vi.spyOn(api, 'getHome').mockResolvedValue(asPartial({ id: 1, name: 'Home1', products: [asPartial({ model: DeviceModel.Q10_S5_PLUS })] }));
vi.spyOn(api, 'getHomev3').mockResolvedValue(undefined);
await expect(api.getHomeWithProducts(1)).rejects.toThrow('Failed to retrieve the home data from v3 API');
});
it('merges v2 rooms if rooms are empty and v2 returns rooms', async () => {
vi.spyOn(api, 'getHome').mockResolvedValue(asPartial({
id: 1,
name: 'Home1',
products: [asPartial({ model: 'other.model' })],
devices: [],
receivedDevices: [],
rooms: [],
}));
vi.spyOn(api, 'getHomev2').mockResolvedValue(asPartial({ id: 99, rooms: [asPartial({ id: 99 })] }));
const result = await api.getHomeWithProducts(1);
expect(result?.rooms).toContainEqual({ id: 99 });
});
it('returns home if rooms are empty and v2 returns no rooms', async () => {
vi.spyOn(api, 'getHome').mockResolvedValue(asPartial({
id: 1,
name: 'Home1',
products: [asPartial({ model: 'other.model' })],
devices: [],
receivedDevices: [],
rooms: [],
}));
vi.spyOn(api, 'getHomev2').mockResolvedValue(undefined);
vi.spyOn(api, 'getHomev3').mockResolvedValue(undefined);
const result = await api.getHomeWithProducts(1);
expect(result).toBeDefined();
expect(result?.rooms).toEqual([]);
});
it('merges v3 rooms if v2 has no rooms but v3 does', async () => {
vi.spyOn(api, 'getHome').mockResolvedValue(asPartial({
id: 1,
name: 'Home1',
products: [asPartial({ model: 'other.model' })],
devices: [],
receivedDevices: [],
rooms: [],
}));
vi.spyOn(api, 'getHomev2').mockResolvedValue(asPartial({ id: 99, rooms: [] }));
vi.spyOn(api, 'getHomev3').mockResolvedValue(asPartial({ id: 77, rooms: [asPartial({ id: 77, name: 'Garage' })] }));
const result = await api.getHomeWithProducts(1);
expect(result?.rooms).toContainEqual({ id: 77, name: 'Garage' });
});
it('returns undefined and logs if getHome returns undefined', async () => {
vi.spyOn(api, 'getHome').mockResolvedValue(undefined);
const result = await api.getHomeWithProducts(1);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith('[getHomeWithProducts Step 1] Failed to retrieve the home data');
expect(mockLogger.error).toHaveBeenCalledWith('[getHomeWithProducts Step 2] Failed to retrieve the home data');
expect(mockLogger.error).toHaveBeenCalledWith('[getHomeWithProducts Step 3] Failed to retrieve the home data');
});
});
let mockLogger;
let mockAxiosInstance;
let mockUserData;
let api;
beforeEach(() => {
mockLogger = createMockLogger();
const mockAxiosFactory = asPartial({
create: vi.fn((config) => {
const axiosInstance = axios.create(config);
mockAxiosInstance = new MockAdapter(axiosInstance);
return axiosInstance;
}),
});
mockUserData = {
rriot: {
r: { a: 'http://base.url' },
u: 'uid',
s: 'sid',
h: 'hkey',
},
};
api = new RoborockIoTApi(asType(mockUserData), mockLogger, mockAxiosFactory);
});
afterEach(() => {
mockAxiosInstance.reset();
vi.restoreAllMocks();
});
it('should initialize logger and api', () => {
expect(api.logger).toBe(mockLogger);
expect(api['api']).toBeDefined();
});
it('getHome should return home if result exists', async () => {
const home = { id: 1, name: 'Home1' };
mockAxiosInstance.onGet('user/homes/1').reply(200, { result: home });
const result = await api.getHome(1);
expect(result).toEqual(home);
});
it('getHome should log error and return undefined if result missing', async () => {
mockAxiosInstance.onGet('user/homes/1').reply(200, {});
const result = await api.getHome(1);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith('[getHome] Failed to retrieve the home data');
});
it('getHomev2 should return home if result exists', async () => {
const home = { id: 2, name: 'Home2' };
mockAxiosInstance.onGet('v2/user/homes/2').reply(200, { result: home });
const result = await api.getHomev2(2);
expect(result).toEqual(home);
});
it('getHomev3 should return home if result exists', async () => {
const home = { id: 3, name: 'Home3' };
mockAxiosInstance.onGet('v3/user/homes/3').reply(200, { result: home });
const result = await api.getHomev3(3);
expect(result).toEqual(home);
});
it('getScenes should return scenes if result exists', async () => {
const scenes = [{ id: 1 }, { id: 2 }];
mockAxiosInstance.onGet('user/scene/home/1').reply(200, { result: scenes });
const result = await api.getScenes(1);
expect(result).toEqual(scenes);
});
it('getScenes should log error and return undefined if result missing', async () => {
mockAxiosInstance.onGet('user/scene/home/1').reply(200, {});
const result = await api.getScenes(1);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith('[getScenes] Failed to retrieve scenes');
});
it('startScene should return result if present', async () => {
mockAxiosInstance.onPost('user/scene/1/execute').reply(200, { success: true });
const result = await api.startScene(1);
expect(result).toBe(true);
});
it('startScene should log error and return undefined if result missing', async () => {
mockAxiosInstance.onPost('user/scene/1/execute').reply(200, {});
const result = await api.startScene(1);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith('[startScene] Failed to execute scene');
});
it('getCustom should return result if present', async () => {
mockAxiosInstance.onGet('/custom/url').reply(200, { result: 'custom' });
const result = await api.getCustom('/custom/url');
expect(result).toBe('custom');
});
it('getCustom should log error and return undefined if result missing', async () => {
mockAxiosInstance.onGet('/custom/url').reply(200, {});
const result = await api.getCustom('/custom/url');
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith('[getCustom] Failed to execute custom request');
});
it('should create axios instance with timeout, redirects and httpsAgent', () => {
const instance = api['api'];
expect(instance).toBeDefined();
expect(instance.defaults).toMatchObject({ baseURL: 'http://base.url', timeout: 10000, maxRedirects: 5 });
expect(instance.defaults.httpsAgent).toBeDefined();
});
it('getHomev2 should log error and return undefined on exception', async () => {
mockAxiosInstance.onGet('v2/user/homes/2').networkErrorOnce();
const result = await api.getHomev2(2);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[getHomev2] Failed'));
});
it('startScene should log error and return undefined on exception', async () => {
mockAxiosInstance.onPost('user/scene/5/execute').networkErrorOnce();
const result = await api.startScene(5);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[startScene] Failed'));
});
it('getHomev2 handles ETIMEDOUT error and logs', async () => {
mockAxiosInstance.onGet('v2/user/homes/2').timeoutOnce();
const result = await api.getHomev2(2);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[getHomev2] Failed'));
});
it('getHomev2 handles ECONNRESET error and logs', async () => {
mockAxiosInstance.onGet('v2/user/homes/2').networkErrorOnce();
const result = await api.getHomev2(2);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[getHomev2] Failed'));
});
it('getHome should log error and return undefined on exception', async () => {
mockAxiosInstance.onGet('user/homes/1').networkErrorOnce();
const result = await api.getHome(1);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[getHome] Failed'));
});
it('getHomev3 should log error and return undefined if result missing', async () => {
mockAxiosInstance.onGet('v3/user/homes/3').reply(200, {});
const result = await api.getHomev3(3);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith('[getHomev3] Failed to retrieve the home data');
});
it('getHomev3 should log error and return undefined on exception', async () => {
mockAxiosInstance.onGet('v3/user/homes/3').networkErrorOnce();
const result = await api.getHomev3(3);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[getHomev3] Failed'));
});
it('getScenes should log error and return undefined on exception', async () => {
mockAxiosInstance.onGet('user/scene/home/1').networkErrorOnce();
const result = await api.getScenes(1);
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[getScenes] Failed'));
});
it('getCustom should log error and return undefined on exception', async () => {
mockAxiosInstance.onGet('/custom/url').networkErrorOnce();
const result = await api.getCustom('/custom/url');
expect(result).toBeUndefined();
expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining('[getCustom] Failed'));
});
});
//# sourceMappingURL=iotClient.test.js.map