From e5c515f85cf95cca3f3589106f99bf2aab7d507f Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 8 Jul 2021 10:15:42 -0700 Subject: [PATCH 1/9] Port integration tests from internal to external; add ci placeholder; upgrade crypto-js --- .github/workflows/ci.yml | 30 ++ .../device-integration-test.js | 174 +++++++++++ .../jobs-integration-test.js | 247 ++++++++++++++++ .../offline-publishing-test.js | 194 +++++++++++++ .../run-device-integration-test.sh | 114 ++++++++ .../run-jobs-integration-test.sh | 110 +++++++ .../run-offline-publishing-test.sh | 150 ++++++++++ .../run-thing-integration-test.sh | 119 ++++++++ .../thing-integration-test.js | 274 ++++++++++++++++++ integration-testing/run-integration-tests.sh | 76 +++++ integration-testing/run-tests.sh | 220 ++++++++++++++ package.json | 2 +- 12 files changed, 1709 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 integration-testing/integration-tests/device-integration-test.js create mode 100644 integration-testing/integration-tests/jobs-integration-test.js create mode 100644 integration-testing/integration-tests/offline-publishing-test.js create mode 100755 integration-testing/integration-tests/run-device-integration-test.sh create mode 100755 integration-testing/integration-tests/run-jobs-integration-test.sh create mode 100755 integration-testing/integration-tests/run-offline-publishing-test.sh create mode 100755 integration-testing/integration-tests/run-thing-integration-test.sh create mode 100644 integration-testing/integration-tests/thing-integration-test.js create mode 100755 integration-testing/run-integration-tests.sh create mode 100755 integration-testing/run-tests.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cae7f1e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +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 }} + +jobs: + unit-tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + image: + - al2012-x64 + - al2-x64 + steps: + # We can't use the `uses: docker://image` version yet, GitHub lacks authentication for actions -> packages + - name: Build ${{ env.PACKAGE_NAME }} + run: | + which node + which npm + ls + 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..d1ab269 --- /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 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 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 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 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" From 6263d5003d16326ba849d3212144a0d7c12cbef8 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 8 Jul 2021 10:20:57 -0700 Subject: [PATCH 2/9] Remove matrix from unit test def --- .github/workflows/ci.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cae7f1e..69cf021 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false - matrix: - image: - - al2012-x64 - - al2-x64 + steps: # We can't use the `uses: docker://image` version yet, GitHub lacks authentication for actions -> packages - name: Build ${{ env.PACKAGE_NAME }} From 9cbf0a558afff4f8bdb5e550454595f4cc7e353e Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 8 Jul 2021 10:24:32 -0700 Subject: [PATCH 3/9] Unit test try #1 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69cf021..8da81ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,5 +23,7 @@ jobs: run: | which node which npm - ls + npm install + npm test + From 7c78c966276d6dac7c991b9fd6f57fe03140c6d3 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 8 Jul 2021 10:30:07 -0700 Subject: [PATCH 4/9] Try #2 --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8da81ec..1686bcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,9 @@ jobs: run: | which node which npm + pwd + cd ${{ github.workspace }} + pwd npm install npm test From 06b2745e8668823722a6d81407ed74623ba7a017 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 8 Jul 2021 10:40:09 -0700 Subject: [PATCH 5/9] Copy-paste github tutorial instead --- .github/workflows/ci.yml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1686bcb..acc9551 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,23 +10,22 @@ 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 jobs: unit-tests: runs-on: ubuntu-latest strategy: fail-fast: false + matrix: + node-version: [10.x, 12.x, 14.x, 15.x] steps: - # We can't use the `uses: docker://image` version yet, GitHub lacks authentication for actions -> packages - - name: Build ${{ env.PACKAGE_NAME }} - run: | - which node - which npm - pwd - cd ${{ github.workspace }} - pwd - npm install - npm test - - + - 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 ci + - run: npm run build --if-present + - run: npm test From ed58622d1c47b9b85a0fcb5151b5a33df797af33 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 8 Jul 2021 10:44:36 -0700 Subject: [PATCH 6/9] Not comfortable committing package-lock yet --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acc9551..623d547 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [10.x, 12.x, 14.x, 15.x] + node-version: [10.x, 12.x, 14.x] steps: - uses: actions/checkout@v2 @@ -26,6 +26,5 @@ jobs: uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm run build --if-present + - run: npm install - run: npm test From d6683250b08c54c8e3183b1f31bb573b924b9682 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 8 Jul 2021 10:47:00 -0700 Subject: [PATCH 7/9] Node 10 only for now --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 623d547..2cc3d84 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [10.x, 12.x, 14.x] + node-version: [10.x] steps: - uses: actions/checkout@v2 From 1d24f8f7711ff2764bd096c8dacbda1f592a4f6f Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 8 Jul 2021 10:54:16 -0700 Subject: [PATCH 8/9] Integration test try #1 --- .github/workflows/ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cc3d84..02b3f0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,3 +28,19 @@ jobs: 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 }} From 01e42b91f58448c19e4c19438c656f16b03e3ae2 Mon Sep 17 00:00:00 2001 From: Bret Ambrose Date: Thu, 8 Jul 2021 11:03:28 -0700 Subject: [PATCH 9/9] Specify region and disable imds region resolution --- .github/workflows/ci.yml | 1 + integration-testing/run-tests.sh | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02b3f0f..771f73b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ env: 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: diff --git a/integration-testing/run-tests.sh b/integration-testing/run-tests.sh index d1ab269..17f1e8b 100755 --- a/integration-testing/run-tests.sh +++ b/integration-testing/run-tests.sh @@ -137,7 +137,7 @@ echo "###################################################################" echo ${0##*/}": retrieving AWS credentials from AWS SecretsManager" echo "###################################################################" # fetch secret value and strip quotes with sed -principal=$(aws secretsmanager get-secret-value --secret-id V1IotSdkIntegrationTestWebsocketAccessKeyId --query SecretString | sed -n 's/^"\(.*\)"/\1/p') +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" @@ -147,7 +147,7 @@ else fi # fetch secret value and strip quotes with sed -credential=$(aws secretsmanager get-secret-value --secret-id V1IotSdkIntegrationTestWebsocketSecretAccessKey --query SecretString | sed -n 's/^"\(.*\)"/\1/p') +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" @@ -177,7 +177,7 @@ case $AUTHENTICATION_TYPE"" in echo "###################################################################" # fetch secret value, strip quotes and replace "\n" with an actual newline - aws secretsmanager get-secret-value --secret-id V1IotSdkIntegrationTestCertificate --query SecretString | sed -n 's/^"\(.*\)"/\1/p' | sed 's/\\n/\ + 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 @@ -188,7 +188,7 @@ case $AUTHENTICATION_TYPE"" in fi # fetch secret value, strip quotes and replace "\n" with an actual newline - aws secretsmanager get-secret-value --secret-id V1IotSdkIntegrationTestPrivateKey --query SecretString | sed -n 's/^"\(.*\)"/\1/p' | sed 's/\\n/\ + 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