diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..771f73b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: + - '*' + - '!main' + +env: + RUN: ${{ github.run_id }}-${{ github.run_number }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + PACKAGE_NAME: aws-iot-device-sdk-js + AWS_EC2_METADATA_DISABLED: true + +jobs: + unit-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [10.x] + + steps: + - uses: actions/checkout@v2 + - name: Unit Tests - Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: npm install + - run: npm test + + integration-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [10.x] + test-type: [websocket, certificate] + + steps: + - uses: actions/checkout@v2 + - name: ${{ matrix.test-type }} Integration Tests - Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + - run: cd integration-testing && ./run-tests.sh ${{ matrix.test-type }} diff --git a/integration-testing/integration-tests/device-integration-test.js b/integration-testing/integration-tests/device-integration-test.js new file mode 100644 index 0000000..23c6207 --- /dev/null +++ b/integration-testing/integration-tests/device-integration-test.js @@ -0,0 +1,174 @@ +/* + * 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 +const deviceModule = require('..').device; +const cmdLineProcess = require('../examples/lib/cmdline'); +const isUndefined = require('../common/lib/is-undefined.js'); + +//begin module + +function processTest( args, argsRemaining ) { +// +// Use the command line flag '--thing-name | -T' to pass in a topic +// prefix; this allows us to run multiple copies of this test simultaneously +// within the same AWS account +// +var topicPrefix = args.thingName; +var integrationTestTopic = 'deviceIntegrationTestTopic'; + +if (!isUndefined(topicPrefix)) { + integrationTestTopic=topicPrefix+'/'+integrationTestTopic; +} + +var customAuthHeaders; +var region = args.region; + +if(args.Protocol === 'wss-custom-auth') { + customAuthHeaders = JSON.parse(process.env.CUSTOM_AUTH_HEADERS); + region = 'us-west-2'; +} + +// +// The device module exports an MQTT instance, which will attempt +// to connect to the AWS IoT endpoint configured in the arguments. +// Once connected, it will emit events which our application can +// handle. +// +const device = deviceModule({ + keyPath: args.privateKey, + certPath: args.clientCert, + caPath: args.caCert, + clientId: args.clientId, + region: region, + reconnectPeriod: args.reconnectPeriod, + protocol: args.Protocol, + port: args.Port, + host: args.Host, + debug: args.Debug, + customAuthHeaders: customAuthHeaders +}); + +var timeout; +var value=1; +var count=1; +var accumulator=0; + +function checkAccumulator() +{ +// +// Check the accumulator to see how many messages were received from the +// partner process via the MQTT passthrough. +// + var i, messages = 0, accSave=accumulator; + for (i = 0; i < 48; i++) + { + if (accumulator & 1) + { + messages++; + } + accumulator = accumulator>>1; + } + console.log(messages+' messages received, accumulator='+accSave.toString(16)); +} + +// +// Do a simple publish/subscribe demo based on the test-mode passed +// in the command line arguments. +// +device + .on('connect', function() { + const minimumDelay=250; + console.log('connect'); + if (args.testMode === 2) + { + device.subscribe(integrationTestTopic); + } + else + { + if ((Math.max(args.delay,minimumDelay) ) !== args.delay) + { + console.log( 'substituting '+ minimumDelay + 'ms delay for ' + args.delay + 'ms...' ); + } + timeout = setInterval( function() { + console.log('publishing value='+value+' on \''+integrationTestTopic+'\''); + device.publish(integrationTestTopic, JSON.stringify({ + value: value }), { qos: 1 }); + +// +// After 48 publishes, exit the process. This number is chosen as it's the last power of 2 less +// than 53 (the mantissa size for Node's native floating point numbers). +// + value=value*2; + count++; + if (count >= 48) + { + device.publish(integrationTestTopic, JSON.stringify({ + quit: 1 })); + setTimeout( function() { + process.exit(0); }, 500); + } + }, Math.max(args.delay,minimumDelay) ); // clip to minimum + } + }); +device + .on('close', function() { + console.log('close'); + process.exit(1); + }); +device + .on('reconnect', function() { + console.log('reconnect'); + process.exit(1); + }); +device + .on('offline', function() { + console.log('offline'); + process.exit(1); + }); +device + .on('error', function(error) { + console.log('error', error); + process.exit(1); + }); +device + .on('message', function(topic, payload) { + + var stateObject = JSON.parse( payload.toString() ); + console.log('received on \''+topic+'\': '+payload.toString()); + if (!isUndefined( stateObject.value )) + { + accumulator+=stateObject.value; + } + if (!isUndefined( stateObject.quit)) + { + checkAccumulator(); + setTimeout( function() { process.exit(0); }, 500 ); + } + }); + +} + +module.exports = cmdLineProcess; + +if (require.main === module) { + cmdLineProcess('connect to the AWS IoT service and publish/subscribe to topics using MQTT, test modes 1-2', + process.argv.slice(2), processTest ); +} + diff --git a/integration-testing/integration-tests/jobs-integration-test.js b/integration-testing/integration-tests/jobs-integration-test.js new file mode 100644 index 0000000..082ae2a --- /dev/null +++ b/integration-testing/integration-tests/jobs-integration-test.js @@ -0,0 +1,247 @@ +/* + * 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 +const jobsModule = require('..').jobs; +const cmdLineProcess = require('../examples/lib/cmdline'); +const isUndefined = require('../common/lib/is-undefined.js'); +const awsSDK = require('aws-sdk'); +awsSDK.config.update({ "accessKeyId": process.env.JOBS_AWS_ACCESS_KEY_ID, "secretAccessKey": process.env.JOBS_AWS_SECRET_ACCESS_KEY, "region": "us-east-1" }); + +var iot = new awsSDK.Iot(); + +//begin module + +function processTest( args, argsRemaining ) { +// +// Use the command line flag '--thing-name | -T' to pass in a topic +// prefix; this allows us to run multiple copies of this test simultaneously +// within the same AWS account +// + +var topicPrefix = args.thingName; +var thingName = args.thingName; +var integrationTestTopic = 'jobsIntegrationTestTopic'; +var preRegisteredThingName = 'testThing1'; + +if (!isUndefined(topicPrefix)) { + integrationTestTopic=topicPrefix+'/'+integrationTestTopic; +} + +// +// The jobs module exports an MQTT instance, which will attempt +// to connect to the AWS IoT endpoint configured in the arguments. +// Once connected, it will emit events which our application can +// handle. +// +const jobs = jobsModule({ + keyPath: args.privateKey, + certPath: args.clientCert, + caPath: args.caCert, + clientId: args.clientId, + region: args.region, + reconnectPeriod: args.reconnectPeriod, + protocol: args.Protocol, + port: args.Port, + host: args.Host, + debug: args.Debug +}); + +var timeout; +var value=1; +var count=1; +var accumulator=0; +var jobCount=5; +var jobCompletedCount=0; + +function checkAccumulator() +{ + console.log('checkAccumulator'); +// +// Check the accumulator to see how many messages were received from the +// partner process via the MQTT passthrough. +// + var i, messages = 0, accSave=accumulator; + for (i = 0; i < 48; i++) + { + if (accumulator & 1) + { + messages++; + } + accumulator = accumulator>>1; + } + console.log(messages+' messages received, accumulator='+accSave.toString(16)); + console.log(jobCompletedCount+' jobs completed'); +} + +function handleJob(err, job) +{ + if (isUndefined(err)) { + console.log('received job: ' + JSON.stringify(job.document)); + job.inProgress(function(err) { + if (isUndefined(err)) { + var jobFunction = job.succeeded; + if (job.document.jobNum & 1) { + jobFunction = job.failed; + } + + jobFunction(function(err) { + if (!isUndefined(err)) { + console.log(err); + } + }); + } else { + console.log(err); + } + }); + } else { + console.log(err); + } +} + +// +// Do a simple publish/subscribe demo based on the test-mode passed +// in the command line arguments. +// +jobs + .on('connect', function() { + const minimumDelay=250; + console.log('connect'); + if (args.testMode === 2) + { + jobs.subscribe(integrationTestTopic); + for (var i = 0; i < jobCount - 1; i++) { + jobs.subscribeToJobs(preRegisteredThingName, 'test' + i.toString(), handleJob); + } + + jobs.subscribeToJobs(preRegisteredThingName, handleJob); + + jobs.startJobNotifications(preRegisteredThingName); + } + else + { + var jobIdPrefix = 'test-job-id-' + (Math.floor(Math.random() * 99999999)).toString(); + + for (var i = 0; i < jobCount; i++) { + iot.createJob({ jobId: jobIdPrefix + '-' + i.toString(), targets: [ 'arn:aws:iot:us-east-1:809478692717:thing/' + preRegisteredThingName ], document: '{ "operation":"test' + i.toString() + '", "jobNum": ' + i.toString() + ' }' }, function(err, data) { + console.log('createJob:'); + if (isUndefined(err)) { + console.log(data); + } else { + console.log(err); + } + }); + } + +// +// Test device messaging through jobs module +// + if ((Math.max(args.delay,minimumDelay) ) !== args.delay) + { + console.log( 'substituting '+ minimumDelay + 'ms delay for ' + args.delay + 'ms...' ); + } + + setTimeout( function() { + function checkJobExecutions(jobNum) { + if (jobNum < jobCount) { + console.log('checking execution status on ' + preRegisteredThingName + ' for job: ' + jobIdPrefix + '-' + jobNum.toString()); + iot.describeJobExecution({ thingName: preRegisteredThingName, jobId: jobIdPrefix + '-' + jobNum.toString() }, function(err, data) { + if (!isUndefined(data) && !isUndefined(data.execution) && + ((jobNum & 1) ? data.execution.status === 'FAILED' : (data.execution.status === 'SUCCESS' || data.execution.status === 'SUCCEEDED'))) { + jobCompletedCount++; + } + + console.log('cancelling job ' + jobIdPrefix + '-' + jobNum.toString() + ' to prevent leaving orphan jobs'); + iot.cancelJob({ jobId: jobIdPrefix + '-' + jobNum.toString() }); + + checkJobExecutions(jobNum + 1); + }); + } + } + + console.log( 'tally completed job executions' ); + checkJobExecutions(0); + }, 15000); + + timeout = setInterval( function() { + console.log('publishing value='+value+' on \''+integrationTestTopic+'\''); + jobs.publish(integrationTestTopic, JSON.stringify({ + value: value }), { qos: 1 }); + +// +// After 48 publishes, exit the process. This number is chosen as it's the last power of 2 less +// than 53 (the mantissa size for Node's native floating point numbers). +// + value=value*2; + count++; + if (count >= 48) + { + jobs.publish(integrationTestTopic, JSON.stringify({ quit: 1, jobCompletedCount: jobCompletedCount }), function() { + setTimeout( function() { process.exit(0); }, 500); + }); + } + }, Math.max(args.delay,minimumDelay) ); // clip to minimum + } + }); +jobs + .on('close', function() { + console.log('close'); + process.exit(1); + }); +jobs + .on('reconnect', function() { + console.log('reconnect'); + process.exit(1); + }); +jobs + .on('offline', function() { + console.log('offline'); + process.exit(1); + }); +jobs + .on('error', function(error) { + console.log('error', error); + process.exit(1); + }); +jobs + .on('message', function(topic, payload) { + + var stateObject = JSON.parse( payload.toString() ); + console.log('received on \''+topic+'\': '+payload.toString()); + if (!isUndefined( stateObject.value )) + { + accumulator+=stateObject.value; + } + if (!isUndefined(stateObject.quit)) + { + jobCompletedCount = (!isUndefined(stateObject.jobCompletedCount) ? stateObject.jobCompletedCount : 0); + checkAccumulator(); + setTimeout( function() { process.exit(0); }, 500 ); + } + }); + +} + +module.exports = cmdLineProcess; + +if (require.main === module) { + cmdLineProcess('connect to the AWS IoT service and publish/subscribe to topics using MQTT, test modes 1-2', + process.argv.slice(2), processTest ); +} + diff --git a/integration-testing/integration-tests/offline-publishing-test.js b/integration-testing/integration-tests/offline-publishing-test.js new file mode 100644 index 0000000..d46f2c9 --- /dev/null +++ b/integration-testing/integration-tests/offline-publishing-test.js @@ -0,0 +1,194 @@ +/* + * 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 +const deviceModule = require('..').device; +const cmdLineProcess = require('../examples/lib/cmdline'); +const isUndefined = require('../common/lib/is-undefined.js'); + +const RECEIVER_TIMEOUT_DELAY = 60000; +const TOTAL_MESSAGES_TO_SEND = 1000; +const RECONNECTS_BEFORE_FAILURE = 3; + +//begin module + +function processTest(args) { + // + // Use the command line flag '--thing-name | -T' to pass in a topic + // prefix; this allows us to run multiple copies of this test simultaneously + // within the same AWS account + // + var topicPrefix = args.thingName; + var offlinePublishTestTopic = 'offlinePublishTestTopic'; + + if (!isUndefined(topicPrefix)) { + offlinePublishTestTopic=topicPrefix+'/'+offlinePublishTestTopic; + } + + var customAuthHeaders; + var region = args.region; + + if(args.Protocol === 'wss-custom-auth') { + customAuthHeaders = JSON.parse(process.env.CUSTOM_AUTH_HEADERS); + region = 'us-west-2'; + } + + // + // The device module exports an MQTT instance, which will attempt + // to connect to the AWS IoT endpoint configured in the arguments. + // Once connected, it will emit events which our application can + // handle. + // + const device = deviceModule({ + keyPath: args.privateKey, + certPath: args.clientCert, + caPath: args.caCert, + clientId: args.clientId, + region: region, + baseReconnectTimeMs: args.baseReconnectTimeMs, + drainTimeMs: 35, + offlineQueueing: true, + keepalive: args.keepAlive, + protocol: args.Protocol, + port: args.Port, + host: args.Host, + debug: args.Debug, + customAuthHeaders: customAuthHeaders + }); + var receiveCount = 0; + var outOfOrderCount = 0; + var connectCount = 0; + var quitTimeout; + var expectedSum = 0; + var actualSum = 0; + + var reconnectsSinceLastSuccessfulConnect = 0; + + function receiverExitWithError() { + console.log('No messages received in the past ' + RECEIVER_TIMEOUT_DELAY + ' ms, exiting test'); + process.exit(1); + } + + if (args.testMode === 1) { + // + // This process is the receiver + // + var noMessageReceivedTimeout; + noMessageReceivedTimeout = setTimeout(receiverExitWithError, RECEIVER_TIMEOUT_DELAY); + device.subscribe(offlinePublishTestTopic); + } else { + var publishTimeout; + var disconnectTimeout; + var transmitCount = 0; + const minimumDelay = 100; + + if ((Math.max(args.delay, minimumDelay)) !== args.delay) { + console.log('substituting ' + minimumDelay + 'ms delay for ' + args.delay + 'ms...'); + } + publishTimeout = setInterval(function() { + transmitCount++; + device.publish(offlinePublishTestTopic, JSON.stringify({ + value: transmitCount + })); + if(transmitCount > TOTAL_MESSAGES_TO_SEND) { + clearInterval(publishTimeout); + setTimeout( function() { + process.exit(0); }, 500); + } + }, Math.max(args.delay, minimumDelay)); // clip to minimum + } + + device + .on('connect', function() { + connectCount++; + reconnectsSinceLastSuccessfulConnect = 0; + if (args.testMode === 2) { + if (connectCount < 10) { + disconnectTimeout = setTimeout(function() { + console.log("Connect count: " + connectCount); + device.simulateNetworkFailure(); + }, 20000); // disconnect us every 20 seconds + } else { + quitTimeout = setTimeout(function() { + device.publish(offlinePublishTestTopic, JSON.stringify({ + value: 'quit' + }), { + qos: 0 + }); + setTimeout(function() { + process.exit(0); + }, 1500); + }, 10000); /* run the test for just under 10 additional seconds */ + } + } + console.log('connect'); + }); + device + .on('close', function() { + console.log('close'); + }); + device + .on('reconnect', function() { + console.log('reconnect'); + reconnectsSinceLastSuccessfulConnect++; + if(reconnectsSinceLastSuccessfulConnect > RECONNECTS_BEFORE_FAILURE) { + console.log('attempted to reconnect too many times'); + process.exit(2); + } + }); + device + .on('offline', function() { + console.log('offline'); + }); + device + .on('error', function(error) { + console.log('error', error); + }); + device + .on('message', function(topic, payload) { + if (args.testMode === 1) { + var obj = JSON.parse(payload.toString()); + if (obj.value === 'quit') { + var errorRate = (expectedSum / actualSum); + + console.log('quality (closer to 1.0 = fewer drops): ' + errorRate.toFixed(6)); + setTimeout(function() { + process.exit(0); + }, 500); + } else { + receiveCount++; + expectedSum += receiveCount; + actualSum += obj.value; + if (obj.value !== receiveCount) { + outOfOrderCount++; + } + clearTimeout(noMessageReceivedTimeout); + noMessageReceivedTimeout = setTimeout(receiverExitWithError, RECEIVER_TIMEOUT_DELAY); + } + } + console.log('message', topic, payload.toString()); + }); +} + +module.exports = cmdLineProcess; + +if (require.main === module) { + cmdLineProcess('connect to the AWS IoT service and publish/subscribe to topics using MQTT, test modes 1-2', + process.argv.slice(2), processTest); +} diff --git a/integration-testing/integration-tests/run-device-integration-test.sh b/integration-testing/integration-tests/run-device-integration-test.sh new file mode 100755 index 0000000..ec4dafc --- /dev/null +++ b/integration-testing/integration-tests/run-device-integration-test.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# +# Run the device-mode integration test. +# +# +# Check to make sure the top-level test directory is defined. +# +if [ $NPMTEST_DIR"" = "" ] +then + echo ${0##*/}": NPMTEST_DIR must be defined!" + exit 1 +fi +# +# Set the name of the node executable (it should be in our path) +# +NODE=node +# +# Set a randomized test tag to isolate this test run from others within +# the AWS account associated with our Odin material set. This ensures +# that all topic names used are unique to this test run. +# +TEST_TAG="test-"$RANDOM +# +# Capture the exit code of the first command which fails in a pipeline. +# +set -o pipefail +# +# The integration tests pass a number of updates between two partner +# processes. Because these are QoS 0, delivery isn't guaranteed, but +# we should expect to receive most of them. All tests use the same number +# of updates (48). +# +RECEIVES_REQUIRED=46 +TRANSMITS_TOTAL=48 +export HOSTNAME="ajje7lpljulm4-ats.iot.us-east-1.amazonaws.com" +export CUSTOM_AUTH_HOST="ajje7lpljulm4.gamma.us-west-2.iot.amazonaws.com" +# +# Process output will be captured in these files. +# +PROC1_OUTFILE=$NPMTEST_DIR/${0##*/}-$RANDOM-$RANDOM-proc-1.out +PROC2_OUTFILE=$NPMTEST_DIR/${0##*/}-$RANDOM-$RANDOM-proc-2.out +# +# Move the test javascript program to the installed SDK +# directory +# +INT_TEST_DIR=$NPMTEST_DIR/aws-iot-device-sdk-js/integration-test +mkdir -p $INT_TEST_DIR +cp ${0%/*}/device-integration-test.js $INT_TEST_DIR +# +# Start the two partner processes for the device-mode integration test and +# save their PIDs. +# +if [ $AUTHENTICATION_TYPE"" == "certificate" ] +then + echo "###################################################################" + echo ${0##*/}": running device integration test (certificate auth)" + echo "###################################################################" + $NODE $INT_TEST_DIR/device-integration-test.js -H $HOSTNAME -f $CERT_DIR -t2 --debug=true -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! + $NODE $INT_TEST_DIR/device-integration-test.js -H $HOSTNAME -f $CERT_DIR -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! +elif [ $AUTHENTICATION_TYPE"" == "custom-auth" ] +then + echo "###################################################################" + echo ${0##*/}": running device integration test (websocket/custom auth)" + echo "###################################################################" + $NODE $INT_TEST_DIR/device-integration-test.js -H $CUSTOM_AUTH_HOST -P=wss-custom-auth -t2 --debug=true -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! + $NODE $INT_TEST_DIR/device-integration-test.js -H $CUSTOM_AUTH_HOST -P=wss-custom-auth -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! +else + echo "###################################################################" + echo ${0##*/}": running device integration test (websocket/sigv4)" + echo "###################################################################" + $NODE $INT_TEST_DIR/device-integration-test.js -H $HOSTNAME -P=wss -t2 --debug=true -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! + $NODE $INT_TEST_DIR/device-integration-test.js -H $HOSTNAME -P=wss -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! +fi +# +# Wait on the two partner processes and record their exit codes. +# +wait $PROC1_PID +PROC1_EXIT_CODE=$? +wait $PROC2_PID +PROC2_EXIT_CODE=$? +# +# Combine the two exit codes; if either process exited abnormally, this +# test is a failure. +# +COMBINED_EXIT_CODE=$((PROC1_EXIT_CODE | PROC_2_EXIT_CODE)) +if [ $COMBINED_EXIT_CODE"" = "0" ] +then + numReceived=`cat $PROC2_OUTFILE | grep -E '^[0-9]+\ messages received, accumulator=.*' |awk '{print $1}'` + receiveMask=`cat $PROC2_OUTFILE | grep -E '^[0-9]+\ messages received, accumulator=.*' |awk '{print $4}'|sed -e 's/.*=//'` + + echo $numReceived" messages received, receive mask ["$receiveMask"]" + + if [ $numReceived"" -gt $RECEIVES_REQUIRED"" ] + then + echo "########################################################" + echo " TEST SUCCEEDED: RECEIVED "$numReceived"/"$TRANSMITS_TOTAL", "$RECEIVES_REQUIRED" required" + echo "########################################################" + else + echo "########################################################" + echo " TEST FAILED: RECEIVED "$numReceived"/"$TRANSMITS_TOTAL", "$RECEIVES_REQUIRED" required" + echo "########################################################" + exit 2 + fi +else + echo ${0##*/}": FAILED ("$PROC1_EXIT_CODE":"$PROC2_EXIT_CODE")" + exit $COMBINED_EXIT_CODE +fi +rm $PROC1_OUTFILE $PROC2_OUTFILE diff --git a/integration-testing/integration-tests/run-jobs-integration-test.sh b/integration-testing/integration-tests/run-jobs-integration-test.sh new file mode 100755 index 0000000..9490402 --- /dev/null +++ b/integration-testing/integration-tests/run-jobs-integration-test.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# +# Run the jobs-mode integration test. +# +# +# Check to make sure the top-level test directory is defined. +# +if [ $NPMTEST_DIR"" = "" ] +then + echo ${0##*/}": NPMTEST_DIR must be defined!" + exit 1 +fi +# +# Set the name of the node executable (it should be in our path) +# +NODE=node +# +# Set a randomized test tag to isolate this test run from others within +# the AWS account associated with our Odin material set. This ensures +# that all topic names used are unique to this test run. +# +TEST_TAG="test-"$RANDOM +# +# Capture the exit code of the first command which fails in a pipeline. +# +set -o pipefail +# +# The integration tests pass a number of updates between two partner +# processes. Because these are QoS 0, delivery isn't guaranteed, but +# we should expect to receive most of them. All tests use the same number +# of updates (48). +# +RECEIVES_REQUIRED=46 +TRANSMITS_TOTAL=48 +COMPLETED_JOBS_REQUIRED=4 +CREATED_JOBS_TOTAL=5 +export HOSTNAME="ajje7lpljulm4-ats.iot.us-east-1.amazonaws.com" +# +# Process output will be captured in these files. +# +PROC1_OUTFILE=$NPMTEST_DIR/${0##*/}-$RANDOM-$RANDOM-proc-1.out +PROC2_OUTFILE=$NPMTEST_DIR/${0##*/}-$RANDOM-$RANDOM-proc-2.out +# +# Move the test javascript program to the installed SDK +# directory +# +INT_TEST_DIR=$NPMTEST_DIR/aws-iot-device-sdk-js/integration-test +mkdir -p $INT_TEST_DIR +cp ${0%/*}/jobs-integration-test.js $INT_TEST_DIR +# +# Start the two partner processes for the jobs-mode integration test and +# save their PIDs. +# +if [ $CERT_DIR"" != "" ] +then + echo "###################################################################" + echo ${0##*/}": running jobs integration test (certificate auth)" + echo "###################################################################" + $NODE $INT_TEST_DIR/jobs-integration-test.js -H $HOSTNAME -f $CERT_DIR -t2 --debug=true -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! + $NODE $INT_TEST_DIR/jobs-integration-test.js -H $HOSTNAME -f $CERT_DIR -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! +else + echo "###################################################################" + echo ${0##*/}": running jobs integration test (websocket/sigv4)" + echo "###################################################################" + $NODE $INT_TEST_DIR/jobs-integration-test.js -H $HOSTNAME -P=wss -t2 --debug=true -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! + $NODE $INT_TEST_DIR/jobs-integration-test.js -H $HOSTNAME -P=wss -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! +fi +# +# Wait on the two partner processes and record their exit codes. +# +wait $PROC1_PID +PROC1_EXIT_CODE=$? +wait $PROC2_PID +PROC2_EXIT_CODE=$? +# +# Combine the two exit codes; if either process exited abnormally, this +# test is a failure. +# +COMBINED_EXIT_CODE=$((PROC1_EXIT_CODE | PROC_2_EXIT_CODE)) +if [ $COMBINED_EXIT_CODE"" = "0" ] +then + jobsCompleted=`cat $PROC2_OUTFILE | grep -E '^[0-9]+\ jobs completed' |awk '{print $1}'` + numReceived=`cat $PROC2_OUTFILE | grep -E '^[0-9]+\ messages received, accumulator=.*' |awk '{print $1}'` + receiveMask=`cat $PROC2_OUTFILE | grep -E '^[0-9]+\ messages received, accumulator=.*' |awk '{print $4}'|sed -e 's/.*=//'` + + echo $numReceived" messages received, receive mask ["$receiveMask"]" + echo $jobsCompleted" jobs completed" + + if !([ $numReceived"" -lt $RECEIVES_REQUIRED"" ] || [ $jobsCompleted -lt $COMPLETED_JOBS_REQUIRED ]) + then + echo "########################################################" + echo " TEST SUCCEEDED: RECEIVED "$numReceived"/"$TRANSMITS_TOTAL", "$RECEIVES_REQUIRED" required" + echo " JOBS COMPLETED "$jobsCompleted"/"$CREATED_JOBS_TOTAL", "$COMPLETED_JOBS_REQUIRED" required" + echo "########################################################" + else + echo "########################################################" + echo " TEST FAILED: RECEIVED "$numReceived"/"$TRANSMITS_TOTAL", "$RECEIVES_REQUIRED" required" + echo " JOBS COMPLETED "$jobsCompleted"/"$CREATED_JOBS_TOTAL", "$COMPLETED_JOBS_REQUIRED" required" + echo "########################################################" + exit 2 + fi +else + echo ${0##*/}": FAILED ("$PROC1_EXIT_CODE":"$PROC2_EXIT_CODE")" + exit $COMBINED_EXIT_CODE +fi +rm $PROC1_OUTFILE $PROC2_OUTFILE diff --git a/integration-testing/integration-tests/run-offline-publishing-test.sh b/integration-testing/integration-tests/run-offline-publishing-test.sh new file mode 100755 index 0000000..978676a --- /dev/null +++ b/integration-testing/integration-tests/run-offline-publishing-test.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# +# Run the offline publishing integration test. +# +# +# Check to make sure the top-level test directory is defined. +# +if [ $NPMTEST_DIR"" = "" ] +then + echo ${0##*/}": NPMTEST_DIR must be defined!" + exit 1 +fi +# +# Set the name of the node executable (it should be in our path) +# +NODE=node +# +# Set a randomized test tag to isolate this test run from others within +# the AWS account associated with our Odin material set. This ensures +# that all topic names used are unique to this test run. +# +TEST_TAG="test-"$RANDOM +export HOSTNAME="ajje7lpljulm4-ats.iot.us-east-1.amazonaws.com" +export CUSTOM_AUTH_HOST="ajje7lpljulm4.gamma.us-west-2.iot.amazonaws.com" +# +# Capture the exit code of the first command which fails in a pipeline. +# +set -o pipefail +# +# The integration tests pass a number of updates between two partner +# processes. Because these are QoS 0, delivery isn't guaranteed, but +# we should expect to receive most of them. In this test, the transmitting +# process is subjected to simulated network failures; since offline +# publish queueing is enabled, published messages are stored until it +# reconnects and are then drained out. The receiving process should +# receive all of these if everything is working correctly. +# +# The transmitting process sends messages with a monotonically increasing +# value, starting at 1. The receiving process maintains a running sum of +# these values, as well as a running sum of the message numbers received. +# At the end of the test, the ratio of these sums is calculated; if no +# messages are missing at the receiver, this ratio will be exactly 1.0. +# Any messages which weren't received will cause this ratio to be less than +# 1.0. Since QoS 0 is used for the test, this is possible so a very small +# error difference is allowed. +# +RECEIVES_REQUIRED=46 +TRANSMITS_TOTAL=48 +# +# Process output will be captured in these files. +# +PROC1_OUTFILE=$NPMTEST_DIR/${0##*/}-$RANDOM-$RANDOM-proc-1.out +PROC2_OUTFILE=$NPMTEST_DIR/${0##*/}-$RANDOM-$RANDOM-proc-2.out +# +# Move the test javascript programs to the installed SDK +# directory +# +INT_TEST_DIR=$NPMTEST_DIR/aws-iot-device-sdk-js/integration-test +mkdir -p $INT_TEST_DIR +cp ${0%/*}/offline-publishing-test.js $INT_TEST_DIR +# +# Start the two partner processes for the offline publishing integration test and +# save their PIDs. +# +if [ $AUTHENTICATION_TYPE"" == "certificate" ] +then + echo "###################################################################" + echo ${0##*/}": running thing integration test (certificate auth)" + echo "###################################################################" + $NODE $INT_TEST_DIR/offline-publishing-test.js -H $HOSTNAME -f $CERT_DIR -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! + sleep 3 # wait 3 seconds prior to starting transmitting process + # + # transmit 4x/second + # + $NODE $INT_TEST_DIR/offline-publishing-test.js -H $HOSTNAME -f $CERT_DIR -t2 --debug=true --delay-ms=250 -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! +elif [ $AUTHENTICATION_TYPE"" == "custom-auth" ] +then + echo "###################################################################" + echo ${0##*/}": running device integration test (websocket/custom auth)" + echo "###################################################################" + $NODE $INT_TEST_DIR/offline-publishing-test.js -H $CUSTOM_AUTH_HOST -P=wss-custom-auth -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! + sleep 3 # wait 3 seconds prior to starting transmitting process + # + # transmit 4x/second + # + $NODE $INT_TEST_DIR/offline-publishing-test.js -H $CUSTOM_AUTH_HOST -P=wss-custom-auth -t2 --debug=true --delay-ms=250 -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! +else + echo "###################################################################" + echo ${0##*/}": running thing integration test (websocket/sigv4)" + echo "###################################################################" + $NODE $INT_TEST_DIR/offline-publishing-test.js -H $HOSTNAME -P=wss -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! + sleep 3 # wait 3 seconds prior to starting transmitting process + # + # transmit 4x/second + # + $NODE $INT_TEST_DIR/offline-publishing-test.js -H $HOSTNAME -P=wss -t2 --debug=true --delay-ms=250 -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! +fi +# +# Wait on the two partner processes and record their exit codes. +# +wait $PROC1_PID +PROC1_EXIT_CODE=$? +wait $PROC2_PID +PROC2_EXIT_CODE=$? +# +# Combine the two exit codes; if either process exited abnormally, this +# test is a failure. +# +COMBINED_EXIT_CODE=$((PROC1_EXIT_CODE | PROC_2_EXIT_CODE)) +if [ $COMBINED_EXIT_CODE"" = "0" ] +then + # + # Print out the received quality for debugging. + # + cat $PROC1_OUTFILE | grep -E '^quality' |awk '{print $NF}' + receiveQuality=`cat $PROC1_OUTFILE | grep -E '^quality' |awk '{print $NF}'` + + # + # We should receive all of these; allow only a very small error margin. + # + minAcceptableQuality=0.99 + qualityGtMinAcceptable=`echo $receiveQuality"" \> $minAcceptableQuality"" | bc -l` + # + # The quality ratio should never exceed 1.0. + # + maxAcceptableQuality=1.00001 + qualityLtMaxAcceptable=`echo $maxAcceptableQuality"" \> $receiveQuality"" | bc -l` + + if [ $qualityGtMinAcceptable"" -eq 1 ] && [ $qualityLtMaxAcceptable"" -eq 1 ] + then + echo "########################################################" + echo " TEST SUCCEEDED: PROC 1 QUALITY "$receiveQuality", "$minAcceptableQuality" required" + echo "########################################################" + else + echo "########################################################" + echo " TEST FAILED: PROC 1 QUALITY "$receiveQuality", "$minAcceptableQuality" required" + echo "########################################################" + exit 2 + fi +else + echo ${0##*/}": FAILED ("$PROC1_EXIT_CODE":"$PROC2_EXIT_CODE")" + exit $COMBINED_EXIT_CODE +fi +rm $PROC1_OUTFILE $PROC2_OUTFILE diff --git a/integration-testing/integration-tests/run-thing-integration-test.sh b/integration-testing/integration-tests/run-thing-integration-test.sh new file mode 100755 index 0000000..983ac2c --- /dev/null +++ b/integration-testing/integration-tests/run-thing-integration-test.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# +# Run the thing-mode integration test. +# +# +# Check to make sure the top-level test directory is defined. +# +if [ $NPMTEST_DIR"" = "" ] +then + echo ${0##*/}": NPMTEST_DIR must be defined!" + exit 1 +fi +# +# Set the name of the node executable (it should be in our path) +# +NODE=node +# +# Set a randomized test tag to isolate this test run from others within +# the AWS account associated with our Odin material set. This ensures +# that all topic names used are unique to this test run. +# +TEST_TAG="test-"$RANDOM +export HOSTNAME="ajje7lpljulm4-ats.iot.us-east-1.amazonaws.com" +export CUSTOM_AUTH_HOST="ajje7lpljulm4.gamma.us-west-2.iot.amazonaws.com" +# +# Capture the exit code of the first command which fails in a pipeline. +# +set -o pipefail +# +# The integration tests pass a number of updates between two partner +# processes. Because these are QoS 0, delivery isn't guaranteed, but +# we should expect to receive most of them. All tests use the same number +# of updates (48). +# +RECEIVES_REQUIRED=46 +TRANSMITS_TOTAL=48 +# +# Process output will be captured in these files. +# +PROC1_OUTFILE=$NPMTEST_DIR/${0##*/}-$RANDOM-$RANDOM-proc-1.out +PROC2_OUTFILE=$NPMTEST_DIR/${0##*/}-$RANDOM-$RANDOM-proc-2.out +# +# Move the test javascript programs to the installed SDK +# directory +# +INT_TEST_DIR=$NPMTEST_DIR/aws-iot-device-sdk-js/integration-test +mkdir -p $INT_TEST_DIR +cp ${0%/*}/thing-integration-test.js $INT_TEST_DIR +# +# Start the two partner processes for the thing-mode integration test and +# save their PIDs. +# +if [ $AUTHENTICATION_TYPE"" == "certificate" ] +then + echo "###################################################################" + echo ${0##*/}": running thing integration test (certificate auth)" + echo "###################################################################" + $NODE $INT_TEST_DIR/thing-integration-test.js -H $HOSTNAME -f $CERT_DIR -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! + $NODE $INT_TEST_DIR/thing-integration-test.js -H $HOSTNAME -f $CERT_DIR -t2 --debug=true -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! +elif [ $AUTHENTICATION_TYPE"" == "custom-auth" ] +then + echo "###################################################################" + echo ${0##*/}": running device integration test (websocket/custom auth)" + echo "###################################################################" + $NODE $INT_TEST_DIR/thing-integration-test.js -H $CUSTOM_AUTH_HOST -P=wss-custom-auth -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! + $NODE $INT_TEST_DIR/thing-integration-test.js -H $CUSTOM_AUTH_HOST -P=wss-custom-auth -t2 --debug=true -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! +else + echo "###################################################################" + echo ${0##*/}": running thing integration test (websocket/sigv4)" + echo "###################################################################" + $NODE $INT_TEST_DIR/thing-integration-test.js -H $HOSTNAME -P=wss -t1 --debug=true -T $TEST_TAG | tee $PROC1_OUTFILE & + PROC1_PID=$! + $NODE $INT_TEST_DIR/thing-integration-test.js -H $HOSTNAME -P=wss -t2 --debug=true -T $TEST_TAG | tee $PROC2_OUTFILE & + PROC2_PID=$! +fi +# +# Wait on the two partner processes and record their exit codes. +# +wait $PROC1_PID +PROC1_EXIT_CODE=$? +wait $PROC2_PID +PROC2_EXIT_CODE=$? +# +# Combine the two exit codes; if either process exited abnormally, this +# test is a failure. +# +COMBINED_EXIT_CODE=$((PROC1_EXIT_CODE | PROC_2_EXIT_CODE)) +if [ $COMBINED_EXIT_CODE"" = "0" ] +then + numReceived1=`cat $PROC1_OUTFILE | grep -E '^[0-9]+\ messages received, accumulator=.*' |awk '{print $1}'` + receiveMask1=`cat $PROC1_OUTFILE | grep -E '^[0-9]+\ messages received, accumulator=.*' |awk '{print $4}'|sed -e 's/.*=//'` + numReceived2=`cat $PROC2_OUTFILE | grep -E '^[0-9]+\ messages received, accumulator=.*' |awk '{print $1}'` + receiveMask2=`cat $PROC2_OUTFILE | grep -E '^[0-9]+\ messages received, accumulator=.*' |awk '{print $4}'|sed -e 's/.*=//'` + + echo "proc 1: "$numReceived1" messages received, receive mask ["$receiveMask1"]" + echo "proc 2: "$numReceived2" messages received, receive mask ["$receiveMask2"]" + + if [ $numReceived1"" -gt $RECEIVES_REQUIRED ] && [ $numReceived2"" -gt $RECEIVES_REQUIRED ] + then + echo "########################################################" + echo " TEST SUCCEEDED: PROC 1 RECEIVED "$numReceived1"/"$TRANSMITS_TOTAL", "$RECEIVES_REQUIRED" required" + echo " PROC 2 RECEIVED "$numReceived2"/"$TRANSMITS_TOTAL", "$RECEIVES_REQUIRED" required" + echo "########################################################" + else + echo "########################################################" + echo " TEST FAILED: PROC 1 RECEIVED "$numReceived1"/"$TRANSMITS_TOTAL", "$RECEIVES_REQUIRED" required" + echo " PROC 2 RECEIVED "$numReceived2"/"$TRANSMITS_TOTAL", "$RECEIVES_REQUIRED" required" + echo "########################################################" + exit 2 + fi +else + echo ${0##*/}": FAILED ("$PROC1_EXIT_CODE":"$PROC2_EXIT_CODE")" + exit $COMBINED_EXIT_CODE +fi +rm $PROC1_OUTFILE $PROC2_OUTFILE diff --git a/integration-testing/integration-tests/thing-integration-test.js b/integration-testing/integration-tests/thing-integration-test.js new file mode 100644 index 0000000..5103720 --- /dev/null +++ b/integration-testing/integration-tests/thing-integration-test.js @@ -0,0 +1,274 @@ +/* + * 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 +const thingShadow = require('..').thingShadow; +const cmdLineProcess = require('../examples/lib/cmdline'); +const isUndefined = require('../common/lib/is-undefined.js'); + +//begin module + +// +// This integration test runs as two processes and uses the example program +// command line flags for configuration. One process (-t 1) updates a thing shadow +// and subscribes to an MQTT topic; the other (-t 2) listens to deltas on the thing +// shadow and publishes to the MQTT topic. The mode 1 process sends a value with +// each update and doubles it afterwards; the mode 2 process publishes this value +// back to the mode 1 process. Each process adds the value received in an +// accumulator. After the mode 1 process has sent 48 messages, it prepares to exit +// and sends a 'quit' command on the thing shadow which forces the mode 2 process +// to enter its exit logic. As both processes exit, they count the number of bits +// set in their accumulators. This test verifies that thing shadow update and +// deltas are working as well as non-thing publish and subscribe. Since actual +// values are used in the test, it verifies that simple state objects are +// transferred correctly, and because it maintains a bitmask of received values it +// allows for some messages to be lost due to QOS:0. A passing integration test +// here should probably expect >90% of the messages to be received on both sides. +// +// NOTE: The mode 1 process needs to be started first, and the mode 2 process +// started less than 10 seconds later. +// +// NOTE: 48 is used as the number of messages here because it's the last power of +// 2 less than 53, which is the mantissa size of Node's native number type (64-bit +// float). +// + +function processTest( args, argsRemaining ) { +// +// Use the command line flag '--thing-name | -T' to pass in a thing name/topic +// prefix; this allows us to run multiple copies of this test simultaneously +// within the same AWS account +// +var topicPrefix = args.thingName; +var integrationTestTopic = 'integrationTestTopic'; +var integrationTestShadow = 'integrationTestShadow'; + +if (!isUndefined(topicPrefix)) { + integrationTestTopic=topicPrefix+'/'+integrationTestTopic; +} + +if (!isUndefined(topicPrefix)) { + integrationTestShadow=topicPrefix+'-'+integrationTestShadow; +} + +var customAuthHeaders; +var region = args.region; + +if(args.Protocol === 'wss-custom-auth') { + customAuthHeaders = JSON.parse(process.env.CUSTOM_AUTH_HEADERS); + region = 'us-west-2'; +} + +// +// Instantiate the thing shadow class. +// +const thingShadows = thingShadow({ + keyPath: args.privateKey, + certPath: args.clientCert, + caPath: args.caCert, + clientId: args.clientId, + region: region, + reconnectPeriod: args.reconnectPeriod, + protocol: args.Protocol, + port: args.Port, + host: args.Host, + debug: args.Debug, + customAuthHeaders: customAuthHeaders +}); + +var value=1; +var count=1; +var clientToken; +var timer; + +var accumulator=0; + +function checkAccumulator() +{ +// +// Check the accumulator to see how many messages were received from the +// partner process via the MQTT passthrough. +// + var i, messages = 0, accSave=accumulator; + for (i = 0; i < 48; i++) + { + if (accumulator & 1) + { + messages++; + } + accumulator = accumulator>>1; + } + console.log(messages+' messages received, accumulator='+accSave.toString(16)); +} + +// +// This test demonstrates the use of thing shadows along with +// non-thing topics. One process updates a thing shadow and +// subscribes to a non-thing topic; the other receives delta +// updates on the thing shadow on publishes to the non-thing +// topic. +// +function updateThingShadow( ) +{ + if (count < 48) + { + console.log('updating thing shadow...'); + clientToken = thingShadows.update( integrationTestShadow, + { state: { desired: { value: value, quit: 0 }}} ); + value = value*2; + count++; + } + else + { + checkAccumulator(); +// +// Tell the partner to exit. +// + clientToken = thingShadows.update( integrationTestShadow, + { state: { desired: { value: value, quit: 1 }}} ); + setTimeout( function() { process.exit(0); }, 500 ); + } +} + +thingShadows + .on('connect', function() { + console.log('connected to things instance, registering thing name'); + + if (args.testMode === 1) + { +// +// This process will update a thing shadow and subscribe to a non- +// thing topic. +// + thingShadows.register( integrationTestShadow, { ignoreDeltas: true } ); + console.log('subscribing to non-thing topic'); + thingShadows.subscribe( integrationTestTopic ); +// +// Start updating after 10 seconds. +// + timer = setInterval( function() { + updateThingShadow(); + }, 10000 ); + } + else + { +// +// This process will listen to deltas on a thing shadow and publish to a +// non-thing topic. +// + thingShadows.register( integrationTestShadow ); + } + }); +thingShadows + .on('close', function() { + console.log('close'); + process.exit(1); + }); +thingShadows + .on('reconnect', function() { + console.log('reconnect'); + process.exit(1); + }); +thingShadows + .on('offline', function() { + console.log('offline'); + process.exit(1); + }); +thingShadows + .on('error', function(error) { + console.log('error', error); + process.exit(1); + }); +thingShadows + .on('message', function(topic, payload) { + console.log('received on \''+topic+'\': '+ payload.toString()); + accumulator += JSON.parse( payload.toString() ).value; + clearInterval( timer ); +// +// After a few seconds, update the thing shadow and if no message has +// been received after 10 seconds, try again. +// + setTimeout( function() { + updateThingShadow(); + timer = setInterval( function() { + updateThingShadow(); + }, 10000 ); + }, args.delay ); + + }); +// +// Only the second process is interested in delta events. +// +if (args.testMode===2) +{ + thingShadows + .on('delta', function(thingName, stateObject) { + + console.log('received delta, state='+JSON.stringify( stateObject.state)); + + if (!stateObject.state.quit) + { + thingShadows.publish( integrationTestTopic, JSON.stringify(stateObject.state)); + accumulator += stateObject.state.value; + } + else + { + checkAccumulator(); +// +// Our partner has told us the test has ended; it's our responsibility to delete +// the shadow afterwards and then exit ourselves. +// + setTimeout( function() { + thingShadows.delete( integrationTestShadow ); + setTimeout( function() { process.exit(0); }, 500 ); }, 500 ); + } + }); +} + +thingShadows + .on('status', function(thingName, statusType, clientToken, stateObject) { + if (statusType !== 'accepted') + { +// +// This update wasn't accepted; do a get operation to re-sync. Wait +// a few seconds, then get the thing shadow to re-sync; restart the +// interval timer. +// + clearInterval( timer ); + + console.log('status: '+statusType+', state: '+ + JSON.stringify(stateObject)); + setTimeout( function() { + clientToken = thingShadows.get( integrationTestShadow ); + timer = setInterval( function() { + updateThingShadow(); + }, 10000 ); + }, args.delay ); + } + }); +} + +module.exports = cmdLineProcess; + +if (require.main === module) { + cmdLineProcess('connect to the AWS IoT service and demonstrate thing shadow APIs, test modes 1-2', + process.argv.slice(2), processTest ); +} + + diff --git a/integration-testing/run-integration-tests.sh b/integration-testing/run-integration-tests.sh new file mode 100755 index 0000000..df5645d --- /dev/null +++ b/integration-testing/run-integration-tests.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# + +INTEGRATION_TEST_PATH=./integration-tests + +TEST_COUNT=0 +# +# Run each integration test; if it exits with a non-zero status, +# stop testing and return that status to the parent process. +# +for file in `ls $INTEGRATION_TEST_PATH` +do +# +# Only run executable files. +# + if [ -x $INTEGRATION_TEST_PATH/$file ] + then +# +# If not running in parallel mode, execute each test runner in sequence. +# + if [ $PARALLEL_EXECUTION"" = "" ] + then + echo "###################################################################" + echo ${0##*/}": running "${file##*/}"..." + echo "###################################################################" + $INTEGRATION_TEST_PATH/$file + statusValue=$? + echo "###################################################################" + echo ${0##*/}": complete, status="$statusValue + echo "###################################################################" + if [ $statusValue"" != 0 ] + then + exit $statusValue"" + fi + else +# +# If running in parallel mode, execute all test runners simultaneously. +# + echo "###################################################################" + echo ${0##*/}": running "${file##*/}" (parallel mode)..." + echo "###################################################################" + $INTEGRATION_TEST_PATH/$file & + PIDS[$TEST_COUNT]=$! + NAMES[$TEST_COUNT]=${file##*/} + TEST_COUNT=$((TEST_COUNT+1)) + fi + fi +done + +if [ $PARALLEL_EXECUTION"" != "" ] +then + COMBINED_EXIT_CODE=0 +# +# Wait on all test runner processes. +# + TEST_COUNT=0 + for PID in "${PIDS[@]}" + do + wait "$PID" + EXIT_CODE=$? + echo "###################################################################" + echo ${NAMES[$TEST_COUNT]}" complete, status="$EXIT_CODE + echo "###################################################################" + TEST_COUNT=$((TEST_COUNT+1)) + COMBINED_EXIT_CODE=$((EXIT_CODE | COMBINED_EXIT_CODE)) + done + echo "###################################################################" + echo ${0##*/}": all test runners complete, status="$COMBINED_EXIT_CODE + echo "###################################################################" + exit $COMBINED_EXIT_CODE +else +# +# All test runners have completed successfully, exit with status 0 +# + exit 0 +fi diff --git a/integration-testing/run-tests.sh b/integration-testing/run-tests.sh new file mode 100755 index 0000000..17f1e8b --- /dev/null +++ b/integration-testing/run-tests.sh @@ -0,0 +1,220 @@ +#!/bin/bash +# +# Master integration test runner for the AWS IoT Node.js device SDK. +# +# USAGE +# +# run-tests.sh +# +# PARAMETERS +# +# : [websocket|certificate] +# +# This program will first validate the given parameters, then attempt to +# retrieve required secrets from AWS secrets manager; if both are successful, it +# will execute all of the scripts under the 'integration-tests' directory +# until either one of them exits with a non-zero status, or they have all +# been executed. If this program exits with a zero status, that indicates +# that both the test setup and the test execution was successful. +# +# RETURNS +# +# 0 if successful, non-zero otherwise +# +# SECRET HANDLING +# +# This script handles retrieving secrets from AWS secrets manager; for +# websocket authentication, it will place the appropriate values in the AWS_ACCESS_KEY_ID +# and AWS_SECRET_ACCESS_KEY environment variables used by the SDK. For +# certificate authentication, the certificate and private key will be stored +# in PEM-format temporary files, along with the root CA certificate; +# the files are named according to the default naming convention +# specified in the README.md, and the following environment variable points +# to their location: +# +# CERT_DIR +# +# Temporary certificate and private key files are deleted after running the +# individual test runner scripts. +# +# + +# +# Set a randomized directory name to isolate this test run from others +# in the same environment and make it available to sub-scripts via export. +# +export NPMTEST_DIR="/tmp/npmtest-"$RANDOM + +# +# Validate arguments +# +if [ $# -eq 1 ] +then + export AUTHENTICATION_TYPE=$1 + + if [ $AUTHENTICATION_TYPE"" != "websocket" ] && \ + [ $AUTHENTICATION_TYPE"" != "certificate" ] + then + echo ${0##*/}": authentication-type must be one of [websocket|certificate]" + exit 2 + fi + export LONG_RUNNING_TEST="" +else + echo ${0##*/}" " + exit 1 +fi + +# +# Set node/npm paths +NODE=`which node` +if [ ! -x $NODE"" ] +then + echo ${0##*/}": can't find node, exiting..." + exit 1 +fi + +NPM=`which npm` +if [ ! -x $NPM"" ] +then + echo ${0##*/}": can't find npm, exiting..." + exit 1 +fi + +# +# node-gyp requires that $HOME be defined +# +#export HOME=$PWD + +# +# This script will run all of the programs under the integration-tests directory until +# either one executes with a non-zero status or they have all been run. +# +RUN_INTEGRATION_TESTS='./run-integration-tests.sh' + +# +# Install the Node.js SDK under a new temporary directory. +# +echo "Running integration tests in ${NPMTEST_DIR}" +mkdir -p $NPMTEST_DIR +(cp -r ../../aws-iot-device-sdk-js $NPMTEST_DIR; cd $NPMTEST_DIR) +if [ $? != "0" ] +then + echo "###################################################################" + echo ${0##*/}": unable to copy iot sdk to test directory!" + echo "###################################################################" + exit 4 +fi + +# +# Attempt an npm install of the AWS Node.js SDK for control plane access for creating test jobs +# +(cd $NPMTEST_DIR/aws-iot-device-sdk-js; $NPM install aws-sdk) +if [ $? != "0" ] +then + echo "###################################################################" + echo ${0##*/}": unable to npm install aws-sdk!" + echo "###################################################################" + exit 4 +fi + + +# +# Attempt an npm install of the Node.js SDK +# +(cd $NPMTEST_DIR/aws-iot-device-sdk-js; $NPM install) +if [ $? != "0" ] +then + echo "###################################################################" + echo ${0##*/}": unable to npm install aws iot device sdk!" + echo "###################################################################" + exit 4 +fi + +# +# The SDK installed without errors; now, retrieve credentials +# +echo "###################################################################" +echo ${0##*/}": retrieving AWS credentials from AWS SecretsManager" +echo "###################################################################" +# fetch secret value and strip quotes with sed +principal=$(aws --region us-east-1 secretsmanager get-secret-value --secret-id V1IotSdkIntegrationTestWebsocketAccessKeyId --query SecretString | sed -n 's/^"\(.*\)"/\1/p') +if [ $? == "0" ] +then + echo ${0##*/}": retrieved ws testing access key id" +else + echo ${0##*/}": couldn't retrieve ws testing access key id!" + exit 5 +fi + +# fetch secret value and strip quotes with sed +credential=$(aws --region us-east-1 secretsmanager get-secret-value --secret-id V1IotSdkIntegrationTestWebsocketSecretAccessKey --query SecretString | sed -n 's/^"\(.*\)"/\1/p') +if [ $? == "0" ] +then + echo ${0##*/}": retrieved ws testing secret access key" +else + echo ${0##*/}": couldn't retrieve ws testing secret access key!" + exit 6 +fi + +case $AUTHENTICATION_TYPE"" in + + websocket) + export AWS_ACCESS_KEY_ID=$principal + export AWS_SECRET_ACCESS_KEY=$credential + + $RUN_INTEGRATION_TESTS + exit $? + ;; + + certificate) + export JOBS_AWS_ACCESS_KEY_ID=$principal + export JOBS_AWS_SECRET_ACCESS_KEY=$credential + + export CERT_DIR=$NPMTEST_DIR/certs + mkdir -p $CERT_DIR + echo "###################################################################" + echo ${0##*/}": retrieving certificate credentials from AWS Secrets Manager" + echo "###################################################################" + + # fetch secret value, strip quotes and replace "\n" with an actual newline + aws --region us-east-1 secretsmanager get-secret-value --secret-id V1IotSdkIntegrationTestCertificate --query SecretString | sed -n 's/^"\(.*\)"/\1/p' | sed 's/\\n/\ +/g' > $CERT_DIR/certificate.pem.crt + if [ $? == "0" ] + then + echo ${0##*/}": retrieved Certificate" + else + echo ${0##*/}": couldn't retrieve Certificate!" + exit 5 + fi + + # fetch secret value, strip quotes and replace "\n" with an actual newline + aws --region us-east-1 secretsmanager get-secret-value --secret-id V1IotSdkIntegrationTestPrivateKey --query SecretString | sed -n 's/^"\(.*\)"/\1/p' | sed 's/\\n/\ +/g' > $CERT_DIR/private.pem.key + if [ $? == "0" ] + then + echo ${0##*/}": retrieved Private Key" + else + echo ${0##*/}": couldn't retrieve Private Key!" + exit 6 + fi + + # + # Retrieve the root CA certificate + # + curl -s 'https://www.amazontrust.com/repository/AmazonRootCA1.pem' > $CERT_DIR/root-CA.crt + if [ $? == "0" ] + then + echo ${0##*/}": retrieved root CA certificate" + else + echo ${0##*/}": couldn't retrieve root CA certificate!" + exit 7 + fi + + $RUN_INTEGRATION_TESTS + exit $? + ;; + + *) + echo ${0##*/}": unsupported authentication type ("$AUTHENTICATION_TYPE")" + ;; +esac diff --git a/package.json b/package.json index d061d45..eb6cbac 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "mqtt" ], "dependencies": { - "crypto-js": "3.1.6", + "crypto-js": "4.0.0", "minimist": "1.2.5", "mqtt": "4.2.8", "websocket-stream": "^5.5.2"