UNPKG

matterbridge-roborock-vacuum-plugin

Version:
230 lines 12.3 kB
import CRC32 from 'crc-32'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { MessageContext, Protocol } from '../../../roborockCommunication/models/index.js'; import { MessageDeserializer } from '../../../roborockCommunication/protocol/deserializers/messageDeserializer.js'; import { MessageSerializerFactory } from '../../../roborockCommunication/protocol/serializers/messageSerializerFactory.js'; import { asType, mkUser } from '../../helpers/testUtils.js'; import { makeLogger } from '../../testUtils.js'; describe('MessageDeserializer', () => { let logger; beforeEach(() => { logger = makeLogger(); }); it('returns ResponseMessage for protocols without payload', () => { const context = new MessageContext(mkUser()); const des = new MessageDeserializer(context, logger); // build header buffer: version(3) + seq(4) + nonce(4) + timestamp(4) + protocol(2) const buf = Buffer.alloc(17); buf.write('A01', 0, 3, 'ascii'); buf.writeUInt32BE(1, 3); // seq buf.writeUInt32BE(2, 7); // nonce buf.writeUInt32BE(3, 11); // timestamp buf.writeUInt16BE(Protocol.hello_response, 15); const res = des.deserialize('duid', buf, 'from'); expect(res.isForProtocol(Protocol.hello_response)).toBe(true); }); it('throws on unsupported protocol version', () => { const context = new MessageContext(mkUser()); const des = new MessageDeserializer(context, logger); const buf = Buffer.alloc(17); buf.write('BAD', 0, 3, 'ascii'); buf.writeUInt32BE(0, 3); buf.writeUInt32BE(0, 7); buf.writeUInt32BE(0, 11); buf.writeUInt16BE(Protocol.rpc_response, 15); expect(() => des.deserialize('duid', buf, 'from')).toThrow(/unknown protocol/); }); it('parses rpc_response and returns a body with parsed dps', async () => { vi.spyOn(MessageSerializerFactory.prototype, 'getMessageSerializer').mockReturnValue({ encode: (_payload, _localKey, _timestamp, _sequence, _nonce) => Buffer.alloc(0), decode: (_payload) => Buffer.from(JSON.stringify({ dps: { '102': JSON.stringify({ x: 1 }) } })), }); const context = new MessageContext(asType({ rriot: { k: 'k' } })); context.registerDevice('duid', 'local', 'A01', 9); const des = new MessageDeserializer(context, logger); const header = Buffer.alloc(17); header.write('A01', 0, 3, 'ascii'); header.writeUInt32BE(1, 3); header.writeUInt32BE(2, 7); header.writeUInt32BE(3, 11); header.writeUInt16BE(Protocol.rpc_response, 15); const decodedPayload = Buffer.from('encrypted'); const contentLen = Buffer.alloc(2); contentLen.writeUInt16BE(decodedPayload.length, 0); const preCrc = Buffer.concat([header, contentLen, decodedPayload]); const crc = CRC32.buf(preCrc) >>> 0; const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc, 0); const full = Buffer.concat([preCrc, crcBuf]); const res = des.deserialize('duid', full, 'from'); expect(res.isForProtocol(Protocol.rpc_response)).toBe(true); const body = res.body; expect(body).toBeDefined(); expect(body?.data['102']).toBeDefined(); const item = body?.data['102']; const parsed = typeof item === 'string' ? JSON.parse(item) : JSON.parse(JSON.stringify(item)); expect(parsed.x).toBe(1); }); it('handles unknown protocol and returns ResponseMessage without throwing', async () => { vi.spyOn(MessageSerializerFactory.prototype, 'getMessageSerializer').mockReturnValue({ encode: (_payload, _localKey, _timestamp, _sequence, _nonce) => Buffer.alloc(0), decode: (_payload) => Buffer.from(JSON.stringify({ dps: { test: 'data' } })), }); const context = new MessageContext(asType({ rriot: { k: 'k' } })); context.registerDevice('duid', 'local', 'A01', 9); const des = new MessageDeserializer(context, logger); const header = Buffer.alloc(17); header.write('A01', 0, 3, 'ascii'); header.writeUInt32BE(1, 3); header.writeUInt32BE(2, 7); header.writeUInt32BE(3, 11); header.writeUInt16BE(999, 15); const decodedPayload = Buffer.from('encrypted'); const contentLen = Buffer.alloc(2); contentLen.writeUInt16BE(decodedPayload.length, 0); const preCrc = Buffer.concat([header, contentLen, decodedPayload]); const crc = CRC32.buf(preCrc) >>> 0; const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc, 0); const full = Buffer.concat([preCrc, crcBuf]); const res = des.deserialize('duid', full, 'from'); expect(res).toBeDefined(); expect(logger.error).toHaveBeenCalledWith('unknown protocol: 999'); }); it('throws on CRC32 mismatch', () => { vi.spyOn(MessageSerializerFactory.prototype, 'getMessageSerializer').mockReturnValue({ encode: (_payload, _localKey, _timestamp, _sequence, _nonce) => Buffer.alloc(0), decode: (_payload) => Buffer.from(JSON.stringify({ dps: {} })), }); const context = new MessageContext(asType({ rriot: { k: 'k' } })); context.registerDevice('duid', 'local', 'A01', 9); const des = new MessageDeserializer(context, logger); const header = Buffer.alloc(17); header.write('A01', 0, 3, 'ascii'); header.writeUInt32BE(1, 3); header.writeUInt32BE(2, 7); header.writeUInt32BE(3, 11); header.writeUInt16BE(Protocol.rpc_response, 15); const decodedPayload = Buffer.from('encrypted'); const contentLen = Buffer.alloc(2); contentLen.writeUInt16BE(decodedPayload.length, 0); const preCrc = Buffer.concat([header, contentLen, decodedPayload]); const badCrcBuf = Buffer.alloc(4); badCrcBuf.writeUInt32BE(0xdeadbeef, 0); const full = Buffer.concat([preCrc, badCrcBuf]); expect(() => des.deserialize('duid', full, 'from')).toThrow(/Wrong CRC32/); }); it('returns ResponseMessage without body when localKey is not found', () => { vi.spyOn(MessageSerializerFactory.prototype, 'getMessageSerializer').mockReturnValue({ encode: (_payload, _localKey, _timestamp, _sequence, _nonce) => Buffer.alloc(0), decode: (_payload) => Buffer.from(JSON.stringify({ dps: {} })), }); const context = new MessageContext(asType({ rriot: { k: 'k' } })); // Register only 'duid' but query with 'unknown-duid' context.registerDevice('duid', 'local', 'A01', 9); const des = new MessageDeserializer(context, logger); const header = Buffer.alloc(17); header.write('A01', 0, 3, 'ascii'); header.writeUInt32BE(1, 3); header.writeUInt32BE(2, 7); header.writeUInt32BE(3, 11); header.writeUInt16BE(Protocol.rpc_response, 15); const decodedPayload = Buffer.from('encrypted'); const contentLen = Buffer.alloc(2); contentLen.writeUInt16BE(decodedPayload.length, 0); const preCrc = Buffer.concat([header, contentLen, decodedPayload]); const crc = CRC32.buf(preCrc) >>> 0; const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc, 0); const full = Buffer.concat([preCrc, crcBuf]); const res = des.deserialize('unknown-duid', full, 'from'); expect(res).toBeDefined(); expect(res.body).toBeUndefined(); expect(logger.notice).toHaveBeenCalledWith(expect.stringContaining('Unable to retrieve local key')); }); it('handles map_response protocol and returns body with map data', () => { vi.spyOn(MessageSerializerFactory.prototype, 'getMessageSerializer').mockReturnValue({ encode: (_payload, _localKey, _timestamp, _sequence, _nonce) => Buffer.alloc(0), decode: (_payload) => Buffer.from([0x01, 0x02, 0x03]), }); const context = new MessageContext(asType({ rriot: { k: 'k' } })); context.registerDevice('duid', 'local', 'A01', 9); const des = new MessageDeserializer(context, logger); const header = Buffer.alloc(17); header.write('A01', 0, 3, 'ascii'); header.writeUInt32BE(1, 3); header.writeUInt32BE(2, 7); header.writeUInt32BE(3, 11); header.writeUInt16BE(Protocol.map_response, 15); const decodedPayload = Buffer.from('mapdata'); const contentLen = Buffer.alloc(2); contentLen.writeUInt16BE(decodedPayload.length, 0); const preCrc = Buffer.concat([header, contentLen, decodedPayload]); const crc = CRC32.buf(preCrc) >>> 0; const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc, 0); const full = Buffer.concat([preCrc, crcBuf]); const res = des.deserialize('duid', full, 'from'); expect(res).toBeDefined(); expect(res.isForProtocol(Protocol.map_response)).toBe(true); expect(res.body).toBeDefined(); expect(res.body?.data[Protocol.map_response]).toBeDefined(); }); it('handles device_status_ota protocol and returns body with parsed JSON', () => { const otaPayload = { state: 'idle', progress: 0 }; vi.spyOn(MessageSerializerFactory.prototype, 'getMessageSerializer').mockReturnValue({ encode: (_payload, _localKey, _timestamp, _sequence, _nonce) => Buffer.alloc(0), decode: (_payload) => Buffer.from(JSON.stringify(otaPayload)), }); const context = new MessageContext(asType({ rriot: { k: 'k' } })); context.registerDevice('duid', 'local', 'A01', 9); const des = new MessageDeserializer(context, logger); const header = Buffer.alloc(17); header.write('A01', 0, 3, 'ascii'); header.writeUInt32BE(1, 3); header.writeUInt32BE(2, 7); header.writeUInt32BE(3, 11); header.writeUInt16BE(Protocol.device_status_ota, 15); const decodedPayload = Buffer.from('encrypted'); const contentLen = Buffer.alloc(2); contentLen.writeUInt16BE(decodedPayload.length, 0); const preCrc = Buffer.concat([header, contentLen, decodedPayload]); const crc = CRC32.buf(preCrc) >>> 0; const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc, 0); const full = Buffer.concat([preCrc, crcBuf]); const res = des.deserialize('duid', full, 'from'); expect(res).toBeDefined(); expect(res.isForProtocol(Protocol.device_status_ota)).toBe(true); expect(res.body).toBeDefined(); expect(res.body?.data[Protocol.device_status_ota]).toEqual(otaPayload); }); it('handles device_status_ota parse error and returns ResponseMessage without body', () => { vi.spyOn(MessageSerializerFactory.prototype, 'getMessageSerializer').mockReturnValue({ encode: (_payload, _localKey, _timestamp, _sequence, _nonce) => Buffer.alloc(0), decode: (_payload) => Buffer.from('not valid json {{{'), }); const context = new MessageContext(asType({ rriot: { k: 'k' } })); context.registerDevice('duid', 'local', 'A01', 9); const des = new MessageDeserializer(context, logger); const header = Buffer.alloc(17); header.write('A01', 0, 3, 'ascii'); header.writeUInt32BE(1, 3); header.writeUInt32BE(2, 7); header.writeUInt32BE(3, 11); header.writeUInt16BE(Protocol.device_status_ota, 15); const decodedPayload = Buffer.from('encrypted'); const contentLen = Buffer.alloc(2); contentLen.writeUInt16BE(decodedPayload.length, 0); const preCrc = Buffer.concat([header, contentLen, decodedPayload]); const crc = CRC32.buf(preCrc) >>> 0; const crcBuf = Buffer.alloc(4); crcBuf.writeUInt32BE(crc, 0); const full = Buffer.concat([preCrc, crcBuf]); const res = des.deserialize('duid', full, 'from'); expect(res).toBeDefined(); expect(res.body).toBeUndefined(); expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('device_status_ota parse error')); }); }); //# sourceMappingURL=messageDeserializer.test.js.map