import * as mqtt from 'mqtt' import axios from 'axios' import { mocked } from 'ts-jest/utils' import * as uuid from 'uuid/v4' import { Client } from '../src' import * as Errors from '../src/errors' import { AccessLevel, CommissionResult, Config, CommandRequest, CreateAndPairParams, CreateAndPairResult, Event, GetStateResult, GetStatusResult, ListChildrenResult, MessageMethod, PairChildParams, PairChildResult, RemoveChildParams, RPCRequestMethod, StateRequest, TopicServiceName, TopicType, SetStateParams, AssociateUserParams, AssociateUserResult, GetDeviceInfoResult, NotificationMessage, } from '../src/types' import { Command, MessageProcessor, MessageRequestTopic, MessageResponseTopic, RPCInternalResponseTopic, RPCRequest, RPCResponseTopic, State, NotificationMessageEvent, } from '../src/utils' const exitSpy = jest.spyOn(process, 'exit') const CONNECT_CONFIG = { host: 'host', ca: 'ca', key: 'key', cert: 'cert', clientId: 'clientId', } const ActualMQTT = jest.requireActual('mqtt') const subscriptionTopic = 'topic' const mockSubscribe = jest.fn().mockImplementation((topic, callback) => callback(undefined, [{ topic: subscriptionTopic, qos: 0, }])) const mockPublish = jest.fn().mockImplementation((topic, payload, callback) => callback()) const mockUnsubscribe = jest.fn().mockImplementation((topic, cb) => cb()) const mockEnd = jest.fn().mockImplementation((force, cb) => cb()) const mockRPC = jest.fn() jest.mock('mqtt', () => { return { MqttClient: jest.fn().mockImplementation(() => { return { subscribe: mockSubscribe, unsubscribe: mockUnsubscribe, publish: mockPublish, end: mockEnd, } }), } }) jest.mock('axios') const mockedAxios = mocked(axios, true) const createSDKClient = (config: Config = CONNECT_CONFIG) => { const sdkClient = new Client(config) sdkClient['_client'] = new mqtt.MqttClient(ActualMQTT.MqttClient.prototype, CONNECT_CONFIG) sdkClient['_rpc'] = mockRPC return sdkClient } let sdkClient = createSDKClient() const initializedClientTest = (methodName: string, isPromise: boolean = true) => { test('Should throw ClientInitializeError if client is not initialized, but not exit process', async () => { const sdkClient = new Client(CONNECT_CONFIG) if (isPromise) { await expect(sdkClient[methodName]()).rejects.toThrowError(Errors.ClientInitializeError) } else { expect(() => sdkClient[methodName]()).toThrow(Errors.ClientInitializeError) } expect(exitSpy).toBeCalledTimes(0) }) } const toBuffer = (msg: any) => { return Buffer.from(JSON.stringify(msg)) } describe('ThinCloud Node SDK', () => { describe('Connect to MQTT API', () => { const mqttConnectMock = jest.fn().mockReturnValue(ActualMQTT.MqttClient.prototype) test('Should attach message event listener on connect', async () => { const connack = 1 // @ts-ignore mqtt.connect = mqttConnectMock const sdkClient = new Client(CONNECT_CONFIG) const emitConnect = () => { if (sdkClient['_client']) { sdkClient['_client'].emit(Event.CONNECT, connack) } } const runInterval = setInterval(emitConnect, 1) await sdkClient.connect() expect(sdkClient.isConnected).toBe(true) expect(sdkClient['_client']).toBeDefined() if (sdkClient['_client']) { expect(sdkClient['_client'].listenerCount(Event.MESSAGE)).toEqual(1) } clearInterval(runInterval) }) test('Should throw error but not exit', async () => { const connectErrorMock = jest.fn().mockReturnValue(new Error()) //@ts-ignore mqtt.connect = connectErrorMock const sdkClient = new Client(CONNECT_CONFIG) await expect(sdkClient.connect()).rejects.toThrow(Errors.ConnectionError) expect(exitSpy).toHaveBeenCalledTimes(0) }) }) describe('Disconnect from MQTT API', () => { initializedClientTest('disconnect') test('Should emit disconnect event and set isConnected bit to false after disconnect', async () => { // @ts-ignore const endSpy = jest.spyOn(sdkClient['_client'], 'end') await sdkClient.disconnect() expect(endSpy).toHaveBeenCalledTimes(1) expect(sdkClient.isConnected).toBeFalsy() }) }) const productId = uuid() describe('Commission device', () => { beforeEach(() => sdkClient = createSDKClient()) const deviceId = uuid() const commissionResult: CommissionResult = { deviceId, productId, } const rpcResult = { id: uuid(), result: commissionResult, } const timeout = 2000 test('Should throw error if missing productId', async () => { await expect(sdkClient.commission()).rejects.toThrowError(Errors.NoDeviceTypeError) }) test('Should call _rpc if force flag is true', async () => { mockRPC.mockResolvedValueOnce(rpcResult) sdkClient.deviceId = deviceId sdkClient.isCommissioned = true await sdkClient.commission({ productId, force: true }) expect(mockRPC).toBeCalledTimes(1) }) test('Should return deviceId if already commissioned', async () => { sdkClient.deviceId = deviceId sdkClient.isCommissioned = true sdkClient['_config'].productId = productId const result = await sdkClient.commission() expect(result).toEqual({ deviceId, productId }) }) test('Should call _rpc if productId is in client config', async () => { const sdkClient = createSDKClient({ ...CONNECT_CONFIG, productId, }) mockRPC.mockResolvedValue(rpcResult) const result = await sdkClient.commission() expect(mockRPC).toHaveBeenCalledWith( 'iot', 'commission', { productId, }, undefined, ) expect(mockSubscribe).toHaveBeenCalledWith( new MessageRequestTopic(CONNECT_CONFIG.clientId).topic, expect.anything()) expect(result).toEqual({ deviceId, productId }) expect(sdkClient['_config'].productId).toEqual(productId) }) test('Should call _rpc if productId is in commission config', async () => { mockRPC.mockResolvedValueOnce(rpcResult) const result = await sdkClient.commission({ productId, }) expect(mockRPC).toHaveBeenCalledWith( 'iot', 'commission', { productId }, undefined, ) expect(result).toEqual({ deviceId, productId }) }) test('Should call _rpc with commission timeout', async () => { mockRPC.mockResolvedValueOnce(rpcResult) const result = await sdkClient.commission({ productId, }, timeout) expect(mockRPC).toHaveBeenCalledWith( 'iot', 'commission', { productId }, timeout, ) expect(result).toEqual({ deviceId, productId }) }) test('Should set isCommissioned to false on error', async () => { mockRPC.mockResolvedValueOnce({}) await expect(sdkClient.commission({ productId })).rejects.toThrow() expect(sdkClient.isCommissioned).toBe(false) }) }) describe('Decommission device', () => { beforeEach(() => sdkClient = createSDKClient()) const deviceId = uuid() test('Should have valid success path', async () => { sdkClient.isCommissioned = true sdkClient.deviceId = deviceId mockRPC.mockResolvedValueOnce({ id: uuid(), result: {}, }) const result = await sdkClient.decommission() expect(mockRPC).toBeCalledWith('iot', 'decommission', {}, undefined) expect(result).toEqual({}) expect(sdkClient.isCommissioned).toBe(false) expect(sdkClient.deviceId).toBe(null) }) test('Should throw error if no result', async () => { sdkClient.isCommissioned = true sdkClient.deviceId = deviceId mockRPC.mockRejectedValueOnce(new Error('error')) await expect(sdkClient.decommission()).rejects.toThrow() expect(sdkClient.isCommissioned).toBe(true) expect(sdkClient.deviceId).toBe(deviceId) }) }) describe('Get Product Info', () => { beforeEach(() => sdkClient = createSDKClient()) const successPayload = { productId, firmwareReleases: [{ version: '1.0.0', labels: [], }], } test('Should get product info with productId in client config', async () => { sdkClient['_config'].productId = productId mockRPC.mockResolvedValueOnce({ id: uuid(), result: successPayload, }) const result = await sdkClient.getProductInfo() expect(mockRPC).toBeCalledWith('product', 'getProductInfo', {}, undefined, productId) expect(result).toEqual(successPayload) }) test('Should get product info when productId is in getProductInfo config', async () => { mockRPC.mockResolvedValueOnce({ id: uuid(), result: successPayload, }) const productId = uuid() const result = await sdkClient.getProductInfo(productId) expect(mockRPC).toBeCalledWith('product', 'getProductInfo', {}, undefined, productId) expect(result).toEqual(successPayload) }) test('Should fail to get product info if _rpc fails', async () => { mockRPC.mockRejectedValueOnce(new Error('error')) expect(mockRPC).toBeCalledWith('product', 'getProductInfo', {}, undefined, productId) await expect(sdkClient.getProductInfo()).rejects.toThrow() }) test('should throw error if no productId is provided or in config', async () => { sdkClient['_config'].productId = undefined await expect(sdkClient.getProductInfo()).rejects.toThrow() }) }) describe('Get Device Info', () => { beforeEach(() => sdkClient = createSDKClient()) const mockResult: GetDeviceInfoResult = { deviceId: uuid(), productId: 'Bulb', } test('Should throw error if _rpc throws error', async () => { mockRPC.mockRejectedValueOnce(new Error('error')) await expect(sdkClient.getDeviceInfo()).rejects.toThrow() }) test('Should call _rpc with correct args', async () => { mockRPC.mockResolvedValueOnce({ id: uuid(), result: mockResult, }) await expect(sdkClient.getDeviceInfo()).resolves.toEqual(mockResult) expect(mockRPC).toHaveBeenCalledWith('devices', 'getDeviceInfo', {}, undefined) }) }) describe('Associate User', () => { beforeEach(() => sdkClient = createSDKClient()) const params: AssociateUserParams = { accessLevel: AccessLevel.OWNER, userId: uuid(), deviceId: uuid(), } const result: AssociateUserResult = { userId: params.userId, accessLevel: params.accessLevel, } test('Should throw error if _rpc throws error', async () => { mockRPC.mockRejectedValueOnce(new Error('error')) await expect(sdkClient.associateUser(params)).rejects.toThrow() }) test('Should call _rpc with correct args', async () => { mockRPC.mockResolvedValueOnce({ requestId: uuid(), result }) await expect(sdkClient.associateUser(params)).resolves.toEqual(result) expect(mockRPC).toHaveBeenCalledWith('devices', 'associateUser', params, undefined) }) }) describe('Get invitation code', () => { beforeEach(() => sdkClient = createSDKClient()) const invitationCodeParams = { accessLevel: AccessLevel.OWNER, userId: uuid(), } const invitationCodeResult = { invitationCode: '123', } test('Should throw error if _rpc throws error', async () => { mockRPC.mockRejectedValueOnce(new Error('error')) await expect(sdkClient.getInvitationCode(invitationCodeParams)).rejects.toThrow() }) test('Should call _rpc with correct args', async () => { mockRPC.mockResolvedValueOnce({ id: uuid(), result: invitationCodeResult, }) const result = await sdkClient.getInvitationCode(invitationCodeParams) expect(mockRPC).toHaveBeenCalledWith('user', 'getInvitationCode', invitationCodeParams, undefined) expect(result).toStrictEqual(invitationCodeResult) }) }) describe('Get State', () => { beforeEach(() => { sdkClient = createSDKClient() mockRPC.mockResolvedValue({ id: uuid(), result, }) }) const result: GetStateResult = { locked: true, } test('Should throw error if _rpc throws error', async () => { mockRPC.mockRejectedValueOnce(new Error('error')) await expect(sdkClient.getState()).rejects.toThrow() }) test('Should call _rpc with correct args', async () => { const resp = await sdkClient.getState() expect(mockRPC).toHaveBeenCalledWith('state', 'get', {}, undefined, undefined) expect(resp).toStrictEqual(result) }) test('Should call _rpc with correct args for child device', async () => { const childDeviceId = uuid() const resp = await sdkClient.getState(childDeviceId) expect(mockRPC).toHaveBeenCalledWith('state', 'get', {}, undefined, childDeviceId) expect(resp).toStrictEqual(result) }) }) describe('Get Status', () => { beforeEach(() => { sdkClient = createSDKClient() mockRPC.mockResolvedValue({ id: uuid(), result, }) }) const result: GetStatusResult = { commissioned: true, deviceId: uuid(), productId: uuid(), } test('Should throw error if _rpc throws error', async () => { mockRPC.mockRejectedValueOnce(new Error('error')) await expect(sdkClient.getStatus()).rejects.toThrow() }) test('Should call _rpc with correct args', async () => { const resp = await sdkClient.getStatus() expect(mockRPC).toHaveBeenCalledWith('iot', 'getStatus', {}, undefined) expect(resp).toStrictEqual(result) }) }) describe('Create And Pair', () => { beforeEach(() => { sdkClient = createSDKClient() mockRPC.mockResolvedValue({ id: uuid(), result, }) }) const params: CreateAndPairParams = { productId: 'Lock', } const result: CreateAndPairResult = { deviceId: uuid(), productId: 'Lock', } test('should throw error if _rpc throws error', async () => { mockRPC.mockRejectedValueOnce(new Error('error')) await expect(sdkClient.createAndPair(params)).rejects.toThrow() }) test('Should call _rpc with correct args', async () => { const resp = await sdkClient.createAndPair(params) expect(mockRPC).toHaveBeenCalledWith('gateway-children', 'createAndPair', params, undefined) expect(resp).toStrictEqual(result) }) }) describe('Pair Child', () => { beforeEach(() => { sdkClient = createSDKClient() mockRPC.mockResolvedValue({ id: uuid(), result, }) }) const params: PairChildParams = { deviceId: uuid(), } const result: PairChildResult = { deviceId: params.deviceId, productId: 'Lock', } test('should throw error if _rpc throws error', async () => { mockRPC.mockRejectedValueOnce(new Error('error')) await expect(sdkClient.pairChild(params)).rejects.toThrow(Errors.PairChildError) }) test('should call _rpc with correct args', async () => { await expect(sdkClient.pairChild(params)).resolves.toEqual(result) expect(mockRPC).toHaveBeenCalledWith('gateway-children', 'pair', params, undefined) }) }) describe('List Children', () => { beforeEach(() => { sdkClient = createSDKClient() mockRPC.mockResolvedValue({ id: uuid(), result, }) }) const result: ListChildrenResult[] = [{ id: uuid(), }] test('Should throw error if _rpc throws error', async () => { mockRPC.mockRejectedValueOnce(new Error('error')) await expect(sdkClient.listChildren()).rejects.toThrow(Errors.ListChildrenError) }) test('Should resolve call _rpc with correct args', async () => { await expect(sdkClient.listChildren()).resolves.toEqual(result) expect(mockRPC).toHaveBeenCalledWith('gateway-children', 'list', {}, undefined) }) }) describe('Remove Child', () => { beforeEach(() => { sdkClient = createSDKClient() mockRPC.mockResolvedValue({ id: uuid(), result: undefined, }) }) const params: RemoveChildParams = { deviceId: uuid(), } test('should throw error if _rpc throws error', async () => { mockRPC.mockRejectedValueOnce(new Error('Could not remove child')) await expect(sdkClient.removeChild(params)).rejects.toThrow(Errors.RemoveChildError) }) test('should call _rpc with correct args', async () => { await expect(sdkClient.removeChild(params)).resolves.toBeUndefined() expect(mockRPC).toHaveBeenCalledWith('gateway-children', 'remove', params, undefined) }) }) describe('Publish messages', () => { beforeEach(() => mockPublish.mockClear()) const payloadId = uuid() initializedClientTest('_publish') test('Should resolve correct payload', async () => { const resolvedPayloadId = await sdkClient['_publish']('topic', { requestId: payloadId, }) expect(resolvedPayloadId.id).toBe(payloadId) }) test('Should call publish with opts if opts is passed in', async () => { mockPublish.mockImplementationOnce((topic, payload, options, callback) => callback()) const resolvedPayloadId = await sdkClient['_publish']('topic', { requestId: payloadId, }, { qos: 1 }) expect(mockPublish.mock.calls[0][2]).toStrictEqual({ qos: 1 }) expect(resolvedPayloadId.id).toBe(payloadId) }) test('Should catch error and not exit process', async () => { mockPublish.mockImplementationOnce((topic, payload, callback) => callback(new Error())) await expect(sdkClient['_publish']('topic', { requestId: payloadId, })).rejects.toThrowError() expect(exitSpy).toHaveBeenCalledTimes(0) }) }) describe('Subscribe to topics', () => { initializedClientTest('_subscribe') beforeEach(() => mockSubscribe.mockClear()) test('Should resolve correct payload', async () => { const resolvedTopic = await sdkClient['_subscribe'](subscriptionTopic) expect(resolvedTopic.topic).toBe(subscriptionTopic) }) test('Should call subscribe with opts if opts is passed in', async () => { mockSubscribe.mockImplementationOnce((topic, options, callback) => callback(undefined, [{ topic: subscriptionTopic, qos: 1 }])) const resolvedTopic = await sdkClient['_subscribe'](subscriptionTopic, { qos: 1 }) expect(mockSubscribe.mock.calls[0][1]).toStrictEqual({ qos: 1 }) expect(resolvedTopic.topic).toBe(subscriptionTopic) }) test('Should catch error and not exit process', async () => { mockSubscribe.mockImplementationOnce((topic, callback) => callback(new Error())) await expect(sdkClient['_subscribe'](subscriptionTopic)).rejects.toThrowError(Errors.SubscribeError) expect(exitSpy).toHaveBeenCalledTimes(0) }) }) describe('Unsubscribe from topics', () => { initializedClientTest('_unsubscribe') const subscriptionTopic = 'topic' beforeEach(() => mockUnsubscribe.mockClear()) test('Should resolve topic', async () => { const unsubscribedTopic = await sdkClient['_unsubscribe'](subscriptionTopic) expect(unsubscribedTopic.topic).toBe(subscriptionTopic) }) test('Should catch error and not exit process', async () => { mockUnsubscribe.mockImplementation((topic, callback) => callback(new Error())) await expect(sdkClient['_unsubscribe'](subscriptionTopic)).rejects.toThrowError(Errors.UnsubscribeError) expect(exitSpy).toHaveBeenCalledTimes(0) }) }) describe('Event Listeners (on and off)', () => { initializedClientTest('on', false) initializedClientTest('off', false) test('Should properly attach event listener (on)', () => { const sdkClient = new Client(Object.assign(CONNECT_CONFIG, { requestTimeout: 3000, })) sdkClient['_client'] = ActualMQTT.MqttClient.prototype let x = 0 //@ts-ignore const listenersBeforeCall = sdkClient['_client'].listeners(Event.CONNECT).length const callback = () => { x = 1 } sdkClient.on(Event.CONNECT, callback) //@ts-ignore const listenersAfterCall = sdkClient['_client'].listeners(Event.CONNECT).length const addedListeners = listenersAfterCall - listenersBeforeCall expect(addedListeners).toBe(1) expect(x).toBe(0) //@ts-ignore sdkClient['_client'].emit(Event.CONNECT) expect(x).toBe(1) }) test('Should properly remove event listener (off)', () => { const sdkClient = new Client(CONNECT_CONFIG) sdkClient['_client'] = ActualMQTT.MqttClient.prototype const callback = () => { return '' } sdkClient.on(Event.CONNECT, callback) //@ts-ignore const listenersAfterOnCall = sdkClient['_client'].listeners(Event.CONNECT).length sdkClient.off(Event.CONNECT, callback) //@ts-ignore const listenersAfterOffCall = sdkClient['_client'].listeners(Event.CONNECT).length const removedListeners = listenersAfterOnCall - listenersAfterOffCall expect(removedListeners).toBe(1) }) }) describe('Handle RPC request/response cycle', () => { const clientConfig = { ...CONNECT_CONFIG, requestTimeout: 3000, } const subscribeMock = jest.fn().mockImplementation(topic => { return new Promise((resolve, reject) => resolve({ topic })) }) const publishMock = jest.fn().mockImplementation(() => { return new Promise((resolve, reject) => resolve({})) }) const sdkClient = new Client(clientConfig) sdkClient['_client'] = ActualMQTT.MqttClient.prototype sdkClient['_subscribe'] = subscribeMock sdkClient['_unsubscribe'] = subscribeMock sdkClient['_publish'] = publishMock test('Should resolve correct success payload', async () => { const requestId = uuid() jest.spyOn(RPCRequest.prototype, 'requestId', 'get').mockReturnValue(requestId) const message = { message: 'message', } const eventTopic = new RPCInternalResponseTopic(TopicType.RPC_RESPONSE, requestId).topic const emitMessage = () => { if (sdkClient['_client']) { sdkClient['_client'].emit(eventTopic, message) } } const runInterval = setInterval(emitMessage, 1) const payload = await sdkClient['_rpc']( TopicServiceName.IOT, RPCRequestMethod.COMMISSION, { productId: 'Light', }) expect(payload).toBe(message) clearInterval(runInterval) }) test('Should emit correct error payload', async () => { const requestId = uuid() jest.spyOn(RPCRequest.prototype, 'requestId', 'get').mockReturnValue(requestId) const error = { requestId, error: { message: 'Error!', }, } const eventTopic = new RPCInternalResponseTopic(TopicType.RPC_RESPONSE, requestId).topic const emitMessage = () => { if (sdkClient['_client']) { sdkClient['_client'].emit(eventTopic, undefined, error) } } const runInterval = setInterval(emitMessage, 1) await expect(sdkClient['_rpc']( TopicServiceName.IOT, RPCRequestMethod.COMMISSION, { productId: 'Light', })).rejects.toThrowError(Errors.RPCResponseError) clearInterval(runInterval) }) test('Should throw error if an error event is emitted', async () => { const error = { error: true, } const emitError = () => { if (sdkClient['_client']) { sdkClient['_client'].emit(Event.ERROR, error) } } const runInterval = setInterval(emitError, 1) await expect(sdkClient['_rpc']( TopicServiceName.IOT, RPCRequestMethod.COMMISSION, { productId: 'Light', })).rejects.toThrowError() clearInterval(runInterval) }) test('Should throw error if no RPC response within timeout', async () => { await expect(sdkClient['_rpc']( TopicServiceName.IOT, RPCRequestMethod.COMMISSION, { productId: 'Light', })).rejects.toThrowError(Errors.RequestTimeoutError) }) }) describe('Message Processor', () => { const sdkClient = new Client(CONNECT_CONFIG) const clientId = CONNECT_CONFIG.clientId sdkClient['_client'] = ActualMQTT.MqttClient.prototype //@ts-ignore const emitSpy = jest.spyOn(sdkClient['_client'], 'emit') test('Should emit correct RPC message event', () => { const requestId = uuid() const rpcPayload = { requestId, result: {}, } const msgProcessor = new MessageProcessor(sdkClient) const msgTopic = new RPCResponseTopic(clientId).topic const msgPayload = toBuffer(rpcPayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = new RPCInternalResponseTopic(TopicType.RPC_RESPONSE, requestId).topic expect(emitSpy).toBeCalledWith(eventTopic, rpcPayload) }) test('Should emit RPC message error event', () => { const rpcFailurePayload = { requestId: uuid(), error: { code: 400, message: 'Error!' }, } const msgProcessor = new MessageProcessor(sdkClient) const msgTopic = new RPCResponseTopic(clientId).topic const msgPayload = toBuffer(rpcFailurePayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = new RPCInternalResponseTopic(TopicType.RPC_RESPONSE, rpcFailurePayload.requestId).topic expect(emitSpy).toBeCalledWith(eventTopic, undefined, rpcFailurePayload) }) describe('Product message events', () => { describe('Notify Method', () => { test('Should emit correct product update message event', () => { const productUpdatePayload: NotificationMessage = { method: MessageMethod.NOTIFY, params: { manufacturer: 'Yonomi', }, } const msgProcessor = new MessageProcessor(sdkClient) const product = new NotificationMessageEvent(sdkClient, productUpdatePayload) const msgTopic = `thincloud/message/${clientId}/product` const msgPayload = toBuffer(productUpdatePayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = Event.PRODUCT expect(emitSpy).toBeCalledWith(eventTopic, product) expect(product.params).toEqual(productUpdatePayload.params) expect(product.childDeviceId).toBeUndefined() }) test('Should emit correct product update message for child device', () => { const productUpdatePayload: NotificationMessage = { method: MessageMethod.NOTIFY, params: { manufacturer: 'Yonomi', }, } const childDeviceId = uuid() const msgProcessor = new MessageProcessor(sdkClient) const product = new NotificationMessageEvent(sdkClient, productUpdatePayload, childDeviceId) const msgTopic = `thincloud/message/${clientId}/product/${childDeviceId}` const msgPayload = toBuffer(productUpdatePayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = Event.PRODUCT expect(emitSpy).toBeCalledWith(eventTopic, product) expect(product.params).toEqual(productUpdatePayload.params) expect(product.childDeviceId).toEqual(childDeviceId) }) }) describe('FirmwareUpdate Method', () => { test('Should emit correct product update message event', () => { const firmwareUpdatePayload: NotificationMessage = { method: MessageMethod.FIRMWARE_UPDATE, params: { firmwareVersion: '1.2.3', firmwareUrl: 'some-url', }, } const msgProcessor = new MessageProcessor(sdkClient) const product = new NotificationMessageEvent(sdkClient, firmwareUpdatePayload) const msgTopic = `thincloud/message/${clientId}/product` const msgPayload = toBuffer(firmwareUpdatePayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = Event.FIRMWARE_UPDATE expect(emitSpy).toBeCalledWith(eventTopic, product) expect(product.params).toEqual(firmwareUpdatePayload.params) expect(product.childDeviceId).toBeUndefined() }) test('Should emit correct product update message event', () => { const firmwareUpdatePayload: NotificationMessage = { method: MessageMethod.FIRMWARE_UPDATE, params: { firmwareVersion: '1.2.3', firmwareUrl: 'some-url', }, } const childDeviceId = uuid() const msgProcessor = new MessageProcessor(sdkClient) const product = new NotificationMessageEvent(sdkClient, firmwareUpdatePayload, childDeviceId) const msgTopic = `thincloud/message/${clientId}/product/${childDeviceId}` const msgPayload = toBuffer(firmwareUpdatePayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = Event.FIRMWARE_UPDATE expect(emitSpy).toBeCalledWith(eventTopic, product) expect(product.params).toEqual(firmwareUpdatePayload.params) expect(product.childDeviceId).toEqual(childDeviceId) }) }) }) describe('Device update message events', () => { it('Should emit correct device update message event', () => { const deviceUpdatePayload: NotificationMessage = { method: MessageMethod.NOTIFY, params: { on: true, }, } const msgProcessor = new MessageProcessor(sdkClient) const device = new NotificationMessageEvent(sdkClient, deviceUpdatePayload) const msgTopic = `thincloud/message/${clientId}/device` const msgPayload = toBuffer(deviceUpdatePayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = Event.DEVICE_UPDATE expect(emitSpy).toBeCalledWith(eventTopic, device) expect(device.params).toEqual(deviceUpdatePayload.params) expect(device.childDeviceId).toBeUndefined() }) it('Should emit correct device update message event for child device', () => { const deviceUpdatePayload: NotificationMessage = { method: MessageMethod.NOTIFY, params: { on: true, }, } const childDeviceId = uuid() const msgProcessor = new MessageProcessor(sdkClient) const device = new NotificationMessageEvent(sdkClient, deviceUpdatePayload, childDeviceId) const msgTopic = `thincloud/message/${clientId}/device/${childDeviceId}` const msgPayload = toBuffer(deviceUpdatePayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = Event.DEVICE_UPDATE expect(emitSpy).toBeCalledWith(eventTopic, device) expect(device.params).toEqual(deviceUpdatePayload.params) expect(device.childDeviceId).toEqual(childDeviceId) }) }) describe('Command message events', () => { test('Should emit correct command message event', () => { const commandPayload: CommandRequest = { requestId: uuid(), method: 'setEffect', params: { payload: JSON.stringify({ fx: 'effect', }), }, } const command = new Command(sdkClient, commandPayload) const msgProcessor = new MessageProcessor(sdkClient) const msgTopic = `thincloud/message/${clientId}/command` const msgPayload = toBuffer(commandPayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = Event.COMMAND expect(emitSpy).toBeCalledWith(eventTopic, command) }) test('Should emit correct related device command message event', () => { const commandPayload: CommandRequest = { requestId: uuid(), method: 'setEffect', params: { payload: JSON.stringify({ fx: 'effect', }), }, } const childDeviceId = uuid() const command = new Command(sdkClient, commandPayload, childDeviceId) const msgProcessor = new MessageProcessor(sdkClient) const msgTopic = `thincloud/message/${clientId}/command/${childDeviceId}` const msgPayload = toBuffer(commandPayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = Event.COMMAND expect(emitSpy).toBeCalledWith(eventTopic, command) expect(command.childDeviceId).toEqual(childDeviceId) }) }) describe('State message events', () => { test('Should emit correct state message event', () => { const statePayload: StateRequest = { requestId: uuid(), method: MessageMethod.MUTATE, params: 'state change', } const msgProcessor = new MessageProcessor(sdkClient) const msgTopic = `thincloud/message/${clientId}/state` const msgPayload = toBuffer(statePayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = Event.STATE expect(emitSpy).toBeCalledWith(eventTopic, new State(sdkClient, statePayload)) }) test('Should emit correct state message event for child device', () => { const childDeviceId = uuid() const statePayload: StateRequest = { requestId: uuid(), method: MessageMethod.MUTATE, params: 'state change', } const msgProcessor = new MessageProcessor(sdkClient) const msgTopic = `thincloud/message/${clientId}/state/${childDeviceId}` const msgPayload = toBuffer(statePayload) msgProcessor.process(msgTopic, msgPayload) const eventTopic = Event.STATE expect(emitSpy).toBeCalledWith(eventTopic, new State(sdkClient, statePayload, childDeviceId)) }) }) test('Should emit message with error if payload is invalid', () => { const invalidPayload = 'invalid message' const msgProcessor = new MessageProcessor(sdkClient) let msgTopic = new MessageRequestTopic(clientId).topic const msgPayload = toBuffer(invalidPayload) msgProcessor.process(msgTopic, msgPayload) expect(emitSpy).toHaveBeenCalledWith(Event.ERROR, new Errors.InvalidMessageError(msgTopic, msgPayload.toString())) msgTopic = new RPCResponseTopic(clientId).topic msgProcessor.process(msgTopic, msgPayload) expect(emitSpy).toHaveBeenCalledWith(Event.ERROR, new Errors.InvalidMessageError(msgTopic, msgPayload.toString())) }) test('Should emit message with error if _toJSON is invalid', () => { const toJSONmock = jest.fn().mockImplementation(() => { throw new Error('error') }) MessageProcessor.prototype['_toJSON'] = toJSONmock const invalidPayload = 'invalid message' const msgProcessor = new MessageProcessor(sdkClient) const msgTopic = new MessageRequestTopic(clientId).topic const msgPayload = toBuffer(invalidPayload) msgProcessor.process(msgTopic, msgPayload) expect(emitSpy).toHaveBeenCalledWith(Event.ERROR, new Errors.InvalidMessageError(msgTopic, msgPayload.toString())) }) }) describe('Commands', () => { const commandPayload = { requestId: uuid(), method: 'setEffect', params: { payload: JSON.stringify({ fx: 'effect', }), }, } as CommandRequest const sdkClient = new Client(CONNECT_CONFIG) sdkClient['_client'] = ActualMQTT.MqttClient.prototype const publishMock = jest.fn().mockResolvedValue({}) sdkClient['_publish'] = publishMock test('Should process command correctly with success payload', async () => { const successPayload = { success: true, } let commandData //@ts-ignore sdkClient['_client'].on(Event.COMMAND, async data => { commandData = data await commandData.success(successPayload) }) //@ts-ignore sdkClient['_client'].emit(Event.COMMAND, new Command(sdkClient, commandPayload)) expect(commandData).toBeDefined() expect(commandData.payload).toEqual(commandPayload.params.payload) expect(commandData.name).toEqual(commandPayload.method) expect(publishMock).toBeCalledWith(new MessageResponseTopic().topic, { requestId: commandPayload.requestId, result: successPayload, }) }) test('Should process command correctly with error payload', async () => { const errPayload = { error: 'Is error!', } let commandData //@ts-ignore sdkClient['_client'].on(Event.COMMAND, async data => { commandData = data await commandData.error(errPayload) }) //@ts-ignore sdkClient['_client'].emit(Event.COMMAND, new Command(sdkClient, commandPayload)) expect(commandData).toBeDefined() expect(commandData.payload).toEqual(commandPayload.params.payload) expect(commandData.name).toEqual(commandPayload.method) expect(publishMock).toBeCalledWith(new MessageResponseTopic().topic, { requestId: commandPayload.requestId, error: { payload: errPayload, }, }) }) }) describe('State Changes', () => { beforeEach(() => { sdkClient = createSDKClient({ ...CONNECT_CONFIG, deviceId, }) sdkClient.isConnected = true mockRPC.mockResolvedValue({ requestId: uuid(), result: stateChangeParams, }) }) const deviceId = uuid() const stateChangeParams: SetStateParams = { stateChange: true, } const stateChangePayload = { requestId: uuid(), method: 'set', params: stateChangeParams, } test('setState should call _rpc with correct params', async () => { await sdkClient.setState(stateChangeParams) expect(mockRPC).toHaveBeenCalledWith( 'state', 'set', stateChangeParams, undefined, undefined, ) }) test('setState should call _rpc with a child device id', async () => { const childDeviceId = uuid() await sdkClient.setState(stateChangeParams, childDeviceId) expect(mockRPC).toHaveBeenCalledWith( 'state', 'set', stateChangeParams, undefined, childDeviceId, ) }) test('Should throw error no client is not connected', async () => { sdkClient.isConnected = false await expect(sdkClient.setState(stateChangeParams)).rejects.toThrow(new Errors.ClientNotConnectedError()) }) test('Should handle request/response cycle', async () => { const sdkClient = new Client(CONNECT_CONFIG) sdkClient['_client'] = ActualMQTT.MqttClient.prototype sdkClient['_rpc'] = mockRPC sdkClient.isConnected = true const requestPayload: StateRequest = { requestId: uuid(), method: MessageMethod.MUTATE, params: { stateChange: true, }, } let stateChangeData //@ts-ignore sdkClient['_client'].on(Event.STATE, async data => { stateChangeData = data await stateChangeData.confirm() }) //@ts-ignore sdkClient['_client'].emit(Event.STATE, new State(sdkClient, requestPayload)) expect(stateChangeData).toBeDefined() expect(stateChangeData.params).toEqual(requestPayload.params) expect(mockRPC).toHaveBeenCalledWith( 'state', 'set', stateChangeParams, undefined, undefined, ) }) }) describe('getFirmware', () => { test('Should throw error if request exceeds timeout', async () => { }) test('Should throw error if request fails', async () => { mockedAxios.get.mockRejectedValueOnce({ error: 'error', }) const sdkClient = createSDKClient() await expect(sdkClient.getFirmware({ url: 'hosted firmware url endpoint', })).rejects.toThrowError(Errors.GetFirmwareError) }) test('Should return successful response data', async () => { mockedAxios.get.mockResolvedValueOnce({ data: 'firmware image' }) const sdkClient = createSDKClient() await expect(sdkClient.getFirmware({ url: 'hosted firmware url endpoint', })).resolves.toEqual('firmware image') }) }) })