UNPKG

node-miio

Version:

Control Mi Home devices, such as Mi Robot Vacuums, Mi Air Purifiers, Mi Smart Home Gateway (Aqara) and more

338 lines (307 loc) 12.1 kB
'use strict'; const TOKEN = 'e381c97af00985577e8e6a3d94732bbe'; const loggerMock = jest.fn().mockImplementation((a, b) => { // Uncomment when debugging tests // console.debug(a, b); }); const debugMock = jest.fn().mockImplementation((label) => { return loggerMock; }); jest.doMock('debug', () => debugMock); function createParentMock() { return { socket: { send: jest .fn() .mockImplementation((data, size, length, port, address, cb) => { cb(); }), }, }; } const DeviceInfo = require('./device_info'); const Packet = require('./packet'); describe('DeviceInfo', () => { jest.useFakeTimers(); describe('constructor', () => { test('creates a new DeviceInfo without id', () => { new DeviceInfo({}, null, 'localhost', 1234); expect(debugMock).toHaveBeenCalledWith('thing:miio:pending'); }); test('creates a new DeviceInfo with id', () => { new DeviceInfo({}, 'MY-ID', 'localhost', 1234); expect(debugMock).toHaveBeenCalledWith('thing:miio:MY-ID'); }); }); describe('token', () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); test('initial token does not exist', () => { expect(device.token).toBeNull(); }); test('token stored', () => { expect(device.tokenChanged).toBe(false); device.token = Buffer.from('my-token'); expect(device.tokenChanged).toBe(true); expect(device.token.toString('utf8')).toBe('my-token'); }); }); describe('enrich', () => { test('fails if the id is not populated yet', async () => { const device = new DeviceInfo({}, null, 'localhost', 1234); await expect(device.enrich()).rejects.toThrow( 'Device has no identifier yet, handshake needed' ); }); test('returns undefined because everything is properly initialised already', async () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); device.token = TOKEN; device.tokenChanged = false; device.model = {}; await expect(device.enrich()).resolves.toBeUndefined(); }); test('happy path', async () => { const model = { test: 1 }; const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); device.token = TOKEN; device.call = jest.fn().mockImplementation(async () => ({ model })); await expect(device.enrich()).resolves.toBeUndefined(); expect(device.tokenChanged).toBe(false); expect(device.model).toBe(model); expect(device.enriched).toBe(true); expect(device.enrichPromise).toBeNull(); }); test('errors with \'missing-token\'', async () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); device.token = TOKEN; device.enrichPromise = Promise.reject({ code: 'missing-token' }); await expect(device.enrich()).rejects.toStrictEqual({ code: 'missing-token', device, }); expect(device.tokenChanged).toBe(true); expect(device.enriched).toBe(true); expect(device.enrichPromise).toBeNull(); }); test('errors with \'connection-failure\'', async () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); device.token = TOKEN; device.tokenChanged = false; device.enrichPromise = Promise.reject('Something went terribly wrong'); await expect(device.enrich()).rejects.toThrow( 'Could not connect to device, token might be wrong' ); expect(device.tokenChanged).toBe(false); expect(device.enriched).toBe(true); expect(device.enrichPromise).toBeNull(); }); test('errors with \'missing-token\' custom', async () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); device.enrichPromise = Promise.reject('Something went terribly wrong'); await expect(device.enrich()).rejects.toThrow( 'Could not connect to device, token needs to be specified' ); expect(device.tokenChanged).toBe(false); expect(device.enriched).toBe(true); expect(device.enrichPromise).toBeNull(); }); }); describe('handshake', () => { test('happy-path: runs the handshake', async () => { const packet = new Packet(false); const parent = createParentMock(); const device = new DeviceInfo(parent, 'MY-ID', 'localhost', 1234); device.token = TOKEN; packet.token = TOKEN; parent.socket.send.mockImplementation( (data, size, length, port, address, cb) => { cb(); packet.data = null; device.onMessage(packet.raw); } ); const token = await device.handshake(); expect(token.toString()).toBe(TOKEN); // Second call is not triggered const token2 = await device.handshake(); expect(token2.toString()).toBe(TOKEN); }); test('fails because \'missing-token\'', async () => { const packet = new Packet(false); const parent = createParentMock(); const device = new DeviceInfo(parent, 'MY-ID', 'localhost', 1234); packet.token = TOKEN; parent.socket.send.mockImplementation( (data, size, length, port, address, cb) => { cb(); packet.data = null; device.onMessage(packet.raw); } ); await expect(device.handshake()).rejects.toThrow( 'Could not connect to device, token needs to be specified' ); }); test('fails because to send the handshake request', async () => { const packet = new Packet(false); const parent = createParentMock(); const device = new DeviceInfo(parent, 'MY-ID', 'localhost', 1234); packet.token = TOKEN; parent.socket.send.mockImplementation( (data, size, length, port, address, cb) => { cb(new Error('Something went terribly wrong')); } ); await expect(device.handshake()).rejects.toThrow( 'Something went terribly wrong' ); }); test('timesout after 2 secs', async () => { const parent = createParentMock(); const device = new DeviceInfo(parent, 'MY-ID', 'localhost', 1234); device.token = TOKEN; const promise = device.handshake(); jest.advanceTimersByTime(2 * 1000); await expect(promise).rejects.toThrow( 'Could not connect to device, handshake timeout' ); }); }); describe('onMessage', () => { test('swallows error to parse the message', async () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); expect(device.onMessage('test')).toBeUndefined(); }); test('handshake message but no handler', async () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); const packet = new Packet(); device.token = TOKEN; packet.token = TOKEN; packet.data = null; expect(device.onMessage(packet.raw)).toBeUndefined(); }); test('swallows error because data is not a valid JSON', async () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); const packet = new Packet(); device.token = TOKEN; packet.token = TOKEN; packet.data = Buffer.from('test').fill(0); expect(device.onMessage(packet.raw)).toBeUndefined(); }); test('does not call the promise resolvers because there\'s none', async () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); const packet = new Packet(); device.token = TOKEN; packet.token = TOKEN; const response = { id: '1', result: 'something' }; packet.data = Buffer.from(JSON.stringify(response)); expect(device.onMessage(packet.raw)).toBeUndefined(); }); test('calls the promise resolver when there\'s a result', async () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); const packet = new Packet(); device.token = TOKEN; packet.token = TOKEN; const response = { id: '1', result: 'something' }; const promiseResolvers = { resolve: jest.fn(), reject: jest.fn() }; device.promises.set('1', promiseResolvers); packet.data = Buffer.from(JSON.stringify(response)); expect(device.onMessage(packet.raw)).toBeUndefined(); expect(promiseResolvers.resolve).toHaveBeenCalledWith('something'); }); test('calls the promise resolver\'s reject when there\'s an error', async () => { const device = new DeviceInfo({}, 'MY-ID', 'localhost', 1234); const packet = new Packet(); device.token = TOKEN; packet.token = TOKEN; const response = { id: '1', error: 'something' }; const promiseResolvers = { resolve: jest.fn(), reject: jest.fn() }; device.promises.set('1', promiseResolvers); packet.data = Buffer.from(JSON.stringify(response)); expect(device.onMessage(packet.raw)).toBeUndefined(); expect(promiseResolvers.reject).toHaveBeenCalledWith('something'); }); }); describe('call', () => { test('happy-path: sends a method \'miIO.info\'', async () => { const packet = new Packet(false); const parent = createParentMock(); const device = new DeviceInfo(parent, 'MY-ID', 'localhost', 1234); device.token = TOKEN; packet.token = TOKEN; parent.socket.send.mockImplementation( (data, size, length, port, address, cb) => { cb(); // Return what you get embedded in `result` packet.raw = Buffer.from(data); const str = packet.data; if (str !== null) { const json = JSON.parse(str); packet.data = JSON.stringify(Object.assign({}, json, {result: json})); } device.onMessage(packet.raw); } ); const response = await device.call('miIO.info'); expect(response).toStrictEqual({ id: device.lastId, method: 'miIO.info', params: [], }); }); test('timeout error', async () => { const packet = new Packet(false); const parent = createParentMock(); const device = new DeviceInfo(parent, 'MY-ID', 'localhost', 1234); packet.token = TOKEN; device.token = TOKEN; device.lastId = 10000; parent.socket.send.mockImplementation( (data, size, length, port, address, cb) => { cb(); // Reply to handshakes only packet.raw = data; const str = packet.data; if (str === null) { device.onMessage(packet.raw); } else { jest.advanceTimersByTime(2 * 1000); } } ); const promise = device.call('miIO.info', [], { retries: 2 }); await expect(promise).rejects.toThrow('Call to device timed out'); expect(device.lastId).toBe(101); }); describe('known errors', () => { const packet = new Packet(false); packet.token = TOKEN; test.each` code | message | expected ${'-5001'} | ${'invalid_arg'} | ${'Invalid argument'} ${'-5001'} | ${'Test msg'} | ${'Test msg'} ${'-5005'} | ${'params error'} | ${'Invalid argument'} ${'-5005'} | ${'Test msg'} | ${'Test msg'} ${'-10000'} | ${'Test msg'} | ${'Method `miIO.info` is not supported'} ${'OTHER'} | ${'Test msg'} | ${'Test msg'} `('$code | $message', async ({ code, message, expected }) => { const parent = createParentMock(); const device = new DeviceInfo(parent, 'MY-ID', 'localhost', 1234); device.token = TOKEN; parent.socket.send.mockImplementation( (data, size, length, port, address, cb) => { cb(); // Return what you get embedded in `result` packet.raw = data; const str = packet.data; if (str !== null) { const json = JSON.parse(str); packet.data = JSON.stringify(Object.assign({}, json, { error: { code, message } })); } device.onMessage(packet.raw); } ); await expect(device.call('miIO.info')).rejects.toThrow(expected); }); }); }); });