aws-iot-device-sdk
Version:
AWS IoT Node.js SDK for Embedded Devices
1,136 lines (1,078 loc) • 76.7 kB
JavaScript
/*
* 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
//npm deps
//app deps
var assert = require('assert');
var sinon = require('sinon');
var device = require('../device');
var mockMQTTClient = require('./mock/mockMQTTClient');
describe( "thing shadow class unit tests", function() {
var mockMQTTClientObject;
var mqttSave;
beforeEach( function () {
// Mock the connect API for mqtt.js
var fakeConnect = function(options) {
mockMQTTClientObject = new mockMQTTClient(); // return the mocking object
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
return mockMQTTClientObject;
};
mqttSave = sinon.stub(device, 'DeviceClient', fakeConnect);
});
afterEach( function () {
mqttSave.restore();
});
var thingShadow = require('..').thingShadow;
// Test cases begin
describe( "register a thing shadow name", function() {
//
// Verify that the thing shadow module does not throw an exception
// when all connection parameters are specified and we register and
// unregister thing shadows.
//
it("does not throw an exception", function() {
assert.doesNotThrow( function( err ) {
var thingShadows = thingShadow( {
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'
} );
thingShadows.register( 'testShadow1' );
thingShadows.unregister( 'testShadow1' );
}, function(err) { console.log('\t['+err+']'); return true; }
);
});
});
describe( "register a thing shadow name", function() {
//
// Verify that the thing shadow invokes the register callback when subscription to all
// topics are finished. The callback is invoked based on the callback from the mqtt library.
//
var thingShadowsConfig = {
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'
};
it("should trigger error when a subscription fails", function () {
var stubTriggerError = sinon.stub(mockMQTTClient, 'triggerError', function(){return true;});
var thingShadows = thingShadow(thingShadowsConfig);
thingShadows.register('testShadow1', { ignoreDeltas: true, persistentSubscribe: true }, function (err, granted) {
assert.notEqual(err, null);
for (var k = 0, grantedLen = granted.length; k < grantedLen; k++) {
//
// 128 is 0x80 - Failure from the MQTT lib.
//
assert.equal(granted[k].qos, 128);
stubTriggerError.restore();
}
});
var thisToken = thingShadows.update('testShadow1', {}); // update will fail as register is still pending
assert.equal(thisToken, null);
});
it("should trigger callback when ignoreDeltas is true and persistentSubscribe is true", function() {
var thingShadows = thingShadow( thingShadowsConfig );
var fakeCallback = sinon.spy();
thingShadows.register( 'testShadow1', {ignoreDeltas:true, persistentSubscribe:true}, fakeCallback);
assert(fakeCallback.calledOnce);
});
it("should trigger callback when ignoreDeltas is false and persistentSubscribe is false", function() {
var thingShadows = thingShadow( thingShadowsConfig );
var fakeCallback = sinon.spy();
thingShadows.register( 'testShadow1', {ignoreDeltas:false, persistentSubscribe:false}, fakeCallback);
assert(fakeCallback.calledOnce);
});
it("should trigger callback when ignoreDeltas is true and persistentSubscribe is false", function() {
var thingShadows = thingShadow( thingShadowsConfig );
var fakeCallback = sinon.spy();
thingShadows.register( 'testShadow1', {ignoreDeltas:true, persistentSubscribe:false}, fakeCallback);
assert(fakeCallback.calledOnce);
});
it("should trigger callback when ignoreDeltas is false and persistentSubscribe is true", function() {
var thingShadows = thingShadow( thingShadowsConfig );
var fakeCallback = sinon.spy();
thingShadows.register( 'testShadow1', {ignoreDeltas:false, persistentSubscribe:true}, fakeCallback);
assert(fakeCallback.calledOnce);
});
it("should trigger callback when shadow option is not provided", function() {
var thingShadows = thingShadow( thingShadowsConfig );
var fakeCallback = sinon.spy();
thingShadows.register( 'testShadow1', fakeCallback);
assert(fakeCallback.calledOnce);
});
});
describe( "subscribe to/unsubscribe from a non-thing topic", function() {
//
// Verify that the thing shadow module does not throw an exception
// when we subscribe to and unsubscribe from a non-thing topic.
//
it("does not throw an exception", function() {
var fakeCallback1 = sinon.spy();
var fakeCallback2 = sinon.spy();
assert.doesNotThrow( function( err ) {
var thingShadows = thingShadow( {
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'
} );
thingShadows.subscribe('nonThingTopic1', {}, fakeCallback1);
thingShadows.unsubscribe('nonThingTopic1', fakeCallback2);
}, function(err) { console.log('\t['+err+']'); return true;}
);
assert(fakeCallback1.calledOnce);
assert(fakeCallback2.calledOnce);
});
});
describe( "subscribe to/unsubscribe from a non-thing topic array", function() {
//
// Verify that the thing shadow module does not throw an exception
// when we subscribe to and unsubscribe from a non-thing topic.
//
it("does not throw an exception", function() {
var fakeCallback1 = sinon.spy();
var fakeCallback2 = sinon.spy();
var topicArray = [];
assert.doesNotThrow( function( err ) {
var thingShadows = thingShadow( {
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'
} );
var MAX_TOPIC_ARRAY_SIZE = 8;
for (var i = 1; i <= MAX_TOPIC_ARRAY_SIZE; i++) {
var topicName = 'nonThingTopic' + i;
topicArray.push(topicName);
}
thingShadows.subscribe(topicArray, {}, fakeCallback1);
thingShadows.unsubscribe(topicArray, fakeCallback2);
}, function(err) { console.log('\t['+err+']'); return true;}
);
assert(fakeCallback1.calledOnce);
assert(fakeCallback2.calledOnce);
for (var i = 0; i < topicArray.length; i++) {
var topicName = topicArray.shift();
assert.equal(mockMQTTClientObject.subscriptions.shift(), topicName);
assert.equal(mockMQTTClientObject.unsubscriptions.shift(), topicName);
}
});
});
describe( "publish to a non-thing topic", function() {
//
// Verify that the thing shadow module does not throw an exception
// when we publish to a non-thing topic.
//
it("does not throw an exception", function() {
assert.doesNotThrow( function( err ) {
var thingShadows = thingShadow( {
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'
} );
thingShadows.publish( 'nonThingTopic1', { data: 'value' } );
}, function(err) { console.log('\t['+err+']'); return true;}
);
});
});
describe( "subscribe to an illegal non-thing topic", function() {
//
// Verify that the thing shadow module throws an exception if we
// attempt to subscribe to an illegal non-thing topic.
//
it("throws an exception", function() {
assert.throws( function( err ) {
var thingShadows = thingShadow( {
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'
} );
thingShadows.subscribe( '$aws/things/nonThingTopic1' );
}, function(err) { console.log('\t['+err+']'); return true;}
);
});
});
describe( "subscribe to an topic array which contains an illegal non-thing topic", function() {
//
// Verify that the thing shadow module throws an exception if we
// attempt to subscribe to an illegal non-thing topic.
//
it("throws an exception", function() {
assert.throws( function( err ) {
var thingShadows = thingShadow( {
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'
} );
thingShadows.subscribe( ['topic1', '$aws/things/nonThingTopic1', 'topic2']);
}, function(err) { console.log('\t['+err+']'); return true;}
);
});
});
describe( "publish to an illegal non-thing topic", function() {
//
// Verify that the thing shadow module throws an exception if we
// attempt to publish to an illegal non-thing topic.
//
it("throws an exception", function() {
assert.throws( function( err ) {
var thingShadows = thingShadow( {
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'
} );
thingShadows.publish( '$aws/things/nonThingTopic1',
{ data: 'value' } );
}, function(err) { console.log('\t['+err+']'); return true;}
);
});
});
describe( "unsubscribe from an illegal non-thing topic", function() {
//
// Verify that the thing shadow module throws an exception if we
// attempt to unsubscribe from an illegal non-thing topic.
//
it("throws an exception", function() {
assert.throws( function( err ) {
var thingShadows = thingShadow( {
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'
} );
thingShadows.unsubscribe( '$aws/things/nonThingTopic1' );
}, function(err) { console.log('\t['+err+']'); return true;}
);
});
});
describe( "unsubscribe from an array which contains illegal non-thing topic", function() {
//
// Verify that the thing shadow module throws an exception if we
// attempt to unsubscribe from an illegal non-thing topic.
//
it("throws an exception", function() {
assert.throws( function( err ) {
var thingShadows = thingShadow( {
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'
} );
thingShadows.unsubscribe( ['topic1', '$aws/things/nonThingTopic1', 'topic2'] );
}, function(err) { console.log('\t['+err+']'); return true;}
);
});
});
//
// Verify that the thing shadow module does not throw an exception
// when the end() method is invoked.
//
describe( "end method does not throw an exception", function() {
it("does not throw an exception", function() {
assert.doesNotThrow( function( err ) {
var thingShadows = thingShadow( {
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'
} );
thingShadows.end( true );
}, function(err) { console.log('\t['+err+']'); return true;}
);
});
});
/**** shadow register/unregister ****/
//
// Verify that the corresponding delta topic is subscribed after the registration of a thing shadow
// if the user is interested in delta (default), and is unsubscribed when unregistered.
//
describe("Thing shadow registration/unregistration", function(){
it("properly subscribes/unsubscribes to delta topic", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
// Init thingShadowsClient
var thingShadows = thingShadow( {
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'
} );
// Register a thing, using default delta settings
thingShadows.register('testShadow1');
assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 1); // Called once for GUD + Delta
mockMQTTClientObject.reInitCommandCalled();
thingShadows.unregister('testShadow1');
assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 1);
});
});
//
// Verify that the delta topic is never subscribed when the option ignoreDeltas is set to be true
//
describe("Thing shadow registration with ignoreDeltas set to be true", function() {
it("never subscribes to delta topic", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
// Init thingShadowClient
var thingShadows = thingShadow( {
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',
debug: true
} );
// Register a thing, using default delta settings
thingShadows.register('testShadow1', {ignoreDeltas:true});
assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 1); // Called once, for GUD
// Register it again and make sure no additional subscriptions
// were generated; this will also generate a console warning
// since the device was instantiated with debug===true
thingShadows.register('testShadow1', {ignoreDeltas:true});
assert.equal(mockMQTTClientObject.commandCalled['subscribe'], 1); // Called once, for GUD
mockMQTTClientObject.reInitCommandCalled();
thingShadows.unregister('testShadow1');
assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 1); // Called once for all
});
});
//
// Verify that registering a thing shadow with malformed inputs should be ignored.
//
describe("Thing shadow registration with malformed options", function() {
it("should properly ignore them", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
assert.doesNotThrow(function(err) {
thingShadows.register('testShadow1', {troubleMaker:123});
thingShadows.unregister('testShadow1');
}, function(err) {console.log('\t['+err+']'); return true;}
);
});
});
//
// Verify that unregistering a thing shadow that is never registered is ignored.
//
describe("Unregister a thing shadow that is never registered", function() {
it("should properly ignore it", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
assert.doesNotThrow(function(err) {
thingShadows.unregister('IamNeverRegistered');
}, function(err) {console.log('\t['+err+']'); return true;});
});
});
//
// Verify that new delta messages with bigger version number triggers the callback.
//
describe("Incoming delta message with bigger version number", function() {
it("should call the corresponding callback", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a thing
thingShadows.register('testShadow2');
// Register a fake callback
var fakeCallback = sinon.spy();
thingShadows.on('delta', fakeCallback);
// Now emit a message from delta topic
mockMQTTClientObject.emit('message', '$aws/things/testShadow2/shadow/update/delta', '{"version":3}');
// Now emit another message from delta topic again, with bigger version number
mockMQTTClientObject.emit('message', '$aws/things/testShadow2/shadow/update/delta', '{"version":5}');
// Check spy
assert(fakeCallback.calledTwice);
// clean up
thingShadows.unregister('testShadow2');
});
});
//
// Verify that new delta message with smaller version number does not trigger the callback.
//
describe("Incoming delta message with smaller version number", function() {
it("should never call callback", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
// Init thingShadowClient
var thingShadows = thingShadow( {
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',
debug: true
} );
// Register a thing
thingShadows.register('testShadow2');
// Register a fake callback
var fakeCallback = sinon.spy();
thingShadows.on('delta', fakeCallback);
// Now emit a message from delta topic
mockMQTTClientObject.emit('message', '$aws/things/testShadow2/shadow/update/delta', '{"version":3}');
// Now emit another message from delta topic again, with bigger version number
mockMQTTClientObject.emit('message', '$aws/things/testShadow2/shadow/update/delta', '{"version":1}');
// Check spy
assert(fakeCallback.calledOnce);
// clean up
thingShadows.unregister('testShadow2');
});
});
//
// Verify that the delta message from some unregistered thing does not trigger the callback.
//
describe("Incoming delta message from unregistered thing", function() {
it("should never call callback", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a thing
thingShadows.register('testShadow2');
// Register a fake callback
var fakeCallback = sinon.spy();
thingShadows.on('delta', fakeCallback);
// Now emit a message from delta topic for some other thing
mockMQTTClientObject.emit('message', '$aws/things/IamNeverRegistered/shadow/update/delta', '{"version":3}');
// Check spy
sinon.assert.notCalled(fakeCallback);
// clean up
thingShadows.unregister('testShadow2');
});
});
/**** shadow get/delete ****/
//
// Verify that a message without clientToken is properly published
// when a shadow Get request is issued.
//
describe("No token is specified for shadow Get/Delete", function() {
it("should generate a token", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Faking timer
var clock = sinon.useFakeTimers();
// Faking callback
var fakeCallback = sinon.spy();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
}, {
operationTimeout:1000 // Set operation timeout to be 1 sec
} );
// Register callback
thingShadows.on('timeout', fakeCallback);
// Register a thing
thingShadows.register('testShadow3');
// Fire a shadow get
var thisToken = thingShadows.get('testShadow3');
clock.tick(3000); // 3 sec later...
assert(fakeCallback.calledOnce);
assert.equal(mockMQTTClientObject.lastPublishedMessage, '{"clientToken":"dummy-client-1-0"}');
mockMQTTClientObject.resetPublishedMessage();
thatToken = thingShadows.delete('testShadow3');
clock.tick(3000); // 3 sec later...
assert(fakeCallback.calledTwice);
assert.equal(mockMQTTClientObject.lastPublishedMessage, '{"clientToken":"dummy-client-1-1"}');
assert.equal(mockMQTTClientObject.commandCalled['publish'], 2);
// Unregister it
thingShadows.unregister('testShadow3');
});
});
//
// Verify that a message containing clientToken is properly published
// when a shadow Get request is issued.
//
describe("User clientToken is specified for shadow Get/Delete", function() {
it("should keep and use the user token", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Faking timer
var clock = sinon.useFakeTimers();
// Faking callback
var fakeCallback = sinon.spy();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
}, {
operationTimeout:1000 // Set operation timeout to be 1 sec
} );
// Register callback
thingShadows.on('timeout', fakeCallback);
// Register a thing
thingShadows.register('testShadow3');
// Fire a shadow get
var thisToken = thingShadows.get('testShadow3', 'CoolToken1');
clock.tick(3000); // 3 sec later...
assert(fakeCallback.calledOnce);
assert.equal(mockMQTTClientObject.lastPublishedMessage, '{"clientToken":"CoolToken1"}');
mockMQTTClientObject.resetPublishedMessage();
thatToken = thingShadows.delete('testShadow3', 'CoolToken2');
clock.tick(3000); // 3 sec later...
assert(fakeCallback.calledTwice);
assert.equal(mockMQTTClientObject.lastPublishedMessage, '{"clientToken":"CoolToken2"}');
assert.equal(mockMQTTClientObject.commandCalled['publish'], 2);
// Unregister it
thingShadows.unregister('testShadow3');
});
});
//
// Verify that a proper incoming message triggers the callback for shadow Get/Delete accepted/rejected.
//
describe("A proper incoming message for shadow Get/Delete is received (accepted/rejected)", function() {
it("should call status callback", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Faking callbacks
var fakeCallback = sinon.spy();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a callback
thingShadows.on('status', fakeCallback);
// Register a thing
thingShadows.register('testShadow3');
// Get
thingShadows.get('testShadow3', 'CoolToken1');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/get/accepted', '{"clientToken":"CoolToken1", "version":2}');
assert(fakeCallback.calledOnce);
thingShadows.get('testShadow3', 'CoolToken2');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/get/rejected', '{"clientToken":"CoolToken2", "version":2}')
assert(fakeCallback.calledTwice);
// Delete
thingShadows.delete('testShadow3', 'CoolToken3');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/delete/accepted', '{"clientToken":"CoolToken3", "version":2}');
assert(fakeCallback.calledThrice);
thingShadows.delete('testShadow3', 'CoolToken4');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/delete/rejected', '{"clientToken":"CoolToken4", "version":2}');
assert.equal(fakeCallback.callCount, 4);
//
assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 0); // Never unsubscribe since persistent
// Unregister it
thingShadows.unregister('testShadow3');
});
});
//
// Verify that the related topics are properly unsubscribed when it is registered as
// NOT persistent subscribe.
//
describe("Shadow Get/Delete feedback for NOT persistent subscribe thing comes", function() {
it("should unsubscribe from related topics (accepted/rejected)", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a thing
thingShadows.register('testShadow3', {persistentSubscribe:false}); // Unsub once there is a feedback
// Get
thingShadows.get('testShadow3', 'CoolToken1');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/get/accepted', '{"clientToken":"CoolToken1", "version":2}');
assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 1);
// Delete
mockMQTTClientObject.reInitCommandCalled();
thingShadows.delete('testShadow3', 'CoolToken2');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/delete/accepted', '{"clientToken":"CoolToken2", "version":2}');
assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 1);
// Unregister it
thingShadows.unregister('testShadow3');
});
});
//
// Verify that timeout triggers the callback for shadow Get/Delete timeout.
//
describe("Get/Delete request timeout", function() {
it("should call timeout callback", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Faking timer
var clock = sinon.useFakeTimers();
// Faking callback
var fakeCallback = sinon.spy();
// Init thingShadowClient
var thingShadows = thingShadow( {
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',
}, {
operationTimeout:1000 // Set operation timeout to be 1 sec
} );
// Register callback
thingShadows.on('timeout', fakeCallback);
// Register a thing
thingShadows.register('testShadow3');
// Get
thingShadows.get('testShadow3', 'CoolToken1');
// Delete
clock.tick(3000); // 3 sec later...
assert(fakeCallback.calledOnce);
thingShadows.delete('testShadow3', 'CoolToken2');
clock.tick(3000); // 3 sec later...
assert(fakeCallback.calledTwice);
//
// Unregister it
thingShadows.unregister('testShadow3');
clock.restore();
});
});
//
// Verify that incoming messages with wrong/none token for shadow Get/Delete are ignored.
//
describe("Incoming messages for shadow Get/Delete are missing/messed up with token", function() {
it("should never call callback", function() {
// Faking callback
fakeCallback = sinon.spy();
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a callback
thingShadows.on('status', fakeCallback);
// Register a thing
thingShadows.register('testShadow3');
// Get
thingShadows.get('testShadow3');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/get/accepted', '{"clientToken":"Garbage1", "version":2}'); // wrong token
thingShadows.get('testShadow3');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/get/accepted', '{"version":2}'); // no token
// Delete
thingShadows.delete('testShadow3');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/delete/accepted', '{"clientToken":"Garbage2", "version":2}'); // wrong token
thingShadows.delete('testShadow3');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/delete/accepted', '{"version":2}'); // no token
// Check
sinon.assert.notCalled(fakeCallback); // Should never trigger the callback
// Unregister it
thingShadows.unregister('testShadow3');
});
});
//
// Verify that incoming message with out-of-date/none version for shadow Get/Delete are ignored.
//
describe("Incoming messages for shadow Get/Delete are missing/messed up with version", function() {
it("should never call callback", function() {
// Faking callback
fakeCallback = sinon.spy();
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a callback
thingShadows.on('status', fakeCallback);
// Register a thing
thingShadows.register('testShadow3');
// Get
thingShadows.get('testShadow3', 'CoolToken1');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/get/accepted', '{"clientToken":"CoolToken1", "version":3}');
fakeCallback.reset(); // Reset spy
thingShadows.get('testShadow3', 'CoolToken2');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/get/accepted', '{"clientToken":"CoolToken2", "version":1}'); // old version
// Delete
thingShadows.delete('testShadow3', 'CoolToken4');
mockMQTTClientObject.emit('message', '$aws/things/testShadow3/shadow/delete/accepted', '{"clientToken":"CoolToken4", "version":1}'); // old version
// Check
sinon.assert.notCalled(fakeCallback);
//Unregister it
thingShadows.unregister('testShadow3');
});
});
/**** shadow update ****/
//
// Verify that token can be generated for shadow update request
// when it is missing in the payload.
//
describe("Token is not specified for shadow Update", function() {
it("should generate the token and insert it into the payload to be published", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a thing
thingShadows.register('testShadow4');
// Generate fake payload
myPayload = '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}}}'; // No token
myStateObject = JSON.parse(myPayload);
// Update
var thisToken = thingShadows.update('testShadow4', myStateObject);
assert.equal(mockMQTTClientObject.lastPublishedMessage, '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"dummy-client-1-0"}');
// Unregister it
thingShadows.unregister('testShadow4');
});
});
//
// Verify that token can be provided by the user for shadw update request.
//
describe("Token is specified for shadow Update", function() {
it("should keep and use user token", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a thing
thingShadows.register('testShadow4');
// Generate fake payload
myPayload = '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"CoolToken1"}'; // No token
myStateObject = JSON.parse(myPayload);
// Update
var thisToken = thingShadows.update('testShadow4', myStateObject);
assert.equal(mockMQTTClientObject.lastPublishedMessage, '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"CoolToken1"}');
// Unregister it
thingShadows.unregister('testShadow4');
});
});
//
// Verify that proper incoming messages trigger the callback for shadow update, accepted and rejected.
//
describe("Proper incoming messages for shadow Update come", function() {
it("should call status callback", function() {
// Faking callback
fakeCallback = sinon.spy();
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a callback
thingShadows.on('status', fakeCallback);
// Register a thing
thingShadows.register('testShadow4');
// Generate fake payload
myPayload = '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"CoolToken1"}';
myStateObject = JSON.parse(myPayload);
// Update accepted
thingShadows.update('testShadow4', myStateObject);
mockMQTTClientObject.emit('message', '$aws/things/testShadow4/shadow/update/accepted', '{"clientToken":"CoolToken1","version":2}');
// Check
assert(fakeCallback.calledOnce);
// Reset
fakeCallback.reset();
// Update rejected
thingShadows.update('testShadow4', myStateObject);
mockMQTTClientObject.emit('message', '$aws/things/testShadow4/shadow/update/rejected', '{"clientToken":"CoolToken1","version":2}');
// Check
assert(fakeCallback.calledOnce);
// Unregister it
thingShadows.unregister('testShadow4');
});
});
//
// Verify that related topics are unsubscribed when the thing is registered as NOT persistent subscribe,
// for shadow update.
//
describe("Shadow Update request for NOT persistent subscribe thing", function() {
it("should unsubscribe from related topics (accepted/rejected)", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a thing
thingShadows.register('testShadow4', {persistentSubscribe:false}); // Unsub once there is a feedback
// Generate fake payload
myPayload = '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"CoolToken1"}';
myStateObject = JSON.parse(myPayload);
// Update
thingShadows.update('testShadow4', myStateObject);
mockMQTTClientObject.emit('message', '$aws/things/testShadow4/shadow/update/accepted', '{"clientToken":"CoolToken1", "version":2}');
assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 1);
// Unregister it
thingShadows.unregister('testShadow4');
});
});
//
// Verify that related topics are not unsubscribed on feedback for persistent subscription
//
describe("Feedback comes for persistent subscribed thing for shadow Update", function() {
it("should never unsubscribe to the related topics", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
});
// Register a thing
thingShadows.register('testShadow4');
// Generate fake payload
myPayload = '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"CoolToken1"}';
myStateObject = JSON.parse(myPayload);
// Update
thingShadows.update('testShadow4', myStateObject);
mockMQTTClientObject.emit('message', '$aws/things/testShadow4/shadow/update/accepted', '{"clientToken":"CoolToken1","version":7}'); // sync local version
// Check
assert.equal(mockMQTTClientObject.commandCalled['unsubscribe'], 0); // Never unsub
// Unregister it
thingShadows.unregister('testShadow4');
});
});
//
// Verify that update fails if subscriptions are not granted
//
describe("Verify that update fails if subscriptions are not granted", function() {
it("should fail if subscriptions fail", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
});
// Register a thing
thingShadows.register('testShadow4', {persistentSubscribe:false});
// Generate fake payload
myPayload = '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"CoolToken1"}';
myStateObject = JSON.parse(myPayload);
// cause subscribe error on update (we subscribe because we have elected to not be persistently subscribed)
var stubTriggerError = sinon.stub(mockMQTTClient, 'triggerError', function(){return true;});
// Update
thingShadows.update('testShadow4', myStateObject);
// Publish will not be called (error before updating state)
assert.equal(mockMQTTClientObject.commandCalled['publish'], 0);
// Unregister it
thingShadows.unregister('testShadow4');
// restore successful publishing
stubTriggerError.restore();
});
});
//
// Verify that version is added to the payload if it is available when the thing is registered as NOT persistent subscribe,
// for shadow update.
//
describe("Version available in local for NOT persistent subscribe thing for shadow Update", function() {
it("should include version in published payload", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Faking timer
var clock = sinon.useFakeTimers();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
});
// Register a thing
thingShadows.register('testShadow4', {persistentSubscribe:false, enableVersioning: true}); // Unsub once there is a feedback
// Generate fake payload
myPayload = '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"CoolToken1"}';
myStateObject = JSON.parse(myPayload);
// Update
thingShadows.update('testShadow4', myStateObject);
clock.tick(3000);
mockMQTTClientObject.emit('message', '$aws/things/testShadow4/shadow/update/accepted', '{"clientToken":"CoolToken1","version":7}'); // sync local version
// Update again
thingShadows.update('testShadow4', myStateObject);
clock.tick(3000);
assert.equal(mockMQTTClientObject.lastPublishedMessage, '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"CoolToken1","version":7}');
// Unregister it
thingShadows.unregister('testShadow4');
clock.restore();
});
});
//
// Verify that timeout triggers the callback for shadow Update timeout.
//
describe("Update request timeout", function() {
it("should call timeout callback", function() {
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Faking timer
var clock = sinon.useFakeTimers();
// Faking callback
var fakeCallback = sinon.spy();
// Init thingShadowClient
var thingShadows = thingShadow( {
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',
}, {
operationTimeout:1000, // Set operation timeout to be 1 sec
} );
// Register callback
thingShadows.on('timeout', fakeCallback);
// Register a thing
thingShadows.register('testShadow4', { persistentSubscribe: false } );
// Generate fake payload
myPayload = '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"CoolToken1"}';
myStateObject = JSON.parse(myPayload);
// Update
thingShadows.update('testShadow4', myStateObject);
//
clock.tick(3000); // 3 sec later...
//
assert(fakeCallback.calledOnce);
// Unregister it
thingShadows.unregister('testShadow4');
clock.restore();
});
});
//
// Verify that incoming messages with wrong/none token for shadow Get/Delete are ignored.
//
describe("Incoming messages are missing/messed up with token for shadow Update", function() {
it("should never call callback", function() {
// Faking callback
fakeCallback = sinon.spy();
// Reinit mockMQTTClientObject
mockMQTTClientObject.reInitCommandCalled();
mockMQTTClientObject.resetPublishedMessage();
// Init thingShadowClient
var thingShadows = thingShadow( {
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'
} );
// Register a callback
thingShadows.on('status', fakeCallback);
// Register a thing
thingShadows.register('testShadow4');
// Generate fake payload
myPayload = '{"state":{"desired":{"color":"RED"},"reported":{"color":"BLUE"}},"clientToken":"CoolToken1"}';
myStateObject = JSON.parse(myPayload);
// Update
thingShadows.update('testShadow4', myStateObject);
mockMQTTClientObject.emit('message', '$aws/things/testShadow4/shadow/update/accepted', '{"clientToken":"Garbage1", "version":2}'); // wrong token
thingShadows.update('testShadow4', myStateObject);
mockMQTTClientObject.emit('message', '$aws/things/testShadow4/shadow/update/accepted', '{"version":2}'); // no token
// Check
sinon.assert.notCalled(fakeCallback); // Should never trigger the callback
// Unregister it
thingShadows.unregister('testShadow4');
});
});
//
// Verify t