UNPKG

aws-iot-device-sdk

Version:
1,193 lines (1,134 loc) 85.1 kB
/* * Copyright 2010-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ //node.js deps const filesys = require('fs'); //npm deps //app deps var rewire = require('rewire'); var sinon = require('sinon'); var assert = require('assert'); var mqtt = require('mqtt'); var myTls = rewire('../device/lib/tls'); var mockTls = require('./mock/mockTls'); var mockMQTTClient = require('./mock/mockMQTTClient'); describe( "device class unit tests", function() { var deviceModule = require('../').device; var mockMQTTClientObject; var fakeConnect; var mqttSave; var mockTlsRevert; var mockMqttRevert; var mockTlsObject = new mockTls(); var mockMqttObject = new mockTls.mqtt(); beforeEach( function () { // Mock the connect API for mqtt.js fakeConnect = function(wrapper,options) { mockMQTTClientObject = new mockMQTTClient(); // return the mocking object mockMQTTClientObject.reInitCommandCalled(); mockMQTTClientObject.resetPublishedMessage(); return mockMQTTClientObject; }; mqttSave = sinon.stub(mqtt, 'MqttClient', fakeConnect); mockTlsRevert = myTls.__set__("tls", mockTlsObject); mockMqttRevert = myTls.__set__("mqtt", mockMqttObject); }); afterEach( function () { mqttSave.restore(); mockTlsRevert(); mockMqttRevert(); }); describe("TLS handler calls the correct functions", function() { it("calls the correct functions", function() { mockTlsObject.reInitCommandCalled(); myTls(mockMqttObject, { 'testOption': true } ); assert.equal(mockTlsObject.commandCalled['connect'], 1); assert.equal(mockTlsObject.commandCalled['on'], 2); assert.equal(mockTlsObject.commandCalled['emit'], 1); assert.equal(mockMqttObject.commandCalled['emit'], 1); }) }); describe( "device is instantiated with empty parameters", function() { // // Verify that the device module throws an exception when all // parameters are empty. // it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device is instantiated with no public key", function() { // // Verify that the device module throws an exception when there is // no valid public key file. // it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device is instantiated with no CA certificate", function() { // // Verify that the device module throws an exception when there is // no valid CA certificate file. // it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { keyPath:'test/data/private.pem.key', certPath:'test/data/certificate.pem.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device is instantiated with no client certificate", function() { // // Verify that the device module throws an exception when there is // no valid client certificate file. // it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device is instantiated with invalid key path", function() { // // Verify that the device module throws an exception when key is not valid. // // it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { keyPath:'test/data/private.pem.key-1', certPath:'test/data/certificate.pem.crt', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device is instantiated with invalid cert path", function() { // // Verify that the device module throws an exception when certificate is not valid. // // it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { keyPath:'test/data/private.pem.key', certPath:'test/data/certificate.pem.crt-1', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device is instantiated with invalid CA path", function() { // // Verify that the device module throws an exception when CA is not valid. // // it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { keyPath:'test/data/private.pem.key', certPath:'test/data/certificate.pem.crt', caPath:'test/data/root-CA.crt-1', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device is instantiated with required parameters", function() { // // Verify that the device module doesn't throw an exception when all // parameters are specified correctly. // it("does not throw an exception", function() { assert.doesNotThrow( function( err ) { var device = deviceModule( { keyPath:'test/data/private.pem.key', certPath:'test/data/certificate.pem.crt', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device accepts certificate data in buffer", function() { // // Verify that the device module accepts certificate and key data in buffers // when using the properties generated by the AWS Console. // it("does not throw an exception", function() { var buffers = {}; buffers.privateKey = filesys.readFileSync('test/data/private.pem.key'); buffers.certificate = filesys.readFileSync('test/data/certificate.pem.crt'); buffers.rootCA = filesys.readFileSync('test/data/root-CA.crt'); assert.doesNotThrow( function( err ) { var device = deviceModule( { clientCert: buffers.certificate, privateKey: buffers.privateKey, caCert:buffers.rootCA, clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device accepts certificate data in buffers+files", function() { // // Verify that the device module accepts certificate and key data in files // as well as buffers when using the properties generated by the AWS Iot // Console. // it("does not throw an exception", function() { var buffers = {}; buffers.privateKey = filesys.readFileSync('test/data/private.pem.key'); buffers.rootCA = filesys.readFileSync('test/data/root-CA.crt'); assert.doesNotThrow( function( err ) { var device = deviceModule( { clientCert:'test/data/certificate.pem.crt', privateKey: buffers.privateKey, caCert:buffers.rootCA, clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device accepts certificate data in buffers+files", function() { // // Verify that the device module accepts certificate and key data in files // as well as buffers when using the properties generated by the AWS Iot // Console. // it("does not throw an exception", function() { var buffers = {}; buffers.rootCA = filesys.readFileSync('test/data/root-CA.crt'); assert.doesNotThrow( function( err ) { var device = deviceModule( { clientCert:'test/data/certificate.pem.crt', privateKey: 'test/data/private.pem.key', caCert:buffers.rootCA, clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device accepts certificate data in buffers+files", function() { // // Verify that the device module accepts certificate and key data in files // using the properties generated by the AWS Iot Console. // it("does not throw an exception", function() { assert.doesNotThrow( function( err ) { var device = deviceModule( { clientCert:'test/data/certificate.pem.crt', privateKey: 'test/data/private.pem.key', caCert: 'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device ensures AWS Console clientCert property is a buffer or file", function() { // // Verify that the device module will not accept a client certificate property // which is neither a file nor a buffer. // it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { clientCert: { }, privateKey: 'test/data/private.pem.key', caCert: 'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device ensures AWS Console privateKey property is a buffer or file", function() { // // Verify that the device module will not accept a private key property // which is neither a file nor a buffer. // it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { clientCert:'test/data/certificate.pem.crt', privateKey: { }, caCert: 'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device ensures AWS Console caCert property is a buffer or file", function() { // // Verify that the device module will not accept a CA certificate property // which is neither a file nor a buffer. // it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { clientCert:'test/data/certificate.pem.crt', privateKey: 'test/data/private.pem.key', caCert: { }, clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device throws an exception if using websocket protocol without IAM credentials", function() { // // Verify that the device module throws an exception when incorrectly // configured for websocket operation. // it("throws exception", function() { delete process.env.AWS_ACCESS_KEY_ID; delete process.env.AWS_SECRET_ACCESS_KEY; assert.throws( function( err ) { var device = deviceModule( { host:'XXXX.iot.us-east-1.amazonaws.com', protocol: 'wss', filename: '' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device throws an exception if using websocket protocol with invalid credential files", function() { // // Verify that the device module throws an exception when incorrectly // configured for websocket operation. // it("throws exception", function() { delete process.env.AWS_ACCESS_KEY_ID; delete process.env.AWS_SECRET_ACCESS_KEY; assert.throws( function( err ) { var device = deviceModule( { host:'XXXX.iot.us-east-1.amazonaws.com', protocol: 'wss', filename: './test/data/invalid_credentials' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device not throw an exception if using websocket protocol with filename specified in options", function() { // // Verify that the device module does not throw an exception when loading // credentials from credential file// // it("does not throws exception", function() { delete process.env.AWS_ACCESS_KEY_ID; delete process.env.AWS_SECRET_ACCESS_KEY; assert.doesNotThrow( function( err ) { var device = deviceModule( { host:'XXXX.iot.us-east-1.amazonaws.com', protocol: 'wss', filename: './test/data/credentials' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device not throw an exception if using websocket protocol with filename specified in options as well as set in environment", function() { // // Verify that the device module does not throw an exception when provided // both options and environment variables // // it("does not throws exception", function() { process.env.AWS_ACCESS_KEY_ID='not a valid access key id'; process.env.AWS_SECRET_ACCESS_KEY='not a valid secret access key'; assert.doesNotThrow( function( err ) { var device = deviceModule( { host:'XXXX.iot.us-east-1.amazonaws.com', protocol: 'wss', filename: './test/data/credentials' } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device throws an exception if using websocket protocol with no host specified", function() { // // Verify that the device module throws an exception when configured for // websocket operation with no host specified. // it("throws exception", function() { assert.throws( function( err ) { process.env.AWS_ACCESS_KEY_ID='not a valid access key id'; process.env.AWS_SECRET_ACCESS_KEY='not a valid secret access key'; var device = deviceModule( { protocol: 'wss', debug: true } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device throws an exception if using websocket protocol with incorrect host specified", function() { // // Verify that the device module throws an exception when configured for // websocket operation with incorrect host specified. // it("throws exception", function() { assert.throws( function( err ) { process.env.AWS_ACCESS_KEY_ID='not a valid access key id'; process.env.AWS_SECRET_ACCESS_KEY='not a valid secret access key'; var device = deviceModule( { host:'not-a-valid-host.com', protocol: 'wss', debug: true } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device does not throw exception if using websocket protocol with IAM credentials in environment", function() { // // Verify that the device module will not throw an exception when correctly // configured for websocket operation. // it("does not throw an exception", function() { assert.doesNotThrow( function( err ) { process.env.AWS_ACCESS_KEY_ID='not a valid access key id'; process.env.AWS_SECRET_ACCESS_KEY='not a valid secret access key'; var device = deviceModule( { host:'XXXX.iot.us-east-1.amazonaws.com', protocol: 'wss', debug: true } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device does not throw exception if using websocket protocol with IAM credentials in class options", function() { // // Verify that the device module will not throw an exception when correctly // configured for websocket operation. // it("does not throw an exception", function() { assert.doesNotThrow( function( err ) { var device = deviceModule( { host:'XXXX.iot.us-east-1.amazonaws.com', protocol: 'wss', debug: true, accessKeyId: 'not a valid access key id', secretKey: 'not a valid secret access key', sessionToken: 'not a valid session token', } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "coverage: device doesn't throw exception if using websocket protocol with IAM credentials", function() { // // Verify that the device module will not throw an exception when correctly // configured for websocket operation. // it("does not throw an exception", function() { assert.doesNotThrow( function( err ) { deviceModule.prepareWebSocketUrl( { host:'XXXX.iot.us-east-1.amazonaws.com', debug: true }, 'not a valid access key', 'not a valid secret access key' ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe("device does not throw exception if using CustomAuth with valid headers", function () { it("does not throw an exception", function () { assert.doesNotThrow(function (err) { var device = deviceModule({ host: 'XXXX.iot.us-east-1.amazonaws.com', protocol: 'wss-custom-auth', customAuthHeaders: { 'X-Amz-CustomAuthorizer-Name': 'AuthorizerFunctionName', 'X-Amz-CustomAuthorizer-Signature': 'Signature', 'NPAuthorizerToken': 'Token' } }); }, function (err) { console.log('\t[' + err + ']'); return true; } ); }); }); describe("device throws exception if using CustomAuth over websocket without headers", function () { it("throws exception", function () { assert.throws(function (err) { var device = deviceModule({ host: 'XXXX.iot.us-east-1.amazonaws.com', protocol: 'wss-custom-auth' }); }, function (err) { console.log('\t[' + err + ']'); return true; } ); }); }); describe("device does not throw exception if using CustomAuth over websocket with non-standard headers", function () { it("does not throw an exception", function () { assert.doesNotThrow(function (err) { var device = deviceModule({ host: 'XXXX.iot.us-east-1.amazonaws.com', protocol: 'wss-custom-auth', customAuthHeaders: { 'Custom-Header-1': 'Value1', 'Custom-Header-2': 'Value2', } }); }, function (err) { console.log('\t[' + err + ']'); return true; } ); }); }); describe( "device doesn't accept invalid timing parameters: baseReconnectTimeMs<1", function() { it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', baseReconnectTimeMs:-1 } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device doesn't accept invalid timing parameters: minimumConnectionTimeMs<baseReconnectTimeMs", function() { it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', baseReconnectTimeMs:1000, minimumConnectionTimeMs:500 } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device doesn't accept invalid timing parameters: maximumReconnectTimeMs<baseReconnectTimeMs", function() { it("throws an exception", function() { assert.throws( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', baseReconnectTimeMs:1000, minimumConnectionTimeMs:2500, maximumReconnectTimeMs:500 } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device accepts valid timing parameters", function() { it("does not throw an exception", function() { assert.doesNotThrow( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', baseReconnectTimeMs:1000, minimumConnectionTimeMs:2500, maximumReconnectTimeMs:5000 } ); }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); describe( "device passes default keepalive time correctly", function(){ it("does not throw an exception", function() { assert.doesNotThrow( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' }); }, function(err) { console.log('\t['+err+']'); return true;} ); assert.equal(mqttSave.firstCall.args[1].keepalive, 300); }); }); describe( "device override default keepalive time when specified in options", function(){ it("does not throw an exception", function(){ assert.doesNotThrow( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', keepalive:600 }); }, function(err) { console.log('\t['+err+']'); return true;} ); assert.equal(mqttSave.firstCall.args[1].keepalive, 600); }); }); describe( "device passes default username in options correctly", function(){ it("does not throw an exception", function(){ var metricPrefix = "?SDK=JavaScript&Version="; var pjson = require('../package.json'); assert.doesNotThrow( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com' }); }, function(err) { console.log('\t['+err+']'); return true;} ); assert.equal(mqttSave.firstCall.args[1].username, metricPrefix + pjson.version); }); }); describe( "device does not passes default username when metics is disabled", function(){ it("does not throw an exception", function(){ assert.doesNotThrow( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', enableMetrics:false }); }, function(err) { console.log('\t['+err+']'); return true;} ); assert.equal(mqttSave.firstCall.args[1].username, undefined); }); }); describe( "Correct username is passed when user specified in options ", function(){ it("does not throw an exception", function(){ var metricPrefix = "?SDK=JavaScript&Version="; var pjson = require('../package.json'); assert.doesNotThrow( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', username:'dummy-user-name' }); }, function(err) { console.log('\t['+err+']'); return true;} ); assert.equal(mqttSave.firstCall.args[1].username, 'dummy-user-name' + metricPrefix + pjson.version); }); }); describe( "Username will be concatenated if customer enable metrics but also provide username ", function(){ it("does not throw an exception", function(){ var metricPrefix = "?SDK=JavaScript&Version="; var pjson = require('../package.json'); assert.doesNotThrow( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', enableMetrics: true, username:'dummy-user-name' }); }, function(err) { console.log('\t['+err+']'); return true;} ); assert.equal(mqttSave.firstCall.args[1].username, 'dummy-user-name' + metricPrefix + pjson.version); }); }); describe( "Username will be overriden if customer disable metrics but provide username ", function(){ it("does not throw an exception", function(){ assert.doesNotThrow( function( err ) { var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', enableMetrics: false, username:'dummy-user-name' }); }, function(err) { console.log('\t['+err+']'); return true;} ); assert.equal(mqttSave.firstCall.args[1].username, 'dummy-user-name'); }); }); describe( "device handles reconnect timing correctly", function() { var clock; before( function() { clock = sinon.useFakeTimers(); } ); after( function() { clock.restore(); } ); it ("sets the reconnect period appropriately", function() { assert.doesNotThrow( function( err ) { // // Constants reconnection quiet time constants used in this test. // const baseReconnectTimeMs = 1000; const minimumConnectionTimeMs = 2500; const maximumReconnectTimeMs = 128000; var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', reconnectPeriod:baseReconnectTimeMs, minimumConnectionTimeMs:minimumConnectionTimeMs, maximumReconnectTimeMs:maximumReconnectTimeMs } ); // // Check reconnection timing and progression to maximum. // mockMQTTClientObject.emit('connect'); mockMQTTClientObject.emit('offline'); mockMQTTClientObject.emit('close'); for (i = 0, currentReconnectTimeMs = baseReconnectTimeMs*2; i < 7; i++, currentReconnectTimeMs*=2) { mockMQTTClientObject.emit('reconnect'); mockMQTTClientObject.emit('close'); mockMQTTClientObject.emit('offline'); assert.equal(mockMQTTClientObject.options.reconnectPeriod, currentReconnectTimeMs); } currentReconnectTimeMs/=2; for (i = 0; i < 4; i++) { mockMQTTClientObject.emit('reconnect'); mockMQTTClientObject.emit('close'); mockMQTTClientObject.emit('offline'); assert.equal(mockMQTTClientObject.options.reconnectPeriod, currentReconnectTimeMs); } // // Check that an unstable connection doesn't reset the reconnection progression // timing. // mockMQTTClientObject.emit('connect'); clock.tick( minimumConnectionTimeMs-1 ); mockMQTTClientObject.emit('offline'); mockMQTTClientObject.emit('close'); clock.tick( minimumConnectionTimeMs ); // make sure timer was cleared mockMQTTClientObject.emit('reconnect'); assert.equal(mockMQTTClientObject.options.reconnectPeriod, currentReconnectTimeMs); // // Check that a stable connection resets the reconnection progression timing. // mockMQTTClientObject.emit('connect'); clock.tick( minimumConnectionTimeMs+1 ); assert.equal(mockMQTTClientObject.options.reconnectPeriod, baseReconnectTimeMs); // // And check that it progresses correctly again... // mockMQTTClientObject.emit('close'); mockMQTTClientObject.emit('connect'); mockMQTTClientObject.emit('offline'); mockMQTTClientObject.emit('close'); for (i = 0, currentReconnectTimeMs = baseReconnectTimeMs*2; i < 7; i++, currentReconnectTimeMs*=2) { mockMQTTClientObject.emit('reconnect'); mockMQTTClientObject.emit('close'); mockMQTTClientObject.emit('offline'); assert.equal(mockMQTTClientObject.options.reconnectPeriod, currentReconnectTimeMs); } currentReconnectTimeMs/=2; for (i = 0; i < 4; i++) { mockMQTTClientObject.emit('reconnect'); mockMQTTClientObject.emit('close'); mockMQTTClientObject.emit('offline'); assert.equal(mockMQTTClientObject.options.reconnectPeriod, currentReconnectTimeMs); } }, function(err) { console.log('\t['+err+']'); return true;} ); }); }); // // Verify that events from the mqtt client are propagated upwards // describe("Ensure that events are propagated upwards", function() { it("should emit the corresponding events", function() { // Reinit mockMQTTClientObject mockMQTTClientObject.reInitCommandCalled(); var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', baseReconnectTimeMs:1000, minimumConnectionTimeMs:2500, maximumReconnectTimeMs:128000 } ); // Register fake callbacks var fakeCallback1 = sinon.spy(); var fakeCallback2 = sinon.spy(); var fakeCallback3 = sinon.spy(); var fakeCallback4 = sinon.spy(); var fakeCallback5 = sinon.spy(); var fakeCallback6 = sinon.spy(); var fakeCallback7 = sinon.spy(); var fakeCallback8 = sinon.spy(); device.on('connect', fakeCallback1); device.on('close', fakeCallback2); device.on('reconnect', fakeCallback3); device.on('offline', fakeCallback4); device.on('error', fakeCallback5); device.on('message', fakeCallback6); device.on('packetsend', fakeCallback7); device.on('packetreceive', fakeCallback8); // Now emit messages mockMQTTClientObject.emit('connect'); assert(fakeCallback1.calledOnce); sinon.assert.notCalled(fakeCallback2); sinon.assert.notCalled(fakeCallback3); sinon.assert.notCalled(fakeCallback4); sinon.assert.notCalled(fakeCallback5); sinon.assert.notCalled(fakeCallback6); sinon.assert.notCalled(fakeCallback7); sinon.assert.notCalled(fakeCallback8); mockMQTTClientObject.emit('close'); assert(fakeCallback1.calledOnce); assert(fakeCallback2.calledOnce); sinon.assert.notCalled(fakeCallback3); sinon.assert.notCalled(fakeCallback4); sinon.assert.notCalled(fakeCallback5); sinon.assert.notCalled(fakeCallback6); sinon.assert.notCalled(fakeCallback7); sinon.assert.notCalled(fakeCallback8); mockMQTTClientObject.emit('reconnect'); assert(fakeCallback1.calledOnce); assert(fakeCallback2.calledOnce); assert(fakeCallback3.calledOnce); sinon.assert.notCalled(fakeCallback4); sinon.assert.notCalled(fakeCallback5); sinon.assert.notCalled(fakeCallback6); sinon.assert.notCalled(fakeCallback7); sinon.assert.notCalled(fakeCallback8); mockMQTTClientObject.emit('offline'); assert(fakeCallback1.calledOnce); assert(fakeCallback2.calledOnce); assert(fakeCallback3.calledOnce); assert(fakeCallback4.calledOnce); sinon.assert.notCalled(fakeCallback5); sinon.assert.notCalled(fakeCallback6); sinon.assert.notCalled(fakeCallback7); sinon.assert.notCalled(fakeCallback8); mockMQTTClientObject.emit('error'); assert(fakeCallback1.calledOnce); assert(fakeCallback2.calledOnce); assert(fakeCallback3.calledOnce); assert(fakeCallback4.calledOnce); assert(fakeCallback5.calledOnce); sinon.assert.notCalled(fakeCallback6); sinon.assert.notCalled(fakeCallback7); sinon.assert.notCalled(fakeCallback8); mockMQTTClientObject.emit('packetsend'); assert(fakeCallback1.calledOnce); assert(fakeCallback2.calledOnce); assert(fakeCallback3.calledOnce); assert(fakeCallback4.calledOnce); assert(fakeCallback5.calledOnce); sinon.assert.notCalled(fakeCallback6); assert(fakeCallback7.calledOnce); sinon.assert.notCalled(fakeCallback8); mockMQTTClientObject.emit('packetreceive'); assert(fakeCallback1.calledOnce); assert(fakeCallback2.calledOnce); assert(fakeCallback3.calledOnce); assert(fakeCallback4.calledOnce); assert(fakeCallback5.calledOnce); sinon.assert.notCalled(fakeCallback6); assert(fakeCallback7.calledOnce); assert(fakeCallback8.calledOnce); mockMQTTClientObject.emit('message'); assert(fakeCallback1.calledOnce); assert(fakeCallback2.calledOnce); assert(fakeCallback3.calledOnce); assert(fakeCallback4.calledOnce); assert(fakeCallback5.calledOnce); assert(fakeCallback6.calledOnce); assert(fakeCallback7.calledOnce); assert(fakeCallback8.calledOnce); }); }); // // Verify that the end and handleMessage APIs are passed-through // describe("Ensure that the end and handleMessage APIs are overriding", function() { var clock; before( function() { clock = sinon.useFakeTimers(); } ); after( function() { clock.restore(); } ); it("should call the corresponding methods in mqtt", function() { // Reinit mockMQTTClientObject mockMQTTClientObject.reInitCommandCalled(); var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', baseReconnectTimeMs:1000, minimumConnectionTimeMs:2500, maximumReconnectTimeMs:128000 } ); mockMQTTClientObject.emit('connect'); device.end( false, null ); assert.equal(mockMQTTClientObject.commandCalled['end'], 1); // Called once // simulate overriding handleMessage var expectedPacket = { data: 'packet data' }; var calledOverride = 0; var calledBack = 0; device.handleMessage = function customHandleMessage(packet, callback) { calledOverride++; assert.deepEqual(packet, expectedPacket); callback(); } mockMQTTClientObject.handleMessage(expectedPacket, function() { calledBack++; assert.equal(calledOverride, 1); assert.equal(calledBack, 1); }) assert.equal(mockMQTTClientObject.commandCalled['end'], 1); // Called once }); }); // // Verify that subscriptions are sent to the mqtt client only after // the connection has been established. // describe("Verify that subscriptions are automatically renewed after connection established", function() { var clock; before( function() { clock = sinon.useFakeTimers(); } ); after( function() { clock.restore(); } ); it("should renew subscriptions after re-connecting", function() { // Test parameters var drainTimeMs = 250; // Reinit mockMQTTClientObject mockMQTTClientObject.reInitCommandCalled(); var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', baseReconnectTimeMs:1000, minimumConnectionTimeMs:20000, maximumReconnectTimeMs:128000, drainTimeMs: drainTimeMs, } ); device.subscribe( 'topic1', { }, null ); device.subscribe( 'topic2', { }, null ); device.subscribe( 'topic3', { }, null ); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 0); // Connection not yet established mockMQTTClientObject.emit('connect'); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 0); // Connection not yet established clock.tick( drainTimeMs * 3 ); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 3); // Connection established, subscriptions sent assert.equal(mockMQTTClientObject.subscriptions.shift(), 'topic1'); assert.equal(mockMQTTClientObject.subscriptions.shift(), 'topic2'); assert.equal(mockMQTTClientObject.subscriptions.shift(), 'topic3'); device.unsubscribe('topic2' ); mockMQTTClientObject.emit('close'); mockMQTTClientObject.emit('connect'); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 3); // Connection not yet established assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 3); // Connection not yet established clock.tick( drainTimeMs * 2 ); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 5); // Connection established assert.equal(mockMQTTClientObject.subscriptions.shift(), 'topic1'); assert.equal(mockMQTTClientObject.subscriptions.shift(), 'topic3'); device.subscribe( [ 'arrayTopic1', 'arrayTopic2', 'arrayTopic3', 'arrayTopic4' ], { }, null ); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 6); // Connection established assert.equal(mockMQTTClientObject.subscriptions.shift(), 'arrayTopic1'); assert.equal(mockMQTTClientObject.subscriptions.shift(), 'arrayTopic2'); assert.equal(mockMQTTClientObject.subscriptions.shift(), 'arrayTopic3'); assert.equal(mockMQTTClientObject.subscriptions.shift(), 'arrayTopic4'); mockMQTTClientObject.emit('close'); device.unsubscribe('arrayTopic2' ); device.unsubscribe('arrayTopic4' ); mockMQTTClientObject.emit('connect'); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 6); // Connection not yet established clock.tick( drainTimeMs * 4 ); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 10); // Connection established }); }); // // Verify that array subscriptions sent when offline are queued as an array request and // then later sent as an array subscribe. // describe("Verify that array subscriptions are queued as arrays", function() { var clock; before( function() { clock = sinon.useFakeTimers(); } ); after( function() { clock.restore(); } ); it("should queue array subs as arrays", function() { // Test parameters var drainTimeMs = 250; // Reinit mockMQTTClientObject mockMQTTClientObject.reInitCommandCalled(); var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', baseReconnectTimeMs:1000, minimumConnectionTimeMs:20000, maximumReconnectTimeMs:128000, drainTimeMs: drainTimeMs, } ); device.subscribe( ['aTopic1','aTopic2','aTopic3'], { }, null ); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 0); // Connection not yet established mockMQTTClientObject.emit('connect'); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 0); // Connection not yet established clock.tick( drainTimeMs * 1 ); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 1); // Connection established, subscriptions sent assert.equal(mockMQTTClientObject.subscriptions.shift(), 'aTopic1'); // one subscribe request assert.equal(mockMQTTClientObject.subscriptions.shift(), 'aTopic2'); // but assert.equal(mockMQTTClientObject.subscriptions.shift(), 'aTopic3'); // three topics seen in client }); }); // // Verify subscribes and unsubscribes are queued when offline // describe("Verify subscribes and unsubscribes are queued when offline", function() { var clock; before( function() { clock = sinon.useFakeTimers(); } ); after( function() { clock.restore(); } ); it("should queue subs and unusbs", function() { // Test parameters var drainTimeMs = 250; // Reinit mockMQTTClientObject mockMQTTClientObject.reInitCommandCalled(); var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', baseReconnectTimeMs:1000, minimumConnectionTimeMs:20000, maximumReconnectTimeMs:128000, drainTimeMs: drainTimeMs, } ); device.subscribe('topic1', { }, null ); device.subscribe('topic2', { }, null ); device.subscribe('topic3', { }, null ); device.unsubscribe('topic2', null ); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 0); assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 0); mockMQTTClientObject.emit('connect'); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 0); assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 0); clock.tick( drainTimeMs * 3 ); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 3); assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 0); assert.equal(mockMQTTClientObject.subscriptions.shift(), 'topic1'); assert.equal(mockMQTTClientObject.subscriptions.shift(), 'topic2'); assert.equal(mockMQTTClientObject.subscriptions.shift(), 'topic3'); clock.tick( drainTimeMs * 1 ); assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 3); assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 1); }); }); // // Verify offline subscription queue is not unlimited // describe("Verify offline subscription queue is not unlimited", function() { var clock; before( function() { clock = sinon.useFakeTimers(); } ); after( function() { clock.restore(); } ); it("should only queue maximum sub/unsub operations", function() { // Test parameters var drainTimeMs = 250; // Reinit mockMQTTClientObject mockMQTTClientObject.reInitCommandCalled(); var device = deviceModule( { certPath:'test/data/certificate.pem.crt', keyPath:'test/data/private.pem.key', caPath:'test/data/root-CA.crt', clientId:'dummy-client-1', host:'XXXX.iot.us-east-1.amazonaws.com', baseReconnectTimeMs:1000, minimumConnectionTimeMs:20000, maximumReconnectTimeMs:128000, drainTimeMs: drainTimeMs, } ); var fakeErrorCallback = sinon.spy(); device.on('error', fakeErrorCallback); for (var i=0; i<25; ++i) { device.subscribe('subtopic' + i, { }, null ); device.unsubscribe('unsubtopic' + i, null ); } sinon.assert.notCalled(fakeErrorCallback); // we're at 50 operations, no error yet device.subscribe('topic1', { }, null ); // one more assert(fakeErrorCallback.calledOnce); // now we got an error }); }); // // Verify subscribe callback called on subscribe but not on resubscribe // describe("Verify subscribe callback called on subscribe but not on resubscribe", function() { var clock; before( function() { clock = sinon.useFakeTimers(); } ); after( function() { clock.restore(); } ); it("should callback on sub, not resub", function