From 999e29b7da3bf5208f18b2a4340720e441e12875 Mon Sep 17 00:00:00 2001 From: Steve Harris Date: Mon, 2 Apr 2018 23:38:52 +0000 Subject: [PATCH 1/6] Jobs support with custom auth support --- CMakeLists.txt | 2 + README.md | 3 + include/ResponseCode.hpp | 7 +- include/jobs/Jobs.hpp | 219 +++++++++++++ network/WebSocket/WebSocketConnection.cpp | 58 +++- network/WebSocket/WebSocketConnection.hpp | 29 ++ samples/Jobs/CMakeLists.txt | 82 +++++ samples/Jobs/JobsSample.cpp | 383 ++++++++++++++++++++++ samples/Jobs/JobsSample.hpp | 68 ++++ samples/README.md | 6 + src/ResponseCode.cpp | 3 + src/jobs/Jobs.cpp | 340 +++++++++++++++++++ tests/integration/include/JobsTest.hpp | 59 ++++ tests/integration/src/IntegTestRunner.cpp | 12 + tests/integration/src/JobsTest.cpp | 327 ++++++++++++++++++ tests/unit/src/ResponseCodeTests.cpp | 5 + tests/unit/src/jobs/JobsTests.cpp | 264 +++++++++++++++ 17 files changed, 1854 insertions(+), 13 deletions(-) create mode 100644 include/jobs/Jobs.hpp create mode 100644 samples/Jobs/CMakeLists.txt create mode 100644 samples/Jobs/JobsSample.cpp create mode 100644 samples/Jobs/JobsSample.hpp create mode 100644 src/jobs/Jobs.cpp create mode 100644 tests/integration/include/JobsTest.hpp create mode 100644 tests/integration/src/JobsTest.cpp create mode 100644 tests/unit/src/jobs/JobsTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ad8a4e1..5e6854c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -161,6 +161,8 @@ add_subdirectory(tests/unit) add_subdirectory(samples/PubSub) +add_subdirectory(samples/Jobs) + add_subdirectory(samples/ShadowDelta) add_subdirectory(samples/Discovery EXCLUDE_FROM_ALL) diff --git a/README.md b/README.md index cc72cb3..89fe3be 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ The Device SDK provides functionality to create and maintain a MQTT Connection. ### Thing Shadow This SDK implements the specific protocol for Thing Shadows to retrieve, update and delete Thing Shadows adhering to the protocol that is implemented to ensure correct versioning and support for client tokens. It abstracts the necessary MQTT topic subscriptions by automatically subscribing to and unsubscribing from the reserved topics as needed for each API call. Inbound state change requests are automatically signalled via a configurable callback. +### Jobs +This SDK also implements the Jobs protocol to interact with the AWS IoT Jobs service. The IoT Job service manages deployment of IoT fleet wide tasks such as device software/firmware deployments and updates, rotation of security certificates, device reboots, and custom device specific management tasks. For additional information please see the [Jobs developer guide](https://docs.aws.amazon.com/iot/latest/developerguide/iot-jobs.html). + ## Design Goals of this SDK The C++ SDK was specifically designed for devices that are not resource constrained and required advanced features such as Message queueing, multi-threading support and the latest language features diff --git a/include/ResponseCode.hpp b/include/ResponseCode.hpp index 0a2e58c..2bfb7fa 100644 --- a/include/ResponseCode.hpp +++ b/include/ResponseCode.hpp @@ -193,7 +193,11 @@ namespace awsiotsdk { // Discovery Response Parsing Error Codes - DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR = -1200 ///< Discover Response Json is missing expected keys + DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR = -1200, ///< Discover Response Json is missing expected keys + + // Jobs Error Codes + + JOBS_INVALID_TOPIC_ERROR = -1300 ///< Jobs invalid topic }; /** @@ -314,6 +318,7 @@ namespace awsiotsdk { const util::String DISCOVER_ACTION_SERVER_ERROR_STRING("Server returned unknown error while performing the discovery action"); const util::String DISCOVER_ACTION_REQUEST_OVERLOAD_STRING("The discovery action is overloading the server, try again after some time"); const util::String DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING("The discover response JSON is incomplete "); + const util::String JOBS_INVALID_TOPIC_ERROR_STRING("Invalid jobs topic"); /** * Takes in a Response Code and returns the appropriate error/success string diff --git a/include/jobs/Jobs.hpp b/include/jobs/Jobs.hpp new file mode 100644 index 0000000..33c2130 --- /dev/null +++ b/include/jobs/Jobs.hpp @@ -0,0 +1,219 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.hpp + * @brief + * + */ + +#pragma once + +#include "mqtt/Client.hpp" + +namespace awsiotsdk { + class Jobs { + public: + // Disabling default and copy constructors. + Jobs() = delete; // Delete Default constructor + Jobs(const Jobs &) = delete; // Delete Copy constructor + Jobs(Jobs &&) = default; // Default Move constructor + Jobs &operator=(const Jobs &) & = delete; // Delete Copy assignment operator + Jobs &operator=(Jobs &&) & = default; // Default Move assignment operator + + /** + * @brief Create factory method. Returns a unique instance of Jobs + * + * @param p_mqtt_client - mqtt client + * @param qos - QoS + * @param thing_name - Thing name + * @param client_token - Client token for correlating messages (optional) + * + * @return std::unique_ptr pointing to a unique Jobs instance + */ + static std::unique_ptr Create(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token = util::String()); + + enum JobExecutionTopicType { + JOB_UNRECOGNIZED_TOPIC = 0, + JOB_GET_PENDING_TOPIC, + JOB_START_NEXT_TOPIC, + JOB_DESCRIBE_TOPIC, + JOB_UPDATE_TOPIC, + JOB_NOTIFY_TOPIC, + JOB_NOTIFY_NEXT_TOPIC, + JOB_WILDCARD_TOPIC + }; + + enum JobExecutionTopicReplyType { + JOB_UNRECOGNIZED_TOPIC_TYPE = 0, + JOB_REQUEST_TYPE, + JOB_ACCEPTED_REPLY_TYPE, + JOB_REJECTED_REPLY_TYPE, + JOB_WILDCARD_REPLY_TYPE + }; + + enum JobExecutionStatus { + JOB_EXECUTION_STATUS_NOT_SET = 0, + JOB_EXECUTION_QUEUED, + JOB_EXECUTION_IN_PROGRESS, + JOB_EXECUTION_FAILED, + JOB_EXECUTION_SUCCEEDED, + JOB_EXECUTION_CANCELED, + JOB_EXECUTION_REJECTED, + /*** + * Used for any status not in the supported list of statuses + */ + JOB_EXECUTION_UNKNOWN_STATUS = 99 + }; + + /** + * @brief GetJobTopic + * + * This function creates a job topic based on the provided parameters. + * + * @param topicType - Jobs topic type + * @param replyType - Topic reply type (optional) + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * + * @return nullptr on error, unique_ptr pointing to a topic string if successful + */ + std::unique_ptr GetJobTopic(JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType = JOB_REQUEST_TYPE, + const util::String &jobId = util::String()); + + /** + * @brief SendJobsQuery + * + * Send a query to the Jobs service using the provided mqtt client + * + * @param topicType - Jobs topic type for type of query + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsQuery(JobExecutionTopicType topicType, + const util::String &jobId = util::String()); + + /** + * @brief SendJobsStartNext + * + * Call Jobs start-next API to start the next pending job execution and trigger response + * + * @param statusDetails - Status details to be associated with started job execution (optional) + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsStartNext(const util::Map &statusDetailsMap = util::Map()); + + /** + * @brief SendJobsDescribe + * + * Send request for job execution details + * + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also + * be omitted to request all pending and in progress job executions + * @param executionNumber - Specific execution number to describe, omit to match latest + * @param includeJobDocument - Flag to indicate whether response should include job document + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsDescribe(const util::String &jobId = util::String(), + int64_t executionNumber = 0, // set to 0 to ignore + bool includeJobDocument = true); + + /** + * @brief SendJobsUpdate + * + * Send update for specified job + * + * @param jobId - Job id associated with job execution to be updated + * @param status - New job execution status + * @param statusDetailsMap - Status details to be associated with job execution (optional) + * @param expectedVersion - Optional expected current job execution number, error response if mismatched + * @param executionNumber - Specific execution number to update, omit to match latest + * @param includeJobExecutionState - Include job execution state in response (optional) + * @param includeJobDocument - Include job document in response (optional) + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsUpdate(const util::String &jobId, + JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, // set to 0 to ignore + int64_t executionNumber = 0, // set to 0 to ignore + bool includeJobExecutionState = false, + bool includeJobDocument = false); + + /** + * @brief CreateJobsSubscription + * + * Create a Jobs Subscription instance + * + * @param p_app_handler - Application Handler instance + * @param p_app_handler_data - Data to be passed to application handler. Can be nullptr + * @param topicType - Jobs topic type to subscribe to (defaults to JOB_WILDCARD_TOPIC) + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * @param replyType - Topic reply type (optional, defaults to JOB_REQUEST_TYPE which omits the reply type in the subscription) + * + * @return shared_ptr Subscription instance + */ + std::shared_ptr CreateJobsSubscription(mqtt::Subscription::ApplicationCallbackHandlerPtr p_app_handler, + std::shared_ptr p_app_handler_data, + JobExecutionTopicType topicType = JOB_WILDCARD_TOPIC, + JobExecutionTopicReplyType replyType = JOB_REQUEST_TYPE, + const util::String &jobId = util::String()); + protected: + std::shared_ptr p_mqtt_client_; + mqtt::QoS qos_; + util::String thing_name_; + util::String client_token_; + + /** + * @brief Jobs constructor + * + * Create Jobs object storing given parameters in created instance + * + * @param p_mqtt_client - mqtt client + * @param qos - QoS + * @param thing_name - Thing name + * @param client_token - Client token for correlating messages (optional) + */ + Jobs(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token); + + static bool BaseTopicRequiresJobId(JobExecutionTopicType topicType); + static const util::String GetOperationForBaseTopic(JobExecutionTopicType topicType); + static const util::String GetSuffixForTopicType(JobExecutionTopicReplyType replyType); + static const util::String GetExecutionStatus(JobExecutionStatus status); + static util::String Escape(const util::String &value); + static util::String SerializeStatusDetails(const util::Map &statusDetailsMap); + + util::String SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, + int64_t executionNumber = 0, + bool includeJobExecutionState = false, + bool includeJobDocument = false); + util::String SerializeDescribeJobExecutionPayload(int64_t executionNumber = 0, + bool includeJobDocument = true); + util::String SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap = util::Map()); + util::String SerializeClientTokenPayload(); + }; +} diff --git a/network/WebSocket/WebSocketConnection.cpp b/network/WebSocket/WebSocketConnection.cpp index 847638b..9214ffd 100644 --- a/network/WebSocket/WebSocketConnection.cpp +++ b/network/WebSocket/WebSocketConnection.cpp @@ -47,6 +47,8 @@ #define X_AMZ_DATE "X-Amz-Date" #define X_AMZ_EXPIRES "X-Amz-Expires" #define X_AMZ_SECURITY_TOKEN "X-Amz-Security-Token" +#define X_AMZ_CUSTOMAUTHORIZER_NAME "X-Amz-CustomAuthorizer-Name" +#define X_AMZ_CUSTOMAUTHORIZER_SIGNATURE "X-Amz-CustomAuthorizer-Signature" #define SIGNING_KEY "AWS4" #define LONG_DATE_FORMAT_STR "%Y%m%dT%H%M%SZ" #define SIMPLE_DATE_FORMAT_STR "%Y%m%d" @@ -99,6 +101,11 @@ namespace awsiotsdk { bool server_verification_flag) : openssl_connection_(endpoint, endpoint_port, root_ca_location, tls_handshake_timeout, tls_read_timeout, tls_write_timeout, server_verification_flag) { + custom_authorizer_name_.clear(); + custom_authorizer_signature_.clear(); + custom_authorizer_token_name_.clear(); + custom_authorizer_token_.clear(); + endpoint_ = endpoint; endpoint_port_ = endpoint_port; root_ca_location_ = root_ca_location; @@ -125,6 +132,21 @@ namespace awsiotsdk { wss_frame_write_ = std::unique_ptr(new wslay_frame_iocb()); } + WebSocketConnection::WebSocketConnection(util::String endpoint, uint16_t endpoint_port, util::String root_ca_location, + std::chrono::milliseconds tls_handshake_timeout, + std::chrono::milliseconds tls_read_timeout, + std::chrono::milliseconds tls_write_timeout, + util::String custom_authorizer_name, util::String custom_authorizer_signature, + util::String custom_authorizer_token_name, util::String custom_authorizer_token, + bool server_verification_flag) + : WebSocketConnection(endpoint, endpoint_port, root_ca_location, "", "", "", "", tls_handshake_timeout, tls_read_timeout, + tls_write_timeout, false) { + custom_authorizer_name_ = custom_authorizer_name; + custom_authorizer_signature_ = custom_authorizer_signature; + custom_authorizer_token_name_ = custom_authorizer_token_name; + custom_authorizer_token_ = custom_authorizer_token; + } + ResponseCode WebSocketConnection::ConnectInternal() { // Init Tls ResponseCode rc = openssl_connection_.Initialize(); @@ -563,17 +585,12 @@ namespace awsiotsdk { } ResponseCode WebSocketConnection::WssHandshake() { + ResponseCode rc; + util::OStringStream stringStream; + // Assuming: // 1. Ssl socket is ready to do read/write. - // Create canonical query string - util::String canonical_query_string; - canonical_query_string.reserve(CANONICAL_QUERY_BUF_LEN); - ResponseCode rc = InitializeCanonicalQueryString(canonical_query_string); - if (ResponseCode::SUCCESS != rc) { - return rc; - } - // Create Wss handshake Http request // -> Generate Wss client key char client_key_buf[WSS_CLIENT_KEY_MAX_LEN + 1]; @@ -583,15 +600,32 @@ namespace awsiotsdk { return rc; } - // -> Assemble Wss Http request - util::OStringStream stringStream; - stringStream << "GET /mqtt?" << canonical_query_string << " " << HTTP_1_1 << "\r\n" - << "Host: " << endpoint_ << "\r\n" + if (custom_authorizer_name_.empty()) { + // Create canonical query string + util::String canonical_query_string; + canonical_query_string.reserve(CANONICAL_QUERY_BUF_LEN); + rc = InitializeCanonicalQueryString(canonical_query_string); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + + // -> Assemble Wss Http request + stringStream << "GET /mqtt?" << canonical_query_string << " " << HTTP_1_1 << "\r\n"; + } else { + // -> Assemble Wss Http request + stringStream << "GET /mqtt " << HTTP_1_1 << "\r\n" + << X_AMZ_CUSTOMAUTHORIZER_NAME << ": " << custom_authorizer_name_ << "\r\n" + << X_AMZ_CUSTOMAUTHORIZER_SIGNATURE << ": " << custom_authorizer_signature_ << "\r\n" + << custom_authorizer_token_name_ << ": " << custom_authorizer_token_ << "\r\n"; + } + + stringStream << "Host: " << endpoint_ << "\r\n" << "Connection: " << UPGRADE << "\r\n" << "Upgrade: " << WEBSOCKET << "\r\n" << "Sec-WebSocket-Version: " << SEC_WEBSOCKET_VERSION_13 << "\r\n" << "sec-websocket-key: " << client_key_buf << "\r\n" << "Sec-WebSocket-Protocol: " << MQTT_PROTOCOL << "\r\n\r\n"; + util::String request_string = stringStream.str(); // Send out request diff --git a/network/WebSocket/WebSocketConnection.hpp b/network/WebSocket/WebSocketConnection.hpp index 761c8cc..fa8a491 100644 --- a/network/WebSocket/WebSocketConnection.hpp +++ b/network/WebSocket/WebSocketConnection.hpp @@ -50,6 +50,10 @@ namespace awsiotsdk { util::String aws_access_key_id_; ///< Pointer to string containing the AWS Access Key Id. util::String aws_secret_access_key_; ///< Pointer to sstring containing the AWS Secret Access Key. util::String aws_session_token_; ///< Pointer to string containing the AWS Session Token. + util::String custom_authorizer_name_; ///< Pointer to string containing the custom authorizer name. + util::String custom_authorizer_signature_; ///< Pointer to string containing the authorizer signature. + util::String custom_authorizer_token_name_; ///< Pointer to string containing the authorizer token name. + util::String custom_authorizer_token_; ///< Pointer to string containing the authorizer token. util::String aws_region_; ///< Region for this connection util::String endpoint_; ///< Endpoint for this connection uint16_t endpoint_port_; ///< Endpoint port @@ -210,6 +214,31 @@ namespace awsiotsdk { std::chrono::milliseconds tls_read_timeout, std::chrono::milliseconds tls_write_timeout, bool server_verification_flag); + /** + * @brief Constructor for the WebSocket for MQTT implementation using custom authentication + * + * Performs any initialization required by the WebSocket layer. + * + * @param util::String endpoint - The target endpoint to connect to + * @param uint16_t endpoint_port - The port on the target to connect to + * @param util::String root_ca_location - Path of the location of the Root CA + * @param std::chrono::milliseconds tls_handshake_timeout - The value to use for timeout of handshake operation + * @param std::chrono::milliseconds tls_read_timeout - The value to use for timeout of read operation + * @param std::chrono::milliseconds tls_write_timeout - The value to use for timeout of write operation + * @param util::String custom_authorizer_name - Name of the authorizer function + * @param util::String custom_authorizer_signature - Authorizer signature + * @param util::String custom_authorizer_token_name - Authorizer token name + * @param util::String custom_authorizer_token - Authorizer token + * @param bool server_verification_flag - used to decide whether server verification is needed or not + * + */ + WebSocketConnection(util::String endpoint, uint16_t endpoint_port, util::String root_ca_location, + std::chrono::milliseconds tls_handshake_timeout, + std::chrono::milliseconds tls_read_timeout, std::chrono::milliseconds tls_write_timeout, + util::String custom_authorizer_name, util::String custom_authorizer_signature, + util::String custom_authorizer_token_name, util::String custom_authorizer_token, + bool server_verification_flag); + /** * @brief Check if WebSocket layer is still connected * diff --git a/samples/Jobs/CMakeLists.txt b/samples/Jobs/CMakeLists.txt new file mode 100644 index 0000000..b11e1c8 --- /dev/null +++ b/samples/Jobs/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.2 FATAL_ERROR) +project(aws-iot-cpp-samples CXX) + +###################################### +# Section : Disable in-source builds # +###################################### + +if (${PROJECT_SOURCE_DIR} STREQUAL ${PROJECT_BINARY_DIR}) + message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt and CMakeFiles folder.") +endif () + +######################################## +# Section : Common Build setttings # +######################################## +# Set required compiler standard to standard c++11. Disable extensions. +set(CMAKE_CXX_STANDARD 11) # C++11... +set(CMAKE_CXX_STANDARD_REQUIRED ON) #...is required... +set(CMAKE_CXX_EXTENSIONS OFF) #...without compiler extensions like gnu++11 + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/archive) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +# Configure Compiler flags +if (UNIX AND NOT APPLE) + # Prefer pthread if found + set(THREADS_PREFER_PTHREAD_FLAG ON) + set(CUSTOM_COMPILER_FLAGS "-fno-exceptions -Wall -Werror") +elseif (APPLE) + set(CUSTOM_COMPILER_FLAGS "-fno-exceptions -Wall -Werror") +elseif (WIN32) + set(CUSTOM_COMPILER_FLAGS "/W4") +endif () + +################################ +# Target : Build Jobs sample # +################################ +set(JOBS_SAMPLE_TARGET_NAME jobs-sample) +# Add Target +add_executable(${JOBS_SAMPLE_TARGET_NAME} "${PROJECT_SOURCE_DIR}/JobsSample.cpp;${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.cpp") + +# Add Target specific includes +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../common) +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}) + +# Configure Threading library +find_package(Threads REQUIRED) + +# Add SDK includes +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${CMAKE_BINARY_DIR}/${DEPENDENCY_DIR}/rapidjson/src/include) +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../include) + +target_link_libraries(${JOBS_SAMPLE_TARGET_NAME} PUBLIC "Threads::Threads") +target_link_libraries(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${SDK_TARGET_NAME}) + +# Copy Json config file +add_custom_command(TARGET ${JOBS_SAMPLE_TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy ${PROJECT_SOURCE_DIR}/../../common/SampleConfig.json $/config/SampleConfig.json) +set_property(TARGET ${JOBS_SAMPLE_TARGET_NAME} APPEND_STRING PROPERTY COMPILE_FLAGS ${CUSTOM_COMPILER_FLAGS}) + +# Gather list of all .cert files in "/cert" +add_custom_command(TARGET ${JOBS_SAMPLE_TARGET_NAME} PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy_directory ${PROJECT_SOURCE_DIR}/../../certs $/certs) + +if (MSVC) + target_sources(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.hpp) + source_group("Header Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.hpp) + source_group("Source Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.cpp) + + target_sources(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/JobsSample.hpp) + source_group("Header Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/JobsSample.hpp) + source_group("Source Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/JobsSample.cpp) +endif () + +######################### +# Add Network libraries # +######################### + +set(NETWORK_WRAPPER_DEST_TARGET ${JOBS_SAMPLE_TARGET_NAME}) +include(${PROJECT_SOURCE_DIR}/../../network/CMakeLists.txt.in) diff --git a/samples/Jobs/JobsSample.cpp b/samples/Jobs/JobsSample.cpp new file mode 100644 index 0000000..f5c5648 --- /dev/null +++ b/samples/Jobs/JobsSample.cpp @@ -0,0 +1,383 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsSample.cpp + * + * This example takes the parameters from the config/SampleConfig.json file and establishes + * a connection to the AWS IoT MQTT Platform. It performs several operations to + * demonstrate the basic capabilities of the AWS IoT Jobs platform. + * + * If all the certs are correct, you should see the list of pending Job Executions + * printed out by the GetPendingCallback callback. If there are any existing pending + * job executions each will be processed one at a time in the NextJobCallback callback. + * After all of the pending jobs have been processed the program will wait for + * notifications for new pending jobs and process them one at a time as they come in. + * + * In the Subscribe function you can see how each callback is registered for each corresponding + * Jobs topic. + * + */ + +#include +#include + +#ifdef USE_WEBSOCKETS +#include "WebSocketConnection.hpp" +#elif defined USE_MBEDTLS +#include "MbedTLSConnection.hpp" +#else +#include "OpenSSLConnection.hpp" +#endif + +#include "util/logging/Logging.hpp" +#include "util/logging/LogMacros.hpp" +#include "util/logging/ConsoleLogSystem.hpp" + +#include "ConfigCommon.hpp" +#include "jobs/Jobs.hpp" +#include "JobsSample.hpp" + +#define LOG_TAG_JOBS "[Sample - Jobs]" + +namespace awsiotsdk { + namespace samples { + ResponseCode JobsSample::GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "GetPendingCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Json Parse for GetPendingCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("inProgressJobs")) { + std::cout << "inProgressJobs : " << util::JsonParser::ToString(doc["inProgressJobs"]) << std::endl; + } + + if (doc.HasMember("queuedJobs")) { + std::cout << "queuedJobs : " << util::JsonParser::ToString(doc["queuedJobs"]) << std::endl; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "NextJobCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Json Parse for NextJobCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("execution")) { + std::cout << "execution : " << util::JsonParser::ToString(doc["execution"]) << std::endl; + + if (doc["execution"].HasMember("jobId")) { + util::Map statusDetailsMap; + + util::String jobId = doc["execution"]["jobId"].GetString(); + std::cout << "jobId : " << jobId << std::endl; + + if (doc["execution"].HasMember("jobDocument")) { + std::cout << "jobDocument : " << util::JsonParser::ToString(doc["execution"]["jobDocument"]) << std::endl; + statusDetailsMap.insert(std::make_pair("exampleDetail", "a value appropriate for your successful job")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_SUCCEEDED, statusDetailsMap); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "SendJobsUpdate failed. %s", ResponseHelper::ToString(rc).c_str()); + return rc; + } + } else { + statusDetailsMap.insert(std::make_pair("failureDetail", "Unable to process job document")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_FAILED, statusDetailsMap); + } + } + } else { + std::cout << "No job execution description found, nothing to do." << std::endl; + done_ = true; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::UpdateAcceptedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + std::cout << std::endl << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::UpdateRejectedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + std::cout << std::endl << "************" << std::endl; + + /* Do error handling here for when the update was rejected */ + + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::DisconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data) { + std::cout << "*******************************************" << std::endl + << client_id << " Disconnected!" << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::ReconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode reconnect_result) { + std::cout << "*******************************************" << std::endl + << client_id << " Reconnect Attempted. Result " << ResponseHelper::ToString(reconnect_result) + << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::ResubscribeCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode resubscribe_result) { + std::cout << "*******************************************" << std::endl + << client_id << " Resubscribe Attempted. Result" << ResponseHelper::ToString(resubscribe_result) + << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + + ResponseCode JobsSample::Subscribe() { + std::cout << "******** Subscribe ***************" << std::endl; + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_pending_handler = + std::bind(&JobsSample::GetPendingCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_next_handler = + std::bind(&JobsSample::NextJobCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_update_accepted_handler = + std::bind(&JobsSample::UpdateAcceptedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_update_rejected_handler = + std::bind(&JobsSample::UpdateRejectedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + util::Vector> topic_vector; + std::shared_ptr p_subscription; + + p_subscription = p_jobs_->CreateJobsSubscription(p_pending_handler, nullptr, Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_update_accepted_handler, nullptr, Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "+"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_update_rejected_handler, nullptr, Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, "+"); + topic_vector.push_back(p_subscription); + + ResponseCode rc = p_iot_client_->Subscribe(topic_vector, ConfigCommon::mqtt_command_timeout_); + return rc; + } + + ResponseCode JobsSample::InitializeTLS() { + ResponseCode rc = ResponseCode::SUCCESS; + +#ifdef USE_WEBSOCKETS + p_network_connection_ = std::shared_ptr( + new network::WebSocketConnection(ConfigCommon::endpoint_, ConfigCommon::endpoint_https_port_, + ConfigCommon::root_ca_path_, ConfigCommon::aws_region_, + ConfigCommon::aws_access_key_id_, + ConfigCommon::aws_secret_access_key_, + ConfigCommon::aws_session_token_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true)); + if (nullptr == p_network_connection_) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } +#elif defined USE_MBEDTLS + p_network_connection_ = std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, + true); + if (nullptr == p_network_connection_) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } +#else + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + rc = p_network_connection->Initialize(); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, + "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#endif + return rc; + } + + ResponseCode JobsSample::RunSample() { + done_ = false; + + ResponseCode rc = InitializeTLS(); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + + ClientCoreState::ApplicationDisconnectCallbackPtr p_disconnect_handler = + std::bind(&JobsSample::DisconnectCallback, this, std::placeholders::_1, std::placeholders::_2); + + ClientCoreState::ApplicationReconnectCallbackPtr p_reconnect_handler = + std::bind(&JobsSample::ReconnectCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + ClientCoreState::ApplicationResubscribeCallbackPtr p_resubscribe_handler = + std::bind(&JobsSample::ResubscribeCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + p_iot_client_ = std::shared_ptr(MqttClient::Create(p_network_connection_, + ConfigCommon::mqtt_command_timeout_, + p_disconnect_handler, nullptr, + p_reconnect_handler, nullptr, + p_resubscribe_handler, nullptr)); + if (nullptr == p_iot_client_) { + return ResponseCode::FAILURE; + } + + util::String client_id_tagged = ConfigCommon::base_client_id_; + client_id_tagged.append("_jobs_sample_"); + client_id_tagged.append(std::to_string(rand())); + std::unique_ptr client_id = Utf8String::Create(client_id_tagged); + + rc = p_iot_client_->Connect(ConfigCommon::mqtt_command_timeout_, ConfigCommon::is_clean_session_, + mqtt::Version::MQTT_3_1_1, ConfigCommon::keep_alive_timeout_secs_, + std::move(client_id), nullptr, nullptr, nullptr); + if (ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED != rc) { + return rc; + } + + p_jobs_ = Jobs::Create(p_iot_client_, mqtt::QoS::QOS1, ConfigCommon::thing_name_, client_id_tagged); + + rc = Subscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Subscribe failed. %s", ResponseHelper::ToString(rc).c_str()); + } else { + rc = p_jobs_->SendJobsQuery(Jobs::JOB_GET_PENDING_TOPIC); + + if (ResponseCode::SUCCESS == rc) { + rc = p_jobs_->SendJobsQuery(Jobs::JOB_DESCRIBE_TOPIC, "$next"); + } + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + } + } + + // Wait for job processing to complete + while (!done_) { + done_ = true; + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + + rc = p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Disconnect failed. %s", ResponseHelper::ToString(rc).c_str()); + } + + std::cout << "Exiting Sample!!!!" << std::endl; + return ResponseCode::SUCCESS; + } + } +} + +int main(int argc, char **argv) { + std::shared_ptr p_log_system = + std::make_shared(awsiotsdk::util::Logging::LogLevel::Info); + awsiotsdk::util::Logging::InitializeAWSLogging(p_log_system); + + std::unique_ptr + jobs_sample = std::unique_ptr(new awsiotsdk::samples::JobsSample()); + + awsiotsdk::ResponseCode rc = awsiotsdk::ConfigCommon::InitializeCommon("config/SampleConfig.json"); + if (awsiotsdk::ResponseCode::SUCCESS == rc) { + rc = jobs_sample->RunSample(); + } +#ifdef WIN32 + std::cout<<"Press any key to continue!!!!"<(rc); +} diff --git a/samples/Jobs/JobsSample.hpp b/samples/Jobs/JobsSample.hpp new file mode 100644 index 0000000..f651d20 --- /dev/null +++ b/samples/Jobs/JobsSample.hpp @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsSample.hpp + * @brief + * + */ + + +#pragma once + +#include "mqtt/Client.hpp" +#include "NetworkConnection.hpp" + +namespace awsiotsdk { + namespace samples { + class JobsSample { + protected: + std::shared_ptr p_network_connection_; + std::shared_ptr p_iot_client_; + std::shared_ptr p_jobs_; + std::atomic done_; + + ResponseCode DisconnectCallback(util::String topic_name, + std::shared_ptr p_app_handler_data); + ResponseCode ReconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode reconnect_result); + ResponseCode ResubscribeCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode resubscribe_result); + + ResponseCode GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode UpdateAcceptedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode UpdateRejectedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + + ResponseCode Subscribe(); + ResponseCode InitializeTLS(); + + public: + ResponseCode RunSample(); + }; + } +} + + diff --git a/samples/README.md b/samples/README.md index 1b97d6b..518853a 100644 --- a/samples/README.md +++ b/samples/README.md @@ -18,6 +18,12 @@ This sample demonstrates how various Shadow operations can be performed. * Code for this sample is located [here](./ShadowDelta) * Target for this sample is `shadow-delta-sample` +### Jobs Sample +This sample demonstrates how various Jobs API operations can be performed including subscribing to Jobs notifications and publishing Job execution updates. + + * Code for this sample is located [here](./Jobs) + * Target for this sample is `jobs-sample` + ### Discovery Sample This sample demonstrates how the discovery operation can be performed to get the connectivity information to connect to a Greengrass Core (GGC). The configuration for this example is slightly different as the Discovery operation is a HTTP call, and uses port 8443, instead of port 8883 which is used for MQTT operations. The endpoint is the same IoT host endpoint used to connect the IoT thing to the cloud. diff --git a/src/ResponseCode.cpp b/src/ResponseCode.cpp index 2f3e21b..fad8420 100644 --- a/src/ResponseCode.cpp +++ b/src/ResponseCode.cpp @@ -343,6 +343,9 @@ namespace awsiotsdk { case ResponseCode::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR: os << awsiotsdk::ResponseHelper::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING; break; + case ResponseCode::JOBS_INVALID_TOPIC_ERROR: + os << awsiotsdk::ResponseHelper::JOBS_INVALID_TOPIC_ERROR_STRING; + break; } os << " : SDK Code " << static_cast(rc) << "."; return os; diff --git a/src/jobs/Jobs.cpp b/src/jobs/Jobs.cpp new file mode 100644 index 0000000..e775473 --- /dev/null +++ b/src/jobs/Jobs.cpp @@ -0,0 +1,340 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.cpp + * @brief + * + */ + +#include "util/logging/LogMacros.hpp" + +#include "jobs/Jobs.hpp" + +#define BASE_THINGS_TOPIC "$aws/things/" + +#define NOTIFY_OPERATION "notify" +#define NOTIFY_NEXT_OPERATION "notify-next" +#define GET_OPERATION "get" +#define START_NEXT_OPERATION "start-next" +#define WILDCARD_OPERATION "+" +#define UPDATE_OPERATION "update" +#define ACCEPTED_REPLY "accepted" +#define REJECTED_REPLY "rejected" +#define WILDCARD_REPLY "#" + +namespace awsiotsdk { + std::unique_ptr Jobs::Create(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token) { + if (nullptr == p_mqtt_client) { + return nullptr; + } + + return std::unique_ptr(new Jobs(p_mqtt_client, qos, thing_name, client_token)); + } + + Jobs::Jobs(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token) { + p_mqtt_client_ = p_mqtt_client; + qos_ = qos; + thing_name_ = thing_name; + client_token_ = client_token; + }; + + bool Jobs::BaseTopicRequiresJobId(JobExecutionTopicType topicType) { + switch (topicType) { + case JOB_UPDATE_TOPIC: + case JOB_DESCRIBE_TOPIC: + return true; + case JOB_NOTIFY_TOPIC: + case JOB_NOTIFY_NEXT_TOPIC: + case JOB_START_NEXT_TOPIC: + case JOB_GET_PENDING_TOPIC: + case JOB_WILDCARD_TOPIC: + case JOB_UNRECOGNIZED_TOPIC: + default: + return false; + } + }; + + const util::String Jobs::GetOperationForBaseTopic(JobExecutionTopicType topicType) { + switch (topicType) { + case JOB_UPDATE_TOPIC: + return UPDATE_OPERATION; + case JOB_NOTIFY_TOPIC: + return NOTIFY_OPERATION; + case JOB_NOTIFY_NEXT_TOPIC: + return NOTIFY_NEXT_OPERATION; + case JOB_GET_PENDING_TOPIC: + case JOB_DESCRIBE_TOPIC: + return GET_OPERATION; + case JOB_START_NEXT_TOPIC: + return START_NEXT_OPERATION; + case JOB_WILDCARD_TOPIC: + return WILDCARD_OPERATION; + case JOB_UNRECOGNIZED_TOPIC: + default: + return ""; + } + }; + + const util::String Jobs::GetSuffixForTopicType(JobExecutionTopicReplyType replyType) { + switch (replyType) { + case JOB_REQUEST_TYPE: + return ""; + case JOB_ACCEPTED_REPLY_TYPE: + return "/" ACCEPTED_REPLY; + case JOB_REJECTED_REPLY_TYPE: + return "/" REJECTED_REPLY; + case JOB_WILDCARD_REPLY_TYPE: + return "/" WILDCARD_REPLY; + case JOB_UNRECOGNIZED_TOPIC_TYPE: + default: + return ""; + } + } + + const util::String Jobs::GetExecutionStatus(JobExecutionStatus status) { + switch (status) { + case JOB_EXECUTION_QUEUED: + return "QUEUED"; + case JOB_EXECUTION_IN_PROGRESS: + return "IN_PROGRESS"; + case JOB_EXECUTION_FAILED: + return "FAILED"; + case JOB_EXECUTION_SUCCEEDED: + return "SUCCEEDED"; + case JOB_EXECUTION_CANCELED: + return "CANCELED"; + case JOB_EXECUTION_REJECTED: + return "REJECTED"; + case JOB_EXECUTION_STATUS_NOT_SET: + case JOB_EXECUTION_UNKNOWN_STATUS: + default: + return ""; + } + } + + util::String Jobs::Escape(const util::String &value) { + util::String result = ""; + + for (int i = 0; i < value.length(); i++) { + switch(value[i]) { + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + case '"': result += "\\\""; break; + case '\\': result += "\\\\"; break; + default: result += value[i]; + } + } + return result; + } + + util::String Jobs::SerializeStatusDetails(const util::Map &statusDetailsMap) { + util::String result = "{"; + + util::Map::const_iterator itr = statusDetailsMap.begin(); + while (itr != statusDetailsMap.end()) { + result += (itr == statusDetailsMap.begin() ? "\"" : ",\""); + result += Escape(itr->first) + "\":\"" + Escape(itr->second) + "\""; + itr++; + } + + result += '}'; + return result; + } + + std::unique_ptr Jobs::GetJobTopic(JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType, + const util::String &jobId) { + if (thing_name_.empty()) { + return nullptr; + } + + if ((topicType == JOB_NOTIFY_TOPIC || topicType == JOB_NOTIFY_NEXT_TOPIC) && replyType != JOB_REQUEST_TYPE) { + return nullptr; + } + + if ((topicType == JOB_GET_PENDING_TOPIC || topicType == JOB_START_NEXT_TOPIC || + topicType == JOB_NOTIFY_TOPIC || topicType == JOB_NOTIFY_NEXT_TOPIC) && !jobId.empty()) { + return nullptr; + } + + const bool requireJobId = BaseTopicRequiresJobId(topicType); + if (jobId.empty() && requireJobId) { + return nullptr; + } + + const util::String operation = GetOperationForBaseTopic(topicType); + if (operation.empty()) { + return nullptr; + } + + const util::String suffix = GetSuffixForTopicType(replyType); + + if (requireJobId) { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/" + jobId + '/' + operation + suffix); + } else if (topicType == JOB_WILDCARD_TOPIC) { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/#"); + } else { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/" + operation + suffix); + } + }; + + util::String Jobs::SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap, + int64_t expectedVersion, // set to 0 to ignore + int64_t executionNumber, // set to 0 to ignore + bool includeJobExecutionState, + bool includeJobDocument) { + const util::String executionStatus = GetExecutionStatus(status); + + if (executionStatus.empty()) { + return ""; + } + + util::String result = "{\"status\":\"" + executionStatus + "\""; + if (!statusDetailsMap.empty()) { + result += ",\"statusDetails\":" + SerializeStatusDetails(statusDetailsMap); + } + if (expectedVersion > 0) { + result += ",\"expectedVersion\":\"" + std::to_string(expectedVersion) + "\""; + } + if (executionNumber > 0) { + result += ",\"executionNumber\":\"" + std::to_string(executionNumber) + "\""; + } + if (includeJobExecutionState) { + result += ",\"includeJobExecutionState\":\"true\""; + } + if (includeJobDocument) { + result += ",\"includeJobDocument\":\"true\""; + } + if (!client_token_.empty()) { + result += ",\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeDescribeJobExecutionPayload(int64_t executionNumber, // set to 0 to ignore + bool includeJobDocument) { + util::String result = "{\"includeJobDocument\":\""; + result += (includeJobDocument ? "true" : "false"); + result += "\""; + if (executionNumber > 0) { + result += ",\"executionNumber\":\"" + std::to_string(executionNumber) + "\""; + } + if (!client_token_.empty()) { + result += "\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap) { + util::String result = "{"; + if (!statusDetailsMap.empty()) { + result += "\"statusDetails\":" + SerializeStatusDetails(statusDetailsMap); + } + if (!client_token_.empty()) { + if (!statusDetailsMap.empty()) { + result += ','; + } + result += "\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeClientTokenPayload() { + if (!client_token_.empty()) { + return "{\"clientToken\":\"" + client_token_ + "\"}"; + } + + return "{}"; + }; + + ResponseCode Jobs::SendJobsQuery(JobExecutionTopicType topicType, + const util::String &jobId) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(topicType, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeClientTokenPayload(), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsStartNext(const util::Map &statusDetailsMap) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_START_NEXT_TOPIC, JOB_REQUEST_TYPE); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeStartNextPendingJobExecutionPayload(statusDetailsMap), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsDescribe(const util::String &jobId, + int64_t executionNumber, // set to 0 to ignore + bool includeJobDocument) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_DESCRIBE_TOPIC, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeDescribeJobExecutionPayload(executionNumber, includeJobDocument), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsUpdate(const util::String &jobId, + JobExecutionStatus status, + const util::Map &statusDetailsMap, + int64_t expectedVersion, // set to 0 to ignore + int64_t executionNumber, // set to 0 to ignore + bool includeJobExecutionState, + bool includeJobDocument) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_UPDATE_TOPIC, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, + SerializeJobExecutionUpdatePayload(status, statusDetailsMap, expectedVersion, executionNumber, + includeJobExecutionState, includeJobDocument), + nullptr, packet_id); + }; + + std::shared_ptr Jobs::CreateJobsSubscription(mqtt::Subscription::ApplicationCallbackHandlerPtr p_app_handler, + std::shared_ptr p_app_handler_data, + JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType, + const util::String &jobId) { + return mqtt::Subscription::Create(GetJobTopic(topicType, replyType, jobId), qos_, p_app_handler, p_app_handler_data); + }; +} diff --git a/tests/integration/include/JobsTest.hpp b/tests/integration/include/JobsTest.hpp new file mode 100644 index 0000000..011034d --- /dev/null +++ b/tests/integration/include/JobsTest.hpp @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.hpp + * @brief + * + */ + + +#pragma once + +#include "mqtt/Client.hpp" +#include "NetworkConnection.hpp" +#include "jobs/Jobs.hpp" + +namespace awsiotsdk { + namespace tests { + namespace integration { + class JobsTest { + protected: + static const std::chrono::seconds keep_alive_timeout_; + + std::shared_ptr p_network_connection_; + std::shared_ptr p_iot_client_; + std::shared_ptr p_jobs_; + std::atomic done_; + + ResponseCode GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + + ResponseCode Subscribe(); + ResponseCode Unsubscribe(); + ResponseCode InitializeTLS(); + + public: + ResponseCode RunTest(); + }; + } + } +} + + diff --git a/tests/integration/src/IntegTestRunner.cpp b/tests/integration/src/IntegTestRunner.cpp index fc6c160..73eef3b 100644 --- a/tests/integration/src/IntegTestRunner.cpp +++ b/tests/integration/src/IntegTestRunner.cpp @@ -28,6 +28,7 @@ #include "ConfigCommon.hpp" #include "IntegTestRunner.hpp" #include "SdkTestConfig.hpp" +#include "JobsTest.hpp" #include "PubSub.hpp" #include "AutoReconnect.hpp" #include "MultipleClients.hpp" @@ -53,6 +54,17 @@ namespace awsiotsdk { ResponseCode IntegTestRunner::RunAllTests() { ResponseCode rc = ResponseCode::SUCCESS; // Each test runs in its own scope to ensure complete cleanup + /** + * Run Jobs Tests + */ + { + JobsTest jobs_test_runner; + rc = jobs_test_runner.RunTest(); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + } + /** * Run Subscribe Publish Tests */ diff --git a/tests/integration/src/JobsTest.cpp b/tests/integration/src/JobsTest.cpp new file mode 100644 index 0000000..3e913df --- /dev/null +++ b/tests/integration/src/JobsTest.cpp @@ -0,0 +1,327 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsTest.cpp + * @brief + * + */ + +#include "JobsTest.hpp" +#include "util/logging/LogMacros.hpp" + +#include +#include + +#ifdef USE_WEBSOCKETS +#include "WebSocketConnection.hpp" +#elif defined USE_MBEDTLS +#include "MbedTLSConnection.hpp" +#else +#include "OpenSSLConnection.hpp" +#endif + +#include "ConfigCommon.hpp" + +#define JOBS_INTEGRATION_TEST_TAG "[Integration Test - Jobs]" + +namespace awsiotsdk { + namespace tests { + namespace integration { + ResponseCode JobsTest::GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "GetPendingCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Json Parse for GetPendingCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("inProgressJobs")) { + std::cout << "inProgressJobs : " << util::JsonParser::ToString(doc["inProgressJobs"]) << std::endl; + } + + if (doc.HasMember("queuedJobs")) { + std::cout << "queuedJobs : " << util::JsonParser::ToString(doc["queuedJobs"]) << std::endl; + } + + std::cout << "************" << std::endl; + + rc = p_jobs_->SendJobsQuery(Jobs::JOB_DESCRIBE_TOPIC, "$next"); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + + return ResponseCode::FAILURE; + } + + return ResponseCode::SUCCESS; + } + + ResponseCode JobsTest::NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "NextJobCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Json Parse for NextJobCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("execution")) { + std::cout << "execution : " << util::JsonParser::ToString(doc["execution"]) << std::endl; + + if (doc["execution"].HasMember("jobId")) { + util::Map statusDetailsMap; + + util::String jobId = doc["execution"]["jobId"].GetString(); + std::cout << "jobId : " << jobId << std::endl; + + if (doc["execution"].HasMember("jobDocument")) { + std::cout << "jobDocument : " << util::JsonParser::ToString(doc["execution"]["jobDocument"]) << std::endl; + statusDetailsMap.insert(std::make_pair("exampleDetail", "a value appropriate for your successful job")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_SUCCEEDED, statusDetailsMap); + } else { + statusDetailsMap.insert(std::make_pair("failureDetail", "Unable to process job document")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_FAILED, statusDetailsMap); + } + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsUpdate failed. %s", ResponseHelper::ToString(rc).c_str()); + return rc; + } + } + } else { + std::cout << "No job execution description found, nothing to do." << std::endl; + done_ = true; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsTest::Subscribe() { + std::cout << "******** Subscribe ***************" << std::endl; + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_pending_handler = + std::bind(&JobsTest::GetPendingCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_next_handler = + std::bind(&JobsTest::NextJobCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + util::Vector> topic_vector; + std::shared_ptr p_subscription; + + p_subscription = p_jobs_->CreateJobsSubscription(p_pending_handler, nullptr, Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(p_subscription); + + ResponseCode rc = p_iot_client_->Subscribe(topic_vector, ConfigCommon::mqtt_command_timeout_); + std::this_thread::sleep_for(std::chrono::seconds(3)); + return rc; + } + + ResponseCode JobsTest::Unsubscribe() { + uint16_t packet_id = 0; + std::unique_ptr p_topic_name; + util::Vector> topic_vector; + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(std::move(p_topic_name)); + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(std::move(p_topic_name)); + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(std::move(p_topic_name)); + + ResponseCode rc = p_iot_client_->UnsubscribeAsync(std::move(topic_vector), nullptr, packet_id); + std::this_thread::sleep_for(std::chrono::seconds(1)); + return rc; + } + + ResponseCode JobsTest::InitializeTLS() { + ResponseCode rc = ResponseCode::SUCCESS; + +#ifdef USE_WEBSOCKETS + p_network_connection_ = std::shared_ptr( + new network::WebSocketConnection(ConfigCommon::endpoint_, ConfigCommon::endpoint_https_port_, + ConfigCommon::root_ca_path_, ConfigCommon::aws_region_, + ConfigCommon::aws_access_key_id_, + ConfigCommon::aws_secret_access_key_, + ConfigCommon::aws_session_token_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true)); +#elif defined USE_MBEDTLS + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + + if (ResponseCode::SUCCESS != rc) { + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#else + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + rc = p_network_connection->Initialize(); + + if (ResponseCode::SUCCESS != rc) { + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#endif + return rc; + } + + ResponseCode JobsTest::RunTest() { + done_ = false; + ResponseCode rc = InitializeTLS(); + + do { + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Failed to initialize TLS layer. %s", + ResponseHelper::ToString(rc).c_str()); + break; + } + + p_iot_client_ = std::shared_ptr( + MqttClient::Create(p_network_connection_, ConfigCommon::mqtt_command_timeout_)); + if (nullptr == p_iot_client_) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Failed to create MQTT Client Instance!!"); + rc = ResponseCode::FAILURE; + break; + } + + util::String client_id_tagged = ConfigCommon::base_client_id_; + client_id_tagged.append("_jobs_tester_"); + client_id_tagged.append(std::to_string(rand())); + std::unique_ptr client_id = Utf8String::Create(client_id_tagged); + + rc = p_iot_client_->Connect(ConfigCommon::mqtt_command_timeout_, ConfigCommon::is_clean_session_, + mqtt::Version::MQTT_3_1_1, ConfigCommon::keep_alive_timeout_secs_, + std::move(client_id), nullptr, nullptr, nullptr); + + p_jobs_ = Jobs::Create(p_iot_client_, mqtt::QoS::QOS1, ConfigCommon::thing_name_, client_id_tagged); + + if (ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "MQTT Connect failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + rc = Subscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Subscribe failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + break; + } + + rc = p_jobs_->SendJobsQuery(Jobs::JOB_GET_PENDING_TOPIC); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + } + + int retries = 5; + while (!done_ && retries-- > 0) { + std::this_thread::sleep_for(std::chrono::seconds(5)); + } + + if (!done_) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Not all jobs processed."); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + rc = ResponseCode::FAILURE; + break; + } + + rc = Unsubscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Unsubscribe failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + break; + } + + rc = p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Disconnect failed. %s", + ResponseHelper::ToString(rc).c_str()); + break; + } + } while (false); + + std::cout << std::endl; + if (ResponseCode::SUCCESS != rc) { + std::cout + << "Test Failed!!!! See above output for details!!" + << std::endl; + std::cout << "**********************************************************" << std::endl; + return ResponseCode::FAILURE; + } + + std::cout << "Test Successful!!!!" << std::endl; + std::cout << "**********************************************************" << std::endl; + return ResponseCode::SUCCESS; + } + } + } +} diff --git a/tests/unit/src/ResponseCodeTests.cpp b/tests/unit/src/ResponseCodeTests.cpp index 0d6f5cc..ada914e 100644 --- a/tests/unit/src/ResponseCodeTests.cpp +++ b/tests/unit/src/ResponseCodeTests.cpp @@ -570,6 +570,11 @@ namespace awsiotsdk { expected_string = ResponseCodeToString(ResponseHelper::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING, ResponseCode::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR); EXPECT_EQ(expected_string, response_string); + + response_string = ResponseHelper::ToString(ResponseCode::JOBS_INVALID_TOPIC_ERROR); + expected_string = ResponseCodeToString(ResponseHelper::JOBS_INVALID_TOPIC_ERROR_STRING, + ResponseCode::JOBS_INVALID_TOPIC_ERROR); + EXPECT_EQ(expected_string, response_string); } } } diff --git a/tests/unit/src/jobs/JobsTests.cpp b/tests/unit/src/jobs/JobsTests.cpp new file mode 100644 index 0000000..56b5161 --- /dev/null +++ b/tests/unit/src/jobs/JobsTests.cpp @@ -0,0 +1,264 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsTests.cpp + * @brief + * + */ + +#include + +#include + +#include "util/logging/LogMacros.hpp" + +#include "TestHelper.hpp" +#include "MockNetworkConnection.hpp" + +#include "jobs/Jobs.hpp" +#include "mqtt/ClientState.hpp" + +#define JOBS_TEST_LOG_TAG "[Jobs Unit Test]" + +namespace awsiotsdk { + namespace tests { + namespace unit { + class JobsTestWrapper : public Jobs { + protected: + static const util::String test_thing_name_; + static const util::String client_token_; + + public: + JobsTestWrapper(bool empty_thing_name, bool empty_client_token): + Jobs(nullptr, mqtt::QoS::QOS0, + empty_thing_name ? "" : test_thing_name_, + empty_client_token ? "" : client_token_) {} + + util::String SerializeStatusDetails(const util::Map &statusDetailsMap) { + return Jobs::SerializeStatusDetails(statusDetailsMap); + } + + util::String SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, + int64_t executionNumber = 0, + bool includeJobExecutionState = false, + bool includeJobDocument = false) { + return Jobs::SerializeJobExecutionUpdatePayload(status, statusDetailsMap, expectedVersion, executionNumber, includeJobExecutionState, includeJobDocument); + } + + util::String SerializeDescribeJobExecutionPayload(int64_t executionNumber = 0, + bool includeJobDocument = true) { + return Jobs::SerializeDescribeJobExecutionPayload(executionNumber, includeJobDocument); + } + + util::String SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap = util::Map()) { + return Jobs::SerializeStartNextPendingJobExecutionPayload(statusDetailsMap); + } + + util::String SerializeClientTokenPayload() { + return Jobs::SerializeClientTokenPayload(); + } + + util::String Escape(const util::String &value) { + return Jobs::Escape(value); + } + }; + + const util::String JobsTestWrapper::test_thing_name_ = "CppSdkTestClient"; + const util::String JobsTestWrapper::client_token_ = "CppSdkTestClientToken"; + + class JobsTester : public ::testing::Test { + protected: + static const util::String job_id_; + + std::shared_ptr p_jobs_; + std::shared_ptr p_jobs_empty_client_token_; + std::shared_ptr p_jobs_empty_thing_name_; + + JobsTester() { + p_jobs_ = std::shared_ptr(new JobsTestWrapper(false, false)); + p_jobs_empty_client_token_ = std::shared_ptr(new JobsTestWrapper(false, true)); + p_jobs_empty_thing_name_ = std::shared_ptr(new JobsTestWrapper(true, false)); + } + }; + + const util::String JobsTester::job_id_ = "TestJobId"; + + TEST_F(JobsTester, ValidTopicsTests) { + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/accepted", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/rejected", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/#", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/accepted", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/rejected", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/#", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/accepted", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/rejected", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/#", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/accepted", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/rejected", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/#", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/notify", p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/notify-next", p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + } + + TEST_F(JobsTester, InvalidTopicsTests) { + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + } + + + TEST_F(JobsTester, PayloadSerializationTests) { + util::Map statusDetailsMap; + statusDetailsMap.insert(std::make_pair("testKey", "testVal")); + + EXPECT_EQ("{}", p_jobs_empty_client_token_->SerializeClientTokenPayload()); + EXPECT_EQ("{\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeClientTokenPayload()); + + EXPECT_EQ("{}", p_jobs_empty_client_token_->SerializeStartNextPendingJobExecutionPayload()); + EXPECT_EQ("{\"statusDetails\":{\"testKey\":\"testVal\"}}", p_jobs_empty_client_token_->SerializeStartNextPendingJobExecutionPayload(statusDetailsMap)); + EXPECT_EQ("{\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeStartNextPendingJobExecutionPayload()); + EXPECT_EQ("{\"statusDetails\":{\"testKey\":\"testVal\"},\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeStartNextPendingJobExecutionPayload(statusDetailsMap)); + + EXPECT_EQ("{\"includeJobDocument\":\"true\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload()); + EXPECT_EQ("{\"includeJobDocument\":\"true\",\"executionNumber\":\"1\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload(1)); + EXPECT_EQ("{\"includeJobDocument\":\"false\",\"executionNumber\":\"1\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload(1, false)); + + EXPECT_EQ("{\"includeJobDocument\":\"true\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload()); + EXPECT_EQ("{\"includeJobDocument\":\"true\",\"executionNumber\":\"1\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload(1)); + EXPECT_EQ("{\"includeJobDocument\":\"false\",\"executionNumber\":\"1\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload(1, false)); + + EXPECT_EQ("", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_STATUS_NOT_SET)); + EXPECT_EQ("", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_UNKNOWN_STATUS)); + EXPECT_EQ("", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_STATUS_NOT_SET)); + EXPECT_EQ("", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_UNKNOWN_STATUS)); + + EXPECT_EQ("{\"status\":\"QUEUED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"}}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"includeJobDocument\":\"true\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true, true)); + + EXPECT_EQ("{\"status\":\"IN_PROGRESS\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_IN_PROGRESS)); + EXPECT_EQ("{\"status\":\"FAILED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_FAILED)); + EXPECT_EQ("{\"status\":\"SUCCEEDED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_SUCCEEDED)); + EXPECT_EQ("{\"status\":\"CANCELED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_CANCELED)); + EXPECT_EQ("{\"status\":\"REJECTED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_REJECTED)); + + EXPECT_EQ("{\"status\":\"QUEUED\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"includeJobDocument\":\"true\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true, true)); + + EXPECT_EQ("{\"status\":\"IN_PROGRESS\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_IN_PROGRESS)); + EXPECT_EQ("{\"status\":\"FAILED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_FAILED)); + EXPECT_EQ("{\"status\":\"SUCCEEDED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_SUCCEEDED)); + EXPECT_EQ("{\"status\":\"CANCELED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_CANCELED)); + EXPECT_EQ("{\"status\":\"REJECTED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_REJECTED)); + + statusDetailsMap.insert(std::make_pair("testEscapeKey \" \t \r \n \\ '!", "testEscapeVal \" \t \r \n \\ '!")); + EXPECT_EQ("{\"testEscapeKey \\\" \\t \\r \\n \\\\ '!\":\"testEscapeVal \\\" \\t \\r \\n \\\\ '!\",\"testKey\":\"testVal\"}", p_jobs_->SerializeStatusDetails(statusDetailsMap)); + } + } + } +} From d78045599e1d204e5ebadf4f0b1d8aaae97cefeb Mon Sep 17 00:00:00 2001 From: "Reddy, Varun" Date: Fri, 9 Mar 2018 13:36:32 -0800 Subject: [PATCH 2/6] Wrap SIGPIPE in ifndef block for Windows Allow Position Independent Code for Static Library (#73) Build the static library with the fPIC compiler flag, so that it can be linked into a shared library. Fix wildcard regex for special topics with $ symbols Adding standard files (#83) Pull requests to fix warnings on Windows Includes #75, #76 and #77. Also includes other changes related to - loading and storing of atomic variables - fix for UTF-8 character representation on Windows Update sample documentation with shadow client token limitation Make Shadow::HandleGetResponse call response handler on Rejected response Fixes #86 Make Shadow::HandleGetResponse call response handler on malformed payload --- .github/PULL_REQUEST_TEMPLATE.md | 6 +++ CMakeLists.txt | 1 + CODE_OF_CONDUCT.md | 4 ++ CONTRIBUTING.md | 61 ++++++++++++++++++++++++++ include/mqtt/Packet.hpp | 8 ++-- include/mqtt/Publish.hpp | 5 ++- network/OpenSSL/OpenSSLConnection.cpp | 6 ++- samples/README.md | 4 +- src/mqtt/Common.cpp | 5 ++- src/mqtt/Publish.cpp | 4 +- src/shadow/Shadow.cpp | 10 ++--- tests/unit/src/mqtt/SubscribeTests.cpp | 12 +++-- 12 files changed, 104 insertions(+), 22 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ab40d21 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,6 @@ +*Issue #, if available:* + +*Description of changes:* + + +By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e6854c..b09e572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ if (BUILD_SHARED_LIBRARY) set_target_properties(${SDK_TARGET_NAME} PROPERTIES SUFFIX ".so") else() add_library(${SDK_TARGET_NAME} "") + set_target_properties(${SDK_TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() # Download and include rapidjson, not optional diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3b64466 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fbe001e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional +documentation, we greatly value feedback and contributions from our community. + +Please read through this document before submitting any issues or pull requests to ensure we have all the necessary +information to effectively respond to your bug report or contribution. + + +## Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features. + +When filing an issue, please check [existing open](https://github.com/aws/aws-iot-device-sdk-cpp/issues), or [recently closed](https://github.com/aws/aws-iot-device-sdk-cpp/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already +reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of our code being used +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + + +## Contributing via Pull Requests +Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: + +1. You are working against the latest source on the *master* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - we would hate for your time to be wasted. + +To send us a pull request, please: + +1. Fork the repository. +2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. +3. Ensure local tests pass. +4. Commit to your fork using clear commit messages. +5. Send us a pull request, answering any default questions in the pull request interface. +6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + + +## Finding contributions to work on +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws/aws-iot-device-sdk-cpp/labels/help%20wanted) issues is a great place to start. + + +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. + + +## Security issue notifications +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. + + +## Licensing + +See the [LICENSE](https://github.com/aws/aws-iot-device-sdk-cpp/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. + +We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/include/mqtt/Packet.hpp b/include/mqtt/Packet.hpp index 767699f..c09058c 100644 --- a/include/mqtt/Packet.hpp +++ b/include/mqtt/Packet.hpp @@ -118,12 +118,12 @@ namespace awsiotsdk { std::atomic_uint_fast16_t packet_id_; ///< Message sequence identifier. Handled automatically by the MQTT client public: - uint16_t GetActionId() { return packet_id_; } - void SetActionId(uint16_t action_id) { packet_id_ = action_id; } + uint16_t GetActionId() { return (uint16_t) packet_id_.load(std::memory_order_relaxed); } + void SetActionId(uint16_t action_id) { packet_id_.store(action_id, std::memory_order_relaxed); } bool isPacketDataValid(); - uint16_t GetPacketId() { return packet_id_; } - void SetPacketId(uint16_t packet_id) { packet_id_ = packet_id; } + uint16_t GetPacketId() { return (uint16_t) packet_id_.load(std::memory_order_relaxed); } + void SetPacketId(uint16_t packet_id) { packet_id_.store(packet_id, std::memory_order_relaxed); } size_t Size() { return serialized_packet_length_; } diff --git a/include/mqtt/Publish.hpp b/include/mqtt/Publish.hpp index 202550f..c51058c 100644 --- a/include/mqtt/Publish.hpp +++ b/include/mqtt/Publish.hpp @@ -201,8 +201,9 @@ namespace awsiotsdk { */ util::String ToString(); - uint16_t GetPublishPacketId() { return publish_packet_id_; } - void SetPublishPacketId(uint16_t publish_packet_id) { publish_packet_id_ = publish_packet_id; } + uint16_t GetPublishPacketId() { return (uint16_t) publish_packet_id_.load(std::memory_order_relaxed); } + void SetPublishPacketId(uint16_t publish_packet_id) { publish_packet_id_.store(publish_packet_id, + std::memory_order_relaxed); } }; /** diff --git a/network/OpenSSL/OpenSSLConnection.cpp b/network/OpenSSL/OpenSSLConnection.cpp index 3bee704..05f11f6 100644 --- a/network/OpenSSL/OpenSSLConnection.cpp +++ b/network/OpenSSL/OpenSSLConnection.cpp @@ -130,7 +130,7 @@ namespace awsiotsdk { WSADATA wsa_data; int result; bool was_wsa_initialized = true; - int s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + UINT_PTR s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(INVALID_SOCKET == s) { if(WSANOTINITIALISED == WSAGetLastError()) { was_wsa_initialized = false; @@ -154,7 +154,9 @@ namespace awsiotsdk { ERR_load_BIO_strings(); ERR_load_crypto_strings(); SSL_load_error_strings(); +#ifndef WIN32 signal(SIGPIPE, SIG_IGN); +#endif is_lib_initialized = true; } const SSL_METHOD *method; @@ -336,7 +338,7 @@ namespace awsiotsdk { // Configure a non-zero callback if desired SSL_set_verify(p_ssl_handle_, SSL_VERIFY_PEER, nullptr); - server_tcp_socket_fd_ = socket(AF_INET, SOCK_STREAM, 0); + server_tcp_socket_fd_ = (int)socket(AF_INET, SOCK_STREAM, 0); if (-1 == server_tcp_socket_fd_) { return ResponseCode::NETWORK_TCP_SETUP_ERROR; } diff --git a/samples/README.md b/samples/README.md index 518853a..00fdfb5 100644 --- a/samples/README.md +++ b/samples/README.md @@ -17,7 +17,9 @@ This sample demonstrates how various Shadow operations can be performed. * Code for this sample is located [here](./ShadowDelta) * Target for this sample is `shadow-delta-sample` - + +Note: The shadow client token is set as the thing name by default in the sample. The shadow client token is limited to 64 bytes and will return an error if a token longer than 64 bytes is used (`"code":400,"message":"invalid client token"`, although receiving a 400 does not necessarily mean that it is due to the length of the client token). Modify the code [here](../ShadowDelta/ShadowDelta.cpp#L184) if your thing name is longer than 64 bytes to prevent this error. + ### Jobs Sample This sample demonstrates how various Jobs API operations can be performed including subscribing to Jobs notifications and publishing Job execution updates. diff --git a/src/mqtt/Common.cpp b/src/mqtt/Common.cpp index 9356517..4d4c947 100644 --- a/src/mqtt/Common.cpp +++ b/src/mqtt/Common.cpp @@ -26,7 +26,7 @@ #define MULTI_LEVEL_WILDCARD '#' #define RESERVED_TOPIC '$' #define SINGLE_LEVEL_REGEX_STRING "[^/]*" // Single level regex to allow all UTF-8 character except '\' -#define MULTI_LEVEL_REGEX_STRING "[^\uc1bf]*" // Placeholder for the multilevel regex to allow all UTF-8 character +#define MULTI_LEVEL_REGEX_STRING u8"[^\uc1bf]*" // Placeholder for the multilevel regex to allow all UTF-8 character namespace awsiotsdk { namespace mqtt { @@ -178,6 +178,9 @@ namespace awsiotsdk { p_topic_regex_.append(SINGLE_LEVEL_REGEX_STRING); } else if (it == MULTI_LEVEL_WILDCARD) { p_topic_regex_.append(MULTI_LEVEL_REGEX_STRING); + } else if (it == RESERVED_TOPIC) { + p_topic_regex_ += "\\"; + p_topic_regex_ += RESERVED_TOPIC; } else { p_topic_regex_ += it; } diff --git a/src/mqtt/Publish.cpp b/src/mqtt/Publish.cpp index 1835d6f..3baab37 100644 --- a/src/mqtt/Publish.cpp +++ b/src/mqtt/Publish.cpp @@ -143,7 +143,7 @@ namespace awsiotsdk { ******************************************/ PubackPacket::PubackPacket(uint16_t publish_packet_id) { packet_size_ = 2; // Packet ID requires 2 bytes in case of QoS1 and QoS2 - publish_packet_id_ = publish_packet_id; + publish_packet_id_.store(publish_packet_id, std::memory_order_relaxed); fixed_header_.Initialize(MessageTypes::PUBACK, false, QoS::QOS0, false, packet_size_); serialized_packet_length_ = packet_size_ + fixed_header_.Length(); } @@ -156,7 +156,7 @@ namespace awsiotsdk { util::String buf; buf.reserve(serialized_packet_length_); fixed_header_.AppendToBuffer(buf); - AppendUInt16ToBuffer(buf, publish_packet_id_); + AppendUInt16ToBuffer(buf, publish_packet_id_.load(std::memory_order_relaxed)); return buf; } diff --git a/src/shadow/Shadow.cpp b/src/shadow/Shadow.cpp index f16036c..9c0cd5d 100644 --- a/src/shadow/Shadow.cpp +++ b/src/shadow/Shadow.cpp @@ -172,16 +172,14 @@ namespace awsiotsdk { return ResponseCode::SHADOW_UNEXPECTED_RESPONSE_TYPE; } - // Validate payload - if (!payload.IsObject() - || !payload.HasMember(SHADOW_DOCUMENT_STATE_KEY)) { - return ResponseCode::SHADOW_UNEXPECTED_RESPONSE_PAYLOAD; - } - ResponseCode rc = ResponseCode::SHADOW_REQUEST_ACCEPTED; if (ShadowResponseType::Rejected == response_type) { AWS_LOG_WARN(SHADOW_LOG_TAG, "Get request rejected for shadow : %s", thing_name_.c_str()); rc = ResponseCode::SHADOW_REQUEST_REJECTED; + } else if (!payload.IsObject() + || !payload.HasMember(SHADOW_DOCUMENT_STATE_KEY)) { + // Invalid payload + rc = ResponseCode::SHADOW_UNEXPECTED_RESPONSE_PAYLOAD; } else { AWS_LOG_DEBUG(SHADOW_LOG_TAG, "Get request accepted for shadow : %s", thing_name_.c_str()); cur_server_state_document_.RemoveAllMembers(); diff --git a/tests/unit/src/mqtt/SubscribeTests.cpp b/tests/unit/src/mqtt/SubscribeTests.cpp index 4e33a49..fd5d618 100644 --- a/tests/unit/src/mqtt/SubscribeTests.cpp +++ b/tests/unit/src/mqtt/SubscribeTests.cpp @@ -30,7 +30,7 @@ #define K 1024 #define LARGE_PAYLOAD_SIZE 127 * K -#define VALID_WILDCARD_TOPICS 6 +#define VALID_WILDCARD_TOPICS 8 #define INVALID_WILDCARD_TOPICS 4 #define WILDCARD_TEST_TOPICS 10 #define UNMATCHED_WILDCARD_TEST_TOPICS 2 @@ -97,7 +97,9 @@ namespace awsiotsdk { "+/+", "/+", "sport/tennis/#", - "+/tennis/#" + "+/tennis/#", + "$/tennis/#", + "$sport/tennis/+" }; const util::String SubUnsubActionTester::valid_topic_regexes[VALID_WILDCARD_TOPICS] = { @@ -105,8 +107,10 @@ namespace awsiotsdk { "sport/[^/]*/player1", "[^/]*/[^/]*", "/[^/]*", - "sport/tennis/[^\uc1bf]*", - "[^/]*/tennis/[^\uc1bf]*" + u8"sport/tennis/[^\uc1bf]*", + u8"[^/]*/tennis/[^\uc1bf]*", + u8"\\$/tennis/[^\uc1bf]*", + "\\$sport/tennis/[^/]*" }; const util::String SubUnsubActionTester::invalid_wildcard_test_topics[INVALID_WILDCARD_TOPICS] = { From 13bf2abc11ea7bfec46bbbfa1481edbf77cadf44 Mon Sep 17 00:00:00 2001 From: Steve Harris Date: Mon, 2 Apr 2018 23:38:52 +0000 Subject: [PATCH 3/6] Jobs support with custom auth support Wrap SIGPIPE in ifndef block for Windows Allow Position Independent Code for Static Library (#73) Build the static library with the fPIC compiler flag, so that it can be linked into a shared library. Fix wildcard regex for special topics with $ symbols Adding standard files (#83) Pull requests to fix warnings on Windows Includes #75, #76 and #77. Also includes other changes related to - loading and storing of atomic variables - fix for UTF-8 character representation on Windows Update sample documentation with shadow client token limitation Make Shadow::HandleGetResponse call response handler on Rejected response Fixes #86 Make Shadow::HandleGetResponse call response handler on malformed payload indentation fixup --- CMakeLists.txt | 2 + README.md | 3 + include/ResponseCode.hpp | 7 +- include/jobs/Jobs.hpp | 219 +++++++++++++ network/WebSocket/WebSocketConnection.cpp | 58 +++- network/WebSocket/WebSocketConnection.hpp | 29 ++ samples/Jobs/CMakeLists.txt | 82 +++++ samples/Jobs/JobsSample.cpp | 383 ++++++++++++++++++++++ samples/Jobs/JobsSample.hpp | 68 ++++ samples/README.md | 8 +- src/ResponseCode.cpp | 3 + src/jobs/Jobs.cpp | 340 +++++++++++++++++++ tests/integration/include/JobsTest.hpp | 59 ++++ tests/integration/src/IntegTestRunner.cpp | 12 + tests/integration/src/JobsTest.cpp | 327 ++++++++++++++++++ tests/unit/src/ResponseCodeTests.cpp | 5 + tests/unit/src/jobs/JobsTests.cpp | 264 +++++++++++++++ 17 files changed, 1855 insertions(+), 14 deletions(-) create mode 100644 include/jobs/Jobs.hpp create mode 100644 samples/Jobs/CMakeLists.txt create mode 100644 samples/Jobs/JobsSample.cpp create mode 100644 samples/Jobs/JobsSample.hpp create mode 100644 src/jobs/Jobs.cpp create mode 100644 tests/integration/include/JobsTest.hpp create mode 100644 tests/integration/src/JobsTest.cpp create mode 100644 tests/unit/src/jobs/JobsTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b2ce97a..b09e572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,6 +162,8 @@ add_subdirectory(tests/unit) add_subdirectory(samples/PubSub) +add_subdirectory(samples/Jobs) + add_subdirectory(samples/ShadowDelta) add_subdirectory(samples/Discovery EXCLUDE_FROM_ALL) diff --git a/README.md b/README.md index cc72cb3..89fe3be 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ The Device SDK provides functionality to create and maintain a MQTT Connection. ### Thing Shadow This SDK implements the specific protocol for Thing Shadows to retrieve, update and delete Thing Shadows adhering to the protocol that is implemented to ensure correct versioning and support for client tokens. It abstracts the necessary MQTT topic subscriptions by automatically subscribing to and unsubscribing from the reserved topics as needed for each API call. Inbound state change requests are automatically signalled via a configurable callback. +### Jobs +This SDK also implements the Jobs protocol to interact with the AWS IoT Jobs service. The IoT Job service manages deployment of IoT fleet wide tasks such as device software/firmware deployments and updates, rotation of security certificates, device reboots, and custom device specific management tasks. For additional information please see the [Jobs developer guide](https://docs.aws.amazon.com/iot/latest/developerguide/iot-jobs.html). + ## Design Goals of this SDK The C++ SDK was specifically designed for devices that are not resource constrained and required advanced features such as Message queueing, multi-threading support and the latest language features diff --git a/include/ResponseCode.hpp b/include/ResponseCode.hpp index 0a2e58c..2bfb7fa 100644 --- a/include/ResponseCode.hpp +++ b/include/ResponseCode.hpp @@ -193,7 +193,11 @@ namespace awsiotsdk { // Discovery Response Parsing Error Codes - DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR = -1200 ///< Discover Response Json is missing expected keys + DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR = -1200, ///< Discover Response Json is missing expected keys + + // Jobs Error Codes + + JOBS_INVALID_TOPIC_ERROR = -1300 ///< Jobs invalid topic }; /** @@ -314,6 +318,7 @@ namespace awsiotsdk { const util::String DISCOVER_ACTION_SERVER_ERROR_STRING("Server returned unknown error while performing the discovery action"); const util::String DISCOVER_ACTION_REQUEST_OVERLOAD_STRING("The discovery action is overloading the server, try again after some time"); const util::String DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING("The discover response JSON is incomplete "); + const util::String JOBS_INVALID_TOPIC_ERROR_STRING("Invalid jobs topic"); /** * Takes in a Response Code and returns the appropriate error/success string diff --git a/include/jobs/Jobs.hpp b/include/jobs/Jobs.hpp new file mode 100644 index 0000000..33c2130 --- /dev/null +++ b/include/jobs/Jobs.hpp @@ -0,0 +1,219 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.hpp + * @brief + * + */ + +#pragma once + +#include "mqtt/Client.hpp" + +namespace awsiotsdk { + class Jobs { + public: + // Disabling default and copy constructors. + Jobs() = delete; // Delete Default constructor + Jobs(const Jobs &) = delete; // Delete Copy constructor + Jobs(Jobs &&) = default; // Default Move constructor + Jobs &operator=(const Jobs &) & = delete; // Delete Copy assignment operator + Jobs &operator=(Jobs &&) & = default; // Default Move assignment operator + + /** + * @brief Create factory method. Returns a unique instance of Jobs + * + * @param p_mqtt_client - mqtt client + * @param qos - QoS + * @param thing_name - Thing name + * @param client_token - Client token for correlating messages (optional) + * + * @return std::unique_ptr pointing to a unique Jobs instance + */ + static std::unique_ptr Create(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token = util::String()); + + enum JobExecutionTopicType { + JOB_UNRECOGNIZED_TOPIC = 0, + JOB_GET_PENDING_TOPIC, + JOB_START_NEXT_TOPIC, + JOB_DESCRIBE_TOPIC, + JOB_UPDATE_TOPIC, + JOB_NOTIFY_TOPIC, + JOB_NOTIFY_NEXT_TOPIC, + JOB_WILDCARD_TOPIC + }; + + enum JobExecutionTopicReplyType { + JOB_UNRECOGNIZED_TOPIC_TYPE = 0, + JOB_REQUEST_TYPE, + JOB_ACCEPTED_REPLY_TYPE, + JOB_REJECTED_REPLY_TYPE, + JOB_WILDCARD_REPLY_TYPE + }; + + enum JobExecutionStatus { + JOB_EXECUTION_STATUS_NOT_SET = 0, + JOB_EXECUTION_QUEUED, + JOB_EXECUTION_IN_PROGRESS, + JOB_EXECUTION_FAILED, + JOB_EXECUTION_SUCCEEDED, + JOB_EXECUTION_CANCELED, + JOB_EXECUTION_REJECTED, + /*** + * Used for any status not in the supported list of statuses + */ + JOB_EXECUTION_UNKNOWN_STATUS = 99 + }; + + /** + * @brief GetJobTopic + * + * This function creates a job topic based on the provided parameters. + * + * @param topicType - Jobs topic type + * @param replyType - Topic reply type (optional) + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * + * @return nullptr on error, unique_ptr pointing to a topic string if successful + */ + std::unique_ptr GetJobTopic(JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType = JOB_REQUEST_TYPE, + const util::String &jobId = util::String()); + + /** + * @brief SendJobsQuery + * + * Send a query to the Jobs service using the provided mqtt client + * + * @param topicType - Jobs topic type for type of query + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsQuery(JobExecutionTopicType topicType, + const util::String &jobId = util::String()); + + /** + * @brief SendJobsStartNext + * + * Call Jobs start-next API to start the next pending job execution and trigger response + * + * @param statusDetails - Status details to be associated with started job execution (optional) + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsStartNext(const util::Map &statusDetailsMap = util::Map()); + + /** + * @brief SendJobsDescribe + * + * Send request for job execution details + * + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also + * be omitted to request all pending and in progress job executions + * @param executionNumber - Specific execution number to describe, omit to match latest + * @param includeJobDocument - Flag to indicate whether response should include job document + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsDescribe(const util::String &jobId = util::String(), + int64_t executionNumber = 0, // set to 0 to ignore + bool includeJobDocument = true); + + /** + * @brief SendJobsUpdate + * + * Send update for specified job + * + * @param jobId - Job id associated with job execution to be updated + * @param status - New job execution status + * @param statusDetailsMap - Status details to be associated with job execution (optional) + * @param expectedVersion - Optional expected current job execution number, error response if mismatched + * @param executionNumber - Specific execution number to update, omit to match latest + * @param includeJobExecutionState - Include job execution state in response (optional) + * @param includeJobDocument - Include job document in response (optional) + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsUpdate(const util::String &jobId, + JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, // set to 0 to ignore + int64_t executionNumber = 0, // set to 0 to ignore + bool includeJobExecutionState = false, + bool includeJobDocument = false); + + /** + * @brief CreateJobsSubscription + * + * Create a Jobs Subscription instance + * + * @param p_app_handler - Application Handler instance + * @param p_app_handler_data - Data to be passed to application handler. Can be nullptr + * @param topicType - Jobs topic type to subscribe to (defaults to JOB_WILDCARD_TOPIC) + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * @param replyType - Topic reply type (optional, defaults to JOB_REQUEST_TYPE which omits the reply type in the subscription) + * + * @return shared_ptr Subscription instance + */ + std::shared_ptr CreateJobsSubscription(mqtt::Subscription::ApplicationCallbackHandlerPtr p_app_handler, + std::shared_ptr p_app_handler_data, + JobExecutionTopicType topicType = JOB_WILDCARD_TOPIC, + JobExecutionTopicReplyType replyType = JOB_REQUEST_TYPE, + const util::String &jobId = util::String()); + protected: + std::shared_ptr p_mqtt_client_; + mqtt::QoS qos_; + util::String thing_name_; + util::String client_token_; + + /** + * @brief Jobs constructor + * + * Create Jobs object storing given parameters in created instance + * + * @param p_mqtt_client - mqtt client + * @param qos - QoS + * @param thing_name - Thing name + * @param client_token - Client token for correlating messages (optional) + */ + Jobs(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token); + + static bool BaseTopicRequiresJobId(JobExecutionTopicType topicType); + static const util::String GetOperationForBaseTopic(JobExecutionTopicType topicType); + static const util::String GetSuffixForTopicType(JobExecutionTopicReplyType replyType); + static const util::String GetExecutionStatus(JobExecutionStatus status); + static util::String Escape(const util::String &value); + static util::String SerializeStatusDetails(const util::Map &statusDetailsMap); + + util::String SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, + int64_t executionNumber = 0, + bool includeJobExecutionState = false, + bool includeJobDocument = false); + util::String SerializeDescribeJobExecutionPayload(int64_t executionNumber = 0, + bool includeJobDocument = true); + util::String SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap = util::Map()); + util::String SerializeClientTokenPayload(); + }; +} diff --git a/network/WebSocket/WebSocketConnection.cpp b/network/WebSocket/WebSocketConnection.cpp index 847638b..9214ffd 100644 --- a/network/WebSocket/WebSocketConnection.cpp +++ b/network/WebSocket/WebSocketConnection.cpp @@ -47,6 +47,8 @@ #define X_AMZ_DATE "X-Amz-Date" #define X_AMZ_EXPIRES "X-Amz-Expires" #define X_AMZ_SECURITY_TOKEN "X-Amz-Security-Token" +#define X_AMZ_CUSTOMAUTHORIZER_NAME "X-Amz-CustomAuthorizer-Name" +#define X_AMZ_CUSTOMAUTHORIZER_SIGNATURE "X-Amz-CustomAuthorizer-Signature" #define SIGNING_KEY "AWS4" #define LONG_DATE_FORMAT_STR "%Y%m%dT%H%M%SZ" #define SIMPLE_DATE_FORMAT_STR "%Y%m%d" @@ -99,6 +101,11 @@ namespace awsiotsdk { bool server_verification_flag) : openssl_connection_(endpoint, endpoint_port, root_ca_location, tls_handshake_timeout, tls_read_timeout, tls_write_timeout, server_verification_flag) { + custom_authorizer_name_.clear(); + custom_authorizer_signature_.clear(); + custom_authorizer_token_name_.clear(); + custom_authorizer_token_.clear(); + endpoint_ = endpoint; endpoint_port_ = endpoint_port; root_ca_location_ = root_ca_location; @@ -125,6 +132,21 @@ namespace awsiotsdk { wss_frame_write_ = std::unique_ptr(new wslay_frame_iocb()); } + WebSocketConnection::WebSocketConnection(util::String endpoint, uint16_t endpoint_port, util::String root_ca_location, + std::chrono::milliseconds tls_handshake_timeout, + std::chrono::milliseconds tls_read_timeout, + std::chrono::milliseconds tls_write_timeout, + util::String custom_authorizer_name, util::String custom_authorizer_signature, + util::String custom_authorizer_token_name, util::String custom_authorizer_token, + bool server_verification_flag) + : WebSocketConnection(endpoint, endpoint_port, root_ca_location, "", "", "", "", tls_handshake_timeout, tls_read_timeout, + tls_write_timeout, false) { + custom_authorizer_name_ = custom_authorizer_name; + custom_authorizer_signature_ = custom_authorizer_signature; + custom_authorizer_token_name_ = custom_authorizer_token_name; + custom_authorizer_token_ = custom_authorizer_token; + } + ResponseCode WebSocketConnection::ConnectInternal() { // Init Tls ResponseCode rc = openssl_connection_.Initialize(); @@ -563,17 +585,12 @@ namespace awsiotsdk { } ResponseCode WebSocketConnection::WssHandshake() { + ResponseCode rc; + util::OStringStream stringStream; + // Assuming: // 1. Ssl socket is ready to do read/write. - // Create canonical query string - util::String canonical_query_string; - canonical_query_string.reserve(CANONICAL_QUERY_BUF_LEN); - ResponseCode rc = InitializeCanonicalQueryString(canonical_query_string); - if (ResponseCode::SUCCESS != rc) { - return rc; - } - // Create Wss handshake Http request // -> Generate Wss client key char client_key_buf[WSS_CLIENT_KEY_MAX_LEN + 1]; @@ -583,15 +600,32 @@ namespace awsiotsdk { return rc; } - // -> Assemble Wss Http request - util::OStringStream stringStream; - stringStream << "GET /mqtt?" << canonical_query_string << " " << HTTP_1_1 << "\r\n" - << "Host: " << endpoint_ << "\r\n" + if (custom_authorizer_name_.empty()) { + // Create canonical query string + util::String canonical_query_string; + canonical_query_string.reserve(CANONICAL_QUERY_BUF_LEN); + rc = InitializeCanonicalQueryString(canonical_query_string); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + + // -> Assemble Wss Http request + stringStream << "GET /mqtt?" << canonical_query_string << " " << HTTP_1_1 << "\r\n"; + } else { + // -> Assemble Wss Http request + stringStream << "GET /mqtt " << HTTP_1_1 << "\r\n" + << X_AMZ_CUSTOMAUTHORIZER_NAME << ": " << custom_authorizer_name_ << "\r\n" + << X_AMZ_CUSTOMAUTHORIZER_SIGNATURE << ": " << custom_authorizer_signature_ << "\r\n" + << custom_authorizer_token_name_ << ": " << custom_authorizer_token_ << "\r\n"; + } + + stringStream << "Host: " << endpoint_ << "\r\n" << "Connection: " << UPGRADE << "\r\n" << "Upgrade: " << WEBSOCKET << "\r\n" << "Sec-WebSocket-Version: " << SEC_WEBSOCKET_VERSION_13 << "\r\n" << "sec-websocket-key: " << client_key_buf << "\r\n" << "Sec-WebSocket-Protocol: " << MQTT_PROTOCOL << "\r\n\r\n"; + util::String request_string = stringStream.str(); // Send out request diff --git a/network/WebSocket/WebSocketConnection.hpp b/network/WebSocket/WebSocketConnection.hpp index 761c8cc..fa8a491 100644 --- a/network/WebSocket/WebSocketConnection.hpp +++ b/network/WebSocket/WebSocketConnection.hpp @@ -50,6 +50,10 @@ namespace awsiotsdk { util::String aws_access_key_id_; ///< Pointer to string containing the AWS Access Key Id. util::String aws_secret_access_key_; ///< Pointer to sstring containing the AWS Secret Access Key. util::String aws_session_token_; ///< Pointer to string containing the AWS Session Token. + util::String custom_authorizer_name_; ///< Pointer to string containing the custom authorizer name. + util::String custom_authorizer_signature_; ///< Pointer to string containing the authorizer signature. + util::String custom_authorizer_token_name_; ///< Pointer to string containing the authorizer token name. + util::String custom_authorizer_token_; ///< Pointer to string containing the authorizer token. util::String aws_region_; ///< Region for this connection util::String endpoint_; ///< Endpoint for this connection uint16_t endpoint_port_; ///< Endpoint port @@ -210,6 +214,31 @@ namespace awsiotsdk { std::chrono::milliseconds tls_read_timeout, std::chrono::milliseconds tls_write_timeout, bool server_verification_flag); + /** + * @brief Constructor for the WebSocket for MQTT implementation using custom authentication + * + * Performs any initialization required by the WebSocket layer. + * + * @param util::String endpoint - The target endpoint to connect to + * @param uint16_t endpoint_port - The port on the target to connect to + * @param util::String root_ca_location - Path of the location of the Root CA + * @param std::chrono::milliseconds tls_handshake_timeout - The value to use for timeout of handshake operation + * @param std::chrono::milliseconds tls_read_timeout - The value to use for timeout of read operation + * @param std::chrono::milliseconds tls_write_timeout - The value to use for timeout of write operation + * @param util::String custom_authorizer_name - Name of the authorizer function + * @param util::String custom_authorizer_signature - Authorizer signature + * @param util::String custom_authorizer_token_name - Authorizer token name + * @param util::String custom_authorizer_token - Authorizer token + * @param bool server_verification_flag - used to decide whether server verification is needed or not + * + */ + WebSocketConnection(util::String endpoint, uint16_t endpoint_port, util::String root_ca_location, + std::chrono::milliseconds tls_handshake_timeout, + std::chrono::milliseconds tls_read_timeout, std::chrono::milliseconds tls_write_timeout, + util::String custom_authorizer_name, util::String custom_authorizer_signature, + util::String custom_authorizer_token_name, util::String custom_authorizer_token, + bool server_verification_flag); + /** * @brief Check if WebSocket layer is still connected * diff --git a/samples/Jobs/CMakeLists.txt b/samples/Jobs/CMakeLists.txt new file mode 100644 index 0000000..b11e1c8 --- /dev/null +++ b/samples/Jobs/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.2 FATAL_ERROR) +project(aws-iot-cpp-samples CXX) + +###################################### +# Section : Disable in-source builds # +###################################### + +if (${PROJECT_SOURCE_DIR} STREQUAL ${PROJECT_BINARY_DIR}) + message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt and CMakeFiles folder.") +endif () + +######################################## +# Section : Common Build setttings # +######################################## +# Set required compiler standard to standard c++11. Disable extensions. +set(CMAKE_CXX_STANDARD 11) # C++11... +set(CMAKE_CXX_STANDARD_REQUIRED ON) #...is required... +set(CMAKE_CXX_EXTENSIONS OFF) #...without compiler extensions like gnu++11 + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/archive) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +# Configure Compiler flags +if (UNIX AND NOT APPLE) + # Prefer pthread if found + set(THREADS_PREFER_PTHREAD_FLAG ON) + set(CUSTOM_COMPILER_FLAGS "-fno-exceptions -Wall -Werror") +elseif (APPLE) + set(CUSTOM_COMPILER_FLAGS "-fno-exceptions -Wall -Werror") +elseif (WIN32) + set(CUSTOM_COMPILER_FLAGS "/W4") +endif () + +################################ +# Target : Build Jobs sample # +################################ +set(JOBS_SAMPLE_TARGET_NAME jobs-sample) +# Add Target +add_executable(${JOBS_SAMPLE_TARGET_NAME} "${PROJECT_SOURCE_DIR}/JobsSample.cpp;${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.cpp") + +# Add Target specific includes +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../common) +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}) + +# Configure Threading library +find_package(Threads REQUIRED) + +# Add SDK includes +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${CMAKE_BINARY_DIR}/${DEPENDENCY_DIR}/rapidjson/src/include) +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../include) + +target_link_libraries(${JOBS_SAMPLE_TARGET_NAME} PUBLIC "Threads::Threads") +target_link_libraries(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${SDK_TARGET_NAME}) + +# Copy Json config file +add_custom_command(TARGET ${JOBS_SAMPLE_TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy ${PROJECT_SOURCE_DIR}/../../common/SampleConfig.json $/config/SampleConfig.json) +set_property(TARGET ${JOBS_SAMPLE_TARGET_NAME} APPEND_STRING PROPERTY COMPILE_FLAGS ${CUSTOM_COMPILER_FLAGS}) + +# Gather list of all .cert files in "/cert" +add_custom_command(TARGET ${JOBS_SAMPLE_TARGET_NAME} PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy_directory ${PROJECT_SOURCE_DIR}/../../certs $/certs) + +if (MSVC) + target_sources(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.hpp) + source_group("Header Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.hpp) + source_group("Source Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.cpp) + + target_sources(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/JobsSample.hpp) + source_group("Header Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/JobsSample.hpp) + source_group("Source Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/JobsSample.cpp) +endif () + +######################### +# Add Network libraries # +######################### + +set(NETWORK_WRAPPER_DEST_TARGET ${JOBS_SAMPLE_TARGET_NAME}) +include(${PROJECT_SOURCE_DIR}/../../network/CMakeLists.txt.in) diff --git a/samples/Jobs/JobsSample.cpp b/samples/Jobs/JobsSample.cpp new file mode 100644 index 0000000..a1f032c --- /dev/null +++ b/samples/Jobs/JobsSample.cpp @@ -0,0 +1,383 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsSample.cpp + * + * This example takes the parameters from the config/SampleConfig.json file and establishes + * a connection to the AWS IoT MQTT Platform. It performs several operations to + * demonstrate the basic capabilities of the AWS IoT Jobs platform. + * + * If all the certs are correct, you should see the list of pending Job Executions + * printed out by the GetPendingCallback callback. If there are any existing pending + * job executions each will be processed one at a time in the NextJobCallback callback. + * After all of the pending jobs have been processed the program will wait for + * notifications for new pending jobs and process them one at a time as they come in. + * + * In the Subscribe function you can see how each callback is registered for each corresponding + * Jobs topic. + * + */ + +#include +#include + +#ifdef USE_WEBSOCKETS +#include "WebSocketConnection.hpp" +#elif defined USE_MBEDTLS +#include "MbedTLSConnection.hpp" +#else +#include "OpenSSLConnection.hpp" +#endif + +#include "util/logging/Logging.hpp" +#include "util/logging/LogMacros.hpp" +#include "util/logging/ConsoleLogSystem.hpp" + +#include "ConfigCommon.hpp" +#include "jobs/Jobs.hpp" +#include "JobsSample.hpp" + +#define LOG_TAG_JOBS "[Sample - Jobs]" + +namespace awsiotsdk { + namespace samples { + ResponseCode JobsSample::GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "GetPendingCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Json Parse for GetPendingCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("inProgressJobs")) { + std::cout << "inProgressJobs : " << util::JsonParser::ToString(doc["inProgressJobs"]) << std::endl; + } + + if (doc.HasMember("queuedJobs")) { + std::cout << "queuedJobs : " << util::JsonParser::ToString(doc["queuedJobs"]) << std::endl; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "NextJobCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Json Parse for NextJobCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("execution")) { + std::cout << "execution : " << util::JsonParser::ToString(doc["execution"]) << std::endl; + + if (doc["execution"].HasMember("jobId")) { + util::Map statusDetailsMap; + + util::String jobId = doc["execution"]["jobId"].GetString(); + std::cout << "jobId : " << jobId << std::endl; + + if (doc["execution"].HasMember("jobDocument")) { + std::cout << "jobDocument : " << util::JsonParser::ToString(doc["execution"]["jobDocument"]) << std::endl; + statusDetailsMap.insert(std::make_pair("exampleDetail", "a value appropriate for your successful job")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_SUCCEEDED, statusDetailsMap); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "SendJobsUpdate failed. %s", ResponseHelper::ToString(rc).c_str()); + return rc; + } + } else { + statusDetailsMap.insert(std::make_pair("failureDetail", "Unable to process job document")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_FAILED, statusDetailsMap); + } + } + } else { + std::cout << "No job execution description found, nothing to do." << std::endl; + done_ = true; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::UpdateAcceptedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + std::cout << std::endl << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::UpdateRejectedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + std::cout << std::endl << "************" << std::endl; + + /* Do error handling here for when the update was rejected */ + + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::DisconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data) { + std::cout << "*******************************************" << std::endl + << client_id << " Disconnected!" << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::ReconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode reconnect_result) { + std::cout << "*******************************************" << std::endl + << client_id << " Reconnect Attempted. Result " << ResponseHelper::ToString(reconnect_result) + << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::ResubscribeCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode resubscribe_result) { + std::cout << "*******************************************" << std::endl + << client_id << " Resubscribe Attempted. Result" << ResponseHelper::ToString(resubscribe_result) + << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + + ResponseCode JobsSample::Subscribe() { + std::cout << "******** Subscribe ***************" << std::endl; + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_pending_handler = + std::bind(&JobsSample::GetPendingCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_next_handler = + std::bind(&JobsSample::NextJobCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_update_accepted_handler = + std::bind(&JobsSample::UpdateAcceptedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_update_rejected_handler = + std::bind(&JobsSample::UpdateRejectedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + util::Vector> topic_vector; + std::shared_ptr p_subscription; + + p_subscription = p_jobs_->CreateJobsSubscription(p_pending_handler, nullptr, Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_update_accepted_handler, nullptr, Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "+"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_update_rejected_handler, nullptr, Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, "+"); + topic_vector.push_back(p_subscription); + + ResponseCode rc = p_iot_client_->Subscribe(topic_vector, ConfigCommon::mqtt_command_timeout_); + return rc; + } + + ResponseCode JobsSample::InitializeTLS() { + ResponseCode rc = ResponseCode::SUCCESS; + +#ifdef USE_WEBSOCKETS + p_network_connection_ = std::shared_ptr( + new network::WebSocketConnection(ConfigCommon::endpoint_, ConfigCommon::endpoint_https_port_, + ConfigCommon::root_ca_path_, ConfigCommon::aws_region_, + ConfigCommon::aws_access_key_id_, + ConfigCommon::aws_secret_access_key_, + ConfigCommon::aws_session_token_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true)); + if (nullptr == p_network_connection_) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } +#elif defined USE_MBEDTLS + p_network_connection_ = std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, + true); + if (nullptr == p_network_connection_) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } +#else + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + rc = p_network_connection->Initialize(); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, + "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#endif + return rc; + } + + ResponseCode JobsSample::RunSample() { + done_ = false; + + ResponseCode rc = InitializeTLS(); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + + ClientCoreState::ApplicationDisconnectCallbackPtr p_disconnect_handler = + std::bind(&JobsSample::DisconnectCallback, this, std::placeholders::_1, std::placeholders::_2); + + ClientCoreState::ApplicationReconnectCallbackPtr p_reconnect_handler = + std::bind(&JobsSample::ReconnectCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + ClientCoreState::ApplicationResubscribeCallbackPtr p_resubscribe_handler = + std::bind(&JobsSample::ResubscribeCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + p_iot_client_ = std::shared_ptr(MqttClient::Create(p_network_connection_, + ConfigCommon::mqtt_command_timeout_, + p_disconnect_handler, nullptr, + p_reconnect_handler, nullptr, + p_resubscribe_handler, nullptr)); + if (nullptr == p_iot_client_) { + return ResponseCode::FAILURE; + } + + util::String client_id_tagged = ConfigCommon::base_client_id_; + client_id_tagged.append("_jobs_sample_"); + client_id_tagged.append(std::to_string(rand())); + std::unique_ptr client_id = Utf8String::Create(client_id_tagged); + + rc = p_iot_client_->Connect(ConfigCommon::mqtt_command_timeout_, ConfigCommon::is_clean_session_, + mqtt::Version::MQTT_3_1_1, ConfigCommon::keep_alive_timeout_secs_, + std::move(client_id), nullptr, nullptr, nullptr); + if (ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED != rc) { + return rc; + } + + p_jobs_ = Jobs::Create(p_iot_client_, mqtt::QoS::QOS1, ConfigCommon::thing_name_, client_id_tagged); + + rc = Subscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Subscribe failed. %s", ResponseHelper::ToString(rc).c_str()); + } else { + rc = p_jobs_->SendJobsQuery(Jobs::JOB_GET_PENDING_TOPIC); + + if (ResponseCode::SUCCESS == rc) { + rc = p_jobs_->SendJobsQuery(Jobs::JOB_DESCRIBE_TOPIC, "$next"); + } + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + } + } + + // Wait for job processing to complete + while (!done_) { + done_ = true; + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + + rc = p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Disconnect failed. %s", ResponseHelper::ToString(rc).c_str()); + } + + std::cout << "Exiting Sample!!!!" << std::endl; + return ResponseCode::SUCCESS; + } + } +} + +int main(int argc, char **argv) { + std::shared_ptr p_log_system = + std::make_shared(awsiotsdk::util::Logging::LogLevel::Info); + awsiotsdk::util::Logging::InitializeAWSLogging(p_log_system); + + std::unique_ptr + jobs_sample = std::unique_ptr(new awsiotsdk::samples::JobsSample()); + + awsiotsdk::ResponseCode rc = awsiotsdk::ConfigCommon::InitializeCommon("config/SampleConfig.json"); + if (awsiotsdk::ResponseCode::SUCCESS == rc) { + rc = jobs_sample->RunSample(); + } +#ifdef WIN32 + std::cout<<"Press any key to continue!!!!"<(rc); +} diff --git a/samples/Jobs/JobsSample.hpp b/samples/Jobs/JobsSample.hpp new file mode 100644 index 0000000..8080b61 --- /dev/null +++ b/samples/Jobs/JobsSample.hpp @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsSample.hpp + * @brief + * + */ + + +#pragma once + +#include "mqtt/Client.hpp" +#include "NetworkConnection.hpp" + +namespace awsiotsdk { + namespace samples { + class JobsSample { + protected: + std::shared_ptr p_network_connection_; + std::shared_ptr p_iot_client_; + std::shared_ptr p_jobs_; + std::atomic done_; + + ResponseCode DisconnectCallback(util::String topic_name, + std::shared_ptr p_app_handler_data); + ResponseCode ReconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode reconnect_result); + ResponseCode ResubscribeCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode resubscribe_result); + + ResponseCode GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode UpdateAcceptedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode UpdateRejectedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + + ResponseCode Subscribe(); + ResponseCode InitializeTLS(); + + public: + ResponseCode RunSample(); + }; + } +} + + diff --git a/samples/README.md b/samples/README.md index 3be0c30..00fdfb5 100644 --- a/samples/README.md +++ b/samples/README.md @@ -17,9 +17,15 @@ This sample demonstrates how various Shadow operations can be performed. * Code for this sample is located [here](./ShadowDelta) * Target for this sample is `shadow-delta-sample` - + Note: The shadow client token is set as the thing name by default in the sample. The shadow client token is limited to 64 bytes and will return an error if a token longer than 64 bytes is used (`"code":400,"message":"invalid client token"`, although receiving a 400 does not necessarily mean that it is due to the length of the client token). Modify the code [here](../ShadowDelta/ShadowDelta.cpp#L184) if your thing name is longer than 64 bytes to prevent this error. +### Jobs Sample +This sample demonstrates how various Jobs API operations can be performed including subscribing to Jobs notifications and publishing Job execution updates. + + * Code for this sample is located [here](./Jobs) + * Target for this sample is `jobs-sample` + ### Discovery Sample This sample demonstrates how the discovery operation can be performed to get the connectivity information to connect to a Greengrass Core (GGC). The configuration for this example is slightly different as the Discovery operation is a HTTP call, and uses port 8443, instead of port 8883 which is used for MQTT operations. The endpoint is the same IoT host endpoint used to connect the IoT thing to the cloud. diff --git a/src/ResponseCode.cpp b/src/ResponseCode.cpp index 2f3e21b..fad8420 100644 --- a/src/ResponseCode.cpp +++ b/src/ResponseCode.cpp @@ -343,6 +343,9 @@ namespace awsiotsdk { case ResponseCode::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR: os << awsiotsdk::ResponseHelper::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING; break; + case ResponseCode::JOBS_INVALID_TOPIC_ERROR: + os << awsiotsdk::ResponseHelper::JOBS_INVALID_TOPIC_ERROR_STRING; + break; } os << " : SDK Code " << static_cast(rc) << "."; return os; diff --git a/src/jobs/Jobs.cpp b/src/jobs/Jobs.cpp new file mode 100644 index 0000000..902ec91 --- /dev/null +++ b/src/jobs/Jobs.cpp @@ -0,0 +1,340 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.cpp + * @brief + * + */ + +#include "util/logging/LogMacros.hpp" + +#include "jobs/Jobs.hpp" + +#define BASE_THINGS_TOPIC "$aws/things/" + +#define NOTIFY_OPERATION "notify" +#define NOTIFY_NEXT_OPERATION "notify-next" +#define GET_OPERATION "get" +#define START_NEXT_OPERATION "start-next" +#define WILDCARD_OPERATION "+" +#define UPDATE_OPERATION "update" +#define ACCEPTED_REPLY "accepted" +#define REJECTED_REPLY "rejected" +#define WILDCARD_REPLY "#" + +namespace awsiotsdk { + std::unique_ptr Jobs::Create(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token) { + if (nullptr == p_mqtt_client) { + return nullptr; + } + + return std::unique_ptr(new Jobs(p_mqtt_client, qos, thing_name, client_token)); + } + + Jobs::Jobs(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token) { + p_mqtt_client_ = p_mqtt_client; + qos_ = qos; + thing_name_ = thing_name; + client_token_ = client_token; + }; + + bool Jobs::BaseTopicRequiresJobId(JobExecutionTopicType topicType) { + switch (topicType) { + case JOB_UPDATE_TOPIC: + case JOB_DESCRIBE_TOPIC: + return true; + case JOB_NOTIFY_TOPIC: + case JOB_NOTIFY_NEXT_TOPIC: + case JOB_START_NEXT_TOPIC: + case JOB_GET_PENDING_TOPIC: + case JOB_WILDCARD_TOPIC: + case JOB_UNRECOGNIZED_TOPIC: + default: + return false; + } + }; + + const util::String Jobs::GetOperationForBaseTopic(JobExecutionTopicType topicType) { + switch (topicType) { + case JOB_UPDATE_TOPIC: + return UPDATE_OPERATION; + case JOB_NOTIFY_TOPIC: + return NOTIFY_OPERATION; + case JOB_NOTIFY_NEXT_TOPIC: + return NOTIFY_NEXT_OPERATION; + case JOB_GET_PENDING_TOPIC: + case JOB_DESCRIBE_TOPIC: + return GET_OPERATION; + case JOB_START_NEXT_TOPIC: + return START_NEXT_OPERATION; + case JOB_WILDCARD_TOPIC: + return WILDCARD_OPERATION; + case JOB_UNRECOGNIZED_TOPIC: + default: + return ""; + } + }; + + const util::String Jobs::GetSuffixForTopicType(JobExecutionTopicReplyType replyType) { + switch (replyType) { + case JOB_REQUEST_TYPE: + return ""; + case JOB_ACCEPTED_REPLY_TYPE: + return "/" ACCEPTED_REPLY; + case JOB_REJECTED_REPLY_TYPE: + return "/" REJECTED_REPLY; + case JOB_WILDCARD_REPLY_TYPE: + return "/" WILDCARD_REPLY; + case JOB_UNRECOGNIZED_TOPIC_TYPE: + default: + return ""; + } + } + + const util::String Jobs::GetExecutionStatus(JobExecutionStatus status) { + switch (status) { + case JOB_EXECUTION_QUEUED: + return "QUEUED"; + case JOB_EXECUTION_IN_PROGRESS: + return "IN_PROGRESS"; + case JOB_EXECUTION_FAILED: + return "FAILED"; + case JOB_EXECUTION_SUCCEEDED: + return "SUCCEEDED"; + case JOB_EXECUTION_CANCELED: + return "CANCELED"; + case JOB_EXECUTION_REJECTED: + return "REJECTED"; + case JOB_EXECUTION_STATUS_NOT_SET: + case JOB_EXECUTION_UNKNOWN_STATUS: + default: + return ""; + } + } + + util::String Jobs::Escape(const util::String &value) { + util::String result = ""; + + for (int i = 0; i < value.length(); i++) { + switch(value[i]) { + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + case '"': result += "\\\""; break; + case '\\': result += "\\\\"; break; + default: result += value[i]; + } + } + return result; + } + + util::String Jobs::SerializeStatusDetails(const util::Map &statusDetailsMap) { + util::String result = "{"; + + util::Map::const_iterator itr = statusDetailsMap.begin(); + while (itr != statusDetailsMap.end()) { + result += (itr == statusDetailsMap.begin() ? "\"" : ",\""); + result += Escape(itr->first) + "\":\"" + Escape(itr->second) + "\""; + itr++; + } + + result += '}'; + return result; + } + + std::unique_ptr Jobs::GetJobTopic(JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType, + const util::String &jobId) { + if (thing_name_.empty()) { + return nullptr; + } + + if ((topicType == JOB_NOTIFY_TOPIC || topicType == JOB_NOTIFY_NEXT_TOPIC) && replyType != JOB_REQUEST_TYPE) { + return nullptr; + } + + if ((topicType == JOB_GET_PENDING_TOPIC || topicType == JOB_START_NEXT_TOPIC || + topicType == JOB_NOTIFY_TOPIC || topicType == JOB_NOTIFY_NEXT_TOPIC) && !jobId.empty()) { + return nullptr; + } + + const bool requireJobId = BaseTopicRequiresJobId(topicType); + if (jobId.empty() && requireJobId) { + return nullptr; + } + + const util::String operation = GetOperationForBaseTopic(topicType); + if (operation.empty()) { + return nullptr; + } + + const util::String suffix = GetSuffixForTopicType(replyType); + + if (requireJobId) { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/" + jobId + '/' + operation + suffix); + } else if (topicType == JOB_WILDCARD_TOPIC) { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/#"); + } else { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/" + operation + suffix); + } + }; + + util::String Jobs::SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap, + int64_t expectedVersion, // set to 0 to ignore + int64_t executionNumber, // set to 0 to ignore + bool includeJobExecutionState, + bool includeJobDocument) { + const util::String executionStatus = GetExecutionStatus(status); + + if (executionStatus.empty()) { + return ""; + } + + util::String result = "{\"status\":\"" + executionStatus + "\""; + if (!statusDetailsMap.empty()) { + result += ",\"statusDetails\":" + SerializeStatusDetails(statusDetailsMap); + } + if (expectedVersion > 0) { + result += ",\"expectedVersion\":\"" + std::to_string(expectedVersion) + "\""; + } + if (executionNumber > 0) { + result += ",\"executionNumber\":\"" + std::to_string(executionNumber) + "\""; + } + if (includeJobExecutionState) { + result += ",\"includeJobExecutionState\":\"true\""; + } + if (includeJobDocument) { + result += ",\"includeJobDocument\":\"true\""; + } + if (!client_token_.empty()) { + result += ",\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeDescribeJobExecutionPayload(int64_t executionNumber, // set to 0 to ignore + bool includeJobDocument) { + util::String result = "{\"includeJobDocument\":\""; + result += (includeJobDocument ? "true" : "false"); + result += "\""; + if (executionNumber > 0) { + result += ",\"executionNumber\":\"" + std::to_string(executionNumber) + "\""; + } + if (!client_token_.empty()) { + result += "\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap) { + util::String result = "{"; + if (!statusDetailsMap.empty()) { + result += "\"statusDetails\":" + SerializeStatusDetails(statusDetailsMap); + } + if (!client_token_.empty()) { + if (!statusDetailsMap.empty()) { + result += ','; + } + result += "\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeClientTokenPayload() { + if (!client_token_.empty()) { + return "{\"clientToken\":\"" + client_token_ + "\"}"; + } + + return "{}"; + }; + + ResponseCode Jobs::SendJobsQuery(JobExecutionTopicType topicType, + const util::String &jobId) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(topicType, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeClientTokenPayload(), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsStartNext(const util::Map &statusDetailsMap) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_START_NEXT_TOPIC, JOB_REQUEST_TYPE); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeStartNextPendingJobExecutionPayload(statusDetailsMap), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsDescribe(const util::String &jobId, + int64_t executionNumber, // set to 0 to ignore + bool includeJobDocument) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_DESCRIBE_TOPIC, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeDescribeJobExecutionPayload(executionNumber, includeJobDocument), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsUpdate(const util::String &jobId, + JobExecutionStatus status, + const util::Map &statusDetailsMap, + int64_t expectedVersion, // set to 0 to ignore + int64_t executionNumber, // set to 0 to ignore + bool includeJobExecutionState, + bool includeJobDocument) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_UPDATE_TOPIC, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, + SerializeJobExecutionUpdatePayload(status, statusDetailsMap, expectedVersion, executionNumber, + includeJobExecutionState, includeJobDocument), + nullptr, packet_id); + }; + + std::shared_ptr Jobs::CreateJobsSubscription(mqtt::Subscription::ApplicationCallbackHandlerPtr p_app_handler, + std::shared_ptr p_app_handler_data, + JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType, + const util::String &jobId) { + return mqtt::Subscription::Create(GetJobTopic(topicType, replyType, jobId), qos_, p_app_handler, p_app_handler_data); + }; +} diff --git a/tests/integration/include/JobsTest.hpp b/tests/integration/include/JobsTest.hpp new file mode 100644 index 0000000..e958fb7 --- /dev/null +++ b/tests/integration/include/JobsTest.hpp @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.hpp + * @brief + * + */ + + +#pragma once + +#include "mqtt/Client.hpp" +#include "NetworkConnection.hpp" +#include "jobs/Jobs.hpp" + +namespace awsiotsdk { + namespace tests { + namespace integration { + class JobsTest { + protected: + static const std::chrono::seconds keep_alive_timeout_; + + std::shared_ptr p_network_connection_; + std::shared_ptr p_iot_client_; + std::shared_ptr p_jobs_; + std::atomic done_; + + ResponseCode GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + + ResponseCode Subscribe(); + ResponseCode Unsubscribe(); + ResponseCode InitializeTLS(); + + public: + ResponseCode RunTest(); + }; + } + } +} + + diff --git a/tests/integration/src/IntegTestRunner.cpp b/tests/integration/src/IntegTestRunner.cpp index fc6c160..73eef3b 100644 --- a/tests/integration/src/IntegTestRunner.cpp +++ b/tests/integration/src/IntegTestRunner.cpp @@ -28,6 +28,7 @@ #include "ConfigCommon.hpp" #include "IntegTestRunner.hpp" #include "SdkTestConfig.hpp" +#include "JobsTest.hpp" #include "PubSub.hpp" #include "AutoReconnect.hpp" #include "MultipleClients.hpp" @@ -53,6 +54,17 @@ namespace awsiotsdk { ResponseCode IntegTestRunner::RunAllTests() { ResponseCode rc = ResponseCode::SUCCESS; // Each test runs in its own scope to ensure complete cleanup + /** + * Run Jobs Tests + */ + { + JobsTest jobs_test_runner; + rc = jobs_test_runner.RunTest(); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + } + /** * Run Subscribe Publish Tests */ diff --git a/tests/integration/src/JobsTest.cpp b/tests/integration/src/JobsTest.cpp new file mode 100644 index 0000000..3e913df --- /dev/null +++ b/tests/integration/src/JobsTest.cpp @@ -0,0 +1,327 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsTest.cpp + * @brief + * + */ + +#include "JobsTest.hpp" +#include "util/logging/LogMacros.hpp" + +#include +#include + +#ifdef USE_WEBSOCKETS +#include "WebSocketConnection.hpp" +#elif defined USE_MBEDTLS +#include "MbedTLSConnection.hpp" +#else +#include "OpenSSLConnection.hpp" +#endif + +#include "ConfigCommon.hpp" + +#define JOBS_INTEGRATION_TEST_TAG "[Integration Test - Jobs]" + +namespace awsiotsdk { + namespace tests { + namespace integration { + ResponseCode JobsTest::GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "GetPendingCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Json Parse for GetPendingCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("inProgressJobs")) { + std::cout << "inProgressJobs : " << util::JsonParser::ToString(doc["inProgressJobs"]) << std::endl; + } + + if (doc.HasMember("queuedJobs")) { + std::cout << "queuedJobs : " << util::JsonParser::ToString(doc["queuedJobs"]) << std::endl; + } + + std::cout << "************" << std::endl; + + rc = p_jobs_->SendJobsQuery(Jobs::JOB_DESCRIBE_TOPIC, "$next"); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + + return ResponseCode::FAILURE; + } + + return ResponseCode::SUCCESS; + } + + ResponseCode JobsTest::NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "NextJobCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Json Parse for NextJobCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("execution")) { + std::cout << "execution : " << util::JsonParser::ToString(doc["execution"]) << std::endl; + + if (doc["execution"].HasMember("jobId")) { + util::Map statusDetailsMap; + + util::String jobId = doc["execution"]["jobId"].GetString(); + std::cout << "jobId : " << jobId << std::endl; + + if (doc["execution"].HasMember("jobDocument")) { + std::cout << "jobDocument : " << util::JsonParser::ToString(doc["execution"]["jobDocument"]) << std::endl; + statusDetailsMap.insert(std::make_pair("exampleDetail", "a value appropriate for your successful job")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_SUCCEEDED, statusDetailsMap); + } else { + statusDetailsMap.insert(std::make_pair("failureDetail", "Unable to process job document")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_FAILED, statusDetailsMap); + } + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsUpdate failed. %s", ResponseHelper::ToString(rc).c_str()); + return rc; + } + } + } else { + std::cout << "No job execution description found, nothing to do." << std::endl; + done_ = true; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsTest::Subscribe() { + std::cout << "******** Subscribe ***************" << std::endl; + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_pending_handler = + std::bind(&JobsTest::GetPendingCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_next_handler = + std::bind(&JobsTest::NextJobCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + util::Vector> topic_vector; + std::shared_ptr p_subscription; + + p_subscription = p_jobs_->CreateJobsSubscription(p_pending_handler, nullptr, Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(p_subscription); + + ResponseCode rc = p_iot_client_->Subscribe(topic_vector, ConfigCommon::mqtt_command_timeout_); + std::this_thread::sleep_for(std::chrono::seconds(3)); + return rc; + } + + ResponseCode JobsTest::Unsubscribe() { + uint16_t packet_id = 0; + std::unique_ptr p_topic_name; + util::Vector> topic_vector; + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(std::move(p_topic_name)); + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(std::move(p_topic_name)); + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(std::move(p_topic_name)); + + ResponseCode rc = p_iot_client_->UnsubscribeAsync(std::move(topic_vector), nullptr, packet_id); + std::this_thread::sleep_for(std::chrono::seconds(1)); + return rc; + } + + ResponseCode JobsTest::InitializeTLS() { + ResponseCode rc = ResponseCode::SUCCESS; + +#ifdef USE_WEBSOCKETS + p_network_connection_ = std::shared_ptr( + new network::WebSocketConnection(ConfigCommon::endpoint_, ConfigCommon::endpoint_https_port_, + ConfigCommon::root_ca_path_, ConfigCommon::aws_region_, + ConfigCommon::aws_access_key_id_, + ConfigCommon::aws_secret_access_key_, + ConfigCommon::aws_session_token_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true)); +#elif defined USE_MBEDTLS + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + + if (ResponseCode::SUCCESS != rc) { + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#else + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + rc = p_network_connection->Initialize(); + + if (ResponseCode::SUCCESS != rc) { + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#endif + return rc; + } + + ResponseCode JobsTest::RunTest() { + done_ = false; + ResponseCode rc = InitializeTLS(); + + do { + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Failed to initialize TLS layer. %s", + ResponseHelper::ToString(rc).c_str()); + break; + } + + p_iot_client_ = std::shared_ptr( + MqttClient::Create(p_network_connection_, ConfigCommon::mqtt_command_timeout_)); + if (nullptr == p_iot_client_) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Failed to create MQTT Client Instance!!"); + rc = ResponseCode::FAILURE; + break; + } + + util::String client_id_tagged = ConfigCommon::base_client_id_; + client_id_tagged.append("_jobs_tester_"); + client_id_tagged.append(std::to_string(rand())); + std::unique_ptr client_id = Utf8String::Create(client_id_tagged); + + rc = p_iot_client_->Connect(ConfigCommon::mqtt_command_timeout_, ConfigCommon::is_clean_session_, + mqtt::Version::MQTT_3_1_1, ConfigCommon::keep_alive_timeout_secs_, + std::move(client_id), nullptr, nullptr, nullptr); + + p_jobs_ = Jobs::Create(p_iot_client_, mqtt::QoS::QOS1, ConfigCommon::thing_name_, client_id_tagged); + + if (ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "MQTT Connect failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + rc = Subscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Subscribe failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + break; + } + + rc = p_jobs_->SendJobsQuery(Jobs::JOB_GET_PENDING_TOPIC); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + } + + int retries = 5; + while (!done_ && retries-- > 0) { + std::this_thread::sleep_for(std::chrono::seconds(5)); + } + + if (!done_) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Not all jobs processed."); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + rc = ResponseCode::FAILURE; + break; + } + + rc = Unsubscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Unsubscribe failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + break; + } + + rc = p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Disconnect failed. %s", + ResponseHelper::ToString(rc).c_str()); + break; + } + } while (false); + + std::cout << std::endl; + if (ResponseCode::SUCCESS != rc) { + std::cout + << "Test Failed!!!! See above output for details!!" + << std::endl; + std::cout << "**********************************************************" << std::endl; + return ResponseCode::FAILURE; + } + + std::cout << "Test Successful!!!!" << std::endl; + std::cout << "**********************************************************" << std::endl; + return ResponseCode::SUCCESS; + } + } + } +} diff --git a/tests/unit/src/ResponseCodeTests.cpp b/tests/unit/src/ResponseCodeTests.cpp index 0d6f5cc..ada914e 100644 --- a/tests/unit/src/ResponseCodeTests.cpp +++ b/tests/unit/src/ResponseCodeTests.cpp @@ -570,6 +570,11 @@ namespace awsiotsdk { expected_string = ResponseCodeToString(ResponseHelper::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING, ResponseCode::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR); EXPECT_EQ(expected_string, response_string); + + response_string = ResponseHelper::ToString(ResponseCode::JOBS_INVALID_TOPIC_ERROR); + expected_string = ResponseCodeToString(ResponseHelper::JOBS_INVALID_TOPIC_ERROR_STRING, + ResponseCode::JOBS_INVALID_TOPIC_ERROR); + EXPECT_EQ(expected_string, response_string); } } } diff --git a/tests/unit/src/jobs/JobsTests.cpp b/tests/unit/src/jobs/JobsTests.cpp new file mode 100644 index 0000000..56b5161 --- /dev/null +++ b/tests/unit/src/jobs/JobsTests.cpp @@ -0,0 +1,264 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsTests.cpp + * @brief + * + */ + +#include + +#include + +#include "util/logging/LogMacros.hpp" + +#include "TestHelper.hpp" +#include "MockNetworkConnection.hpp" + +#include "jobs/Jobs.hpp" +#include "mqtt/ClientState.hpp" + +#define JOBS_TEST_LOG_TAG "[Jobs Unit Test]" + +namespace awsiotsdk { + namespace tests { + namespace unit { + class JobsTestWrapper : public Jobs { + protected: + static const util::String test_thing_name_; + static const util::String client_token_; + + public: + JobsTestWrapper(bool empty_thing_name, bool empty_client_token): + Jobs(nullptr, mqtt::QoS::QOS0, + empty_thing_name ? "" : test_thing_name_, + empty_client_token ? "" : client_token_) {} + + util::String SerializeStatusDetails(const util::Map &statusDetailsMap) { + return Jobs::SerializeStatusDetails(statusDetailsMap); + } + + util::String SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, + int64_t executionNumber = 0, + bool includeJobExecutionState = false, + bool includeJobDocument = false) { + return Jobs::SerializeJobExecutionUpdatePayload(status, statusDetailsMap, expectedVersion, executionNumber, includeJobExecutionState, includeJobDocument); + } + + util::String SerializeDescribeJobExecutionPayload(int64_t executionNumber = 0, + bool includeJobDocument = true) { + return Jobs::SerializeDescribeJobExecutionPayload(executionNumber, includeJobDocument); + } + + util::String SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap = util::Map()) { + return Jobs::SerializeStartNextPendingJobExecutionPayload(statusDetailsMap); + } + + util::String SerializeClientTokenPayload() { + return Jobs::SerializeClientTokenPayload(); + } + + util::String Escape(const util::String &value) { + return Jobs::Escape(value); + } + }; + + const util::String JobsTestWrapper::test_thing_name_ = "CppSdkTestClient"; + const util::String JobsTestWrapper::client_token_ = "CppSdkTestClientToken"; + + class JobsTester : public ::testing::Test { + protected: + static const util::String job_id_; + + std::shared_ptr p_jobs_; + std::shared_ptr p_jobs_empty_client_token_; + std::shared_ptr p_jobs_empty_thing_name_; + + JobsTester() { + p_jobs_ = std::shared_ptr(new JobsTestWrapper(false, false)); + p_jobs_empty_client_token_ = std::shared_ptr(new JobsTestWrapper(false, true)); + p_jobs_empty_thing_name_ = std::shared_ptr(new JobsTestWrapper(true, false)); + } + }; + + const util::String JobsTester::job_id_ = "TestJobId"; + + TEST_F(JobsTester, ValidTopicsTests) { + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/accepted", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/rejected", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/#", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/accepted", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/rejected", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/#", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/accepted", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/rejected", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/#", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/accepted", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/rejected", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/#", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/notify", p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/notify-next", p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + } + + TEST_F(JobsTester, InvalidTopicsTests) { + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + } + + + TEST_F(JobsTester, PayloadSerializationTests) { + util::Map statusDetailsMap; + statusDetailsMap.insert(std::make_pair("testKey", "testVal")); + + EXPECT_EQ("{}", p_jobs_empty_client_token_->SerializeClientTokenPayload()); + EXPECT_EQ("{\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeClientTokenPayload()); + + EXPECT_EQ("{}", p_jobs_empty_client_token_->SerializeStartNextPendingJobExecutionPayload()); + EXPECT_EQ("{\"statusDetails\":{\"testKey\":\"testVal\"}}", p_jobs_empty_client_token_->SerializeStartNextPendingJobExecutionPayload(statusDetailsMap)); + EXPECT_EQ("{\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeStartNextPendingJobExecutionPayload()); + EXPECT_EQ("{\"statusDetails\":{\"testKey\":\"testVal\"},\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeStartNextPendingJobExecutionPayload(statusDetailsMap)); + + EXPECT_EQ("{\"includeJobDocument\":\"true\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload()); + EXPECT_EQ("{\"includeJobDocument\":\"true\",\"executionNumber\":\"1\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload(1)); + EXPECT_EQ("{\"includeJobDocument\":\"false\",\"executionNumber\":\"1\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload(1, false)); + + EXPECT_EQ("{\"includeJobDocument\":\"true\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload()); + EXPECT_EQ("{\"includeJobDocument\":\"true\",\"executionNumber\":\"1\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload(1)); + EXPECT_EQ("{\"includeJobDocument\":\"false\",\"executionNumber\":\"1\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload(1, false)); + + EXPECT_EQ("", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_STATUS_NOT_SET)); + EXPECT_EQ("", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_UNKNOWN_STATUS)); + EXPECT_EQ("", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_STATUS_NOT_SET)); + EXPECT_EQ("", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_UNKNOWN_STATUS)); + + EXPECT_EQ("{\"status\":\"QUEUED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"}}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"includeJobDocument\":\"true\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true, true)); + + EXPECT_EQ("{\"status\":\"IN_PROGRESS\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_IN_PROGRESS)); + EXPECT_EQ("{\"status\":\"FAILED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_FAILED)); + EXPECT_EQ("{\"status\":\"SUCCEEDED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_SUCCEEDED)); + EXPECT_EQ("{\"status\":\"CANCELED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_CANCELED)); + EXPECT_EQ("{\"status\":\"REJECTED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_REJECTED)); + + EXPECT_EQ("{\"status\":\"QUEUED\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"includeJobDocument\":\"true\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true, true)); + + EXPECT_EQ("{\"status\":\"IN_PROGRESS\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_IN_PROGRESS)); + EXPECT_EQ("{\"status\":\"FAILED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_FAILED)); + EXPECT_EQ("{\"status\":\"SUCCEEDED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_SUCCEEDED)); + EXPECT_EQ("{\"status\":\"CANCELED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_CANCELED)); + EXPECT_EQ("{\"status\":\"REJECTED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_REJECTED)); + + statusDetailsMap.insert(std::make_pair("testEscapeKey \" \t \r \n \\ '!", "testEscapeVal \" \t \r \n \\ '!")); + EXPECT_EQ("{\"testEscapeKey \\\" \\t \\r \\n \\\\ '!\":\"testEscapeVal \\\" \\t \\r \\n \\\\ '!\",\"testKey\":\"testVal\"}", p_jobs_->SerializeStatusDetails(statusDetailsMap)); + } + } + } +} From 64c697297f8f27dd34df19fd09db62a781804958 Mon Sep 17 00:00:00 2001 From: Steve Harris Date: Mon, 2 Apr 2018 23:38:52 +0000 Subject: [PATCH 4/6] Jobs support with custom auth support Wrap SIGPIPE in ifndef block for Windows Allow Position Independent Code for Static Library (#73) Build the static library with the fPIC compiler flag, so that it can be linked into a shared library. Fix wildcard regex for special topics with $ symbols Adding standard files (#83) Pull requests to fix warnings on Windows Includes #75, #76 and #77. Also includes other changes related to - loading and storing of atomic variables - fix for UTF-8 character representation on Windows Update sample documentation with shadow client token limitation Make Shadow::HandleGetResponse call response handler on Rejected response Fixes #86 Make Shadow::HandleGetResponse call response handler on malformed payload --- CMakeLists.txt | 2 + README.md | 3 + include/ResponseCode.hpp | 7 +- include/jobs/Jobs.hpp | 219 +++++++++++++ network/WebSocket/WebSocketConnection.cpp | 58 +++- network/WebSocket/WebSocketConnection.hpp | 29 ++ samples/Jobs/CMakeLists.txt | 82 +++++ samples/Jobs/JobsSample.cpp | 383 ++++++++++++++++++++++ samples/Jobs/JobsSample.hpp | 68 ++++ samples/README.md | 8 +- src/ResponseCode.cpp | 3 + src/jobs/Jobs.cpp | 340 +++++++++++++++++++ tests/integration/include/JobsTest.hpp | 59 ++++ tests/integration/src/IntegTestRunner.cpp | 12 + tests/integration/src/JobsTest.cpp | 327 ++++++++++++++++++ tests/unit/src/ResponseCodeTests.cpp | 5 + tests/unit/src/jobs/JobsTests.cpp | 264 +++++++++++++++ 17 files changed, 1855 insertions(+), 14 deletions(-) create mode 100644 include/jobs/Jobs.hpp create mode 100644 samples/Jobs/CMakeLists.txt create mode 100644 samples/Jobs/JobsSample.cpp create mode 100644 samples/Jobs/JobsSample.hpp create mode 100644 src/jobs/Jobs.cpp create mode 100644 tests/integration/include/JobsTest.hpp create mode 100644 tests/integration/src/JobsTest.cpp create mode 100644 tests/unit/src/jobs/JobsTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b2ce97a..b09e572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,6 +162,8 @@ add_subdirectory(tests/unit) add_subdirectory(samples/PubSub) +add_subdirectory(samples/Jobs) + add_subdirectory(samples/ShadowDelta) add_subdirectory(samples/Discovery EXCLUDE_FROM_ALL) diff --git a/README.md b/README.md index cc72cb3..89fe3be 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ The Device SDK provides functionality to create and maintain a MQTT Connection. ### Thing Shadow This SDK implements the specific protocol for Thing Shadows to retrieve, update and delete Thing Shadows adhering to the protocol that is implemented to ensure correct versioning and support for client tokens. It abstracts the necessary MQTT topic subscriptions by automatically subscribing to and unsubscribing from the reserved topics as needed for each API call. Inbound state change requests are automatically signalled via a configurable callback. +### Jobs +This SDK also implements the Jobs protocol to interact with the AWS IoT Jobs service. The IoT Job service manages deployment of IoT fleet wide tasks such as device software/firmware deployments and updates, rotation of security certificates, device reboots, and custom device specific management tasks. For additional information please see the [Jobs developer guide](https://docs.aws.amazon.com/iot/latest/developerguide/iot-jobs.html). + ## Design Goals of this SDK The C++ SDK was specifically designed for devices that are not resource constrained and required advanced features such as Message queueing, multi-threading support and the latest language features diff --git a/include/ResponseCode.hpp b/include/ResponseCode.hpp index 0a2e58c..2bfb7fa 100644 --- a/include/ResponseCode.hpp +++ b/include/ResponseCode.hpp @@ -193,7 +193,11 @@ namespace awsiotsdk { // Discovery Response Parsing Error Codes - DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR = -1200 ///< Discover Response Json is missing expected keys + DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR = -1200, ///< Discover Response Json is missing expected keys + + // Jobs Error Codes + + JOBS_INVALID_TOPIC_ERROR = -1300 ///< Jobs invalid topic }; /** @@ -314,6 +318,7 @@ namespace awsiotsdk { const util::String DISCOVER_ACTION_SERVER_ERROR_STRING("Server returned unknown error while performing the discovery action"); const util::String DISCOVER_ACTION_REQUEST_OVERLOAD_STRING("The discovery action is overloading the server, try again after some time"); const util::String DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING("The discover response JSON is incomplete "); + const util::String JOBS_INVALID_TOPIC_ERROR_STRING("Invalid jobs topic"); /** * Takes in a Response Code and returns the appropriate error/success string diff --git a/include/jobs/Jobs.hpp b/include/jobs/Jobs.hpp new file mode 100644 index 0000000..33c2130 --- /dev/null +++ b/include/jobs/Jobs.hpp @@ -0,0 +1,219 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.hpp + * @brief + * + */ + +#pragma once + +#include "mqtt/Client.hpp" + +namespace awsiotsdk { + class Jobs { + public: + // Disabling default and copy constructors. + Jobs() = delete; // Delete Default constructor + Jobs(const Jobs &) = delete; // Delete Copy constructor + Jobs(Jobs &&) = default; // Default Move constructor + Jobs &operator=(const Jobs &) & = delete; // Delete Copy assignment operator + Jobs &operator=(Jobs &&) & = default; // Default Move assignment operator + + /** + * @brief Create factory method. Returns a unique instance of Jobs + * + * @param p_mqtt_client - mqtt client + * @param qos - QoS + * @param thing_name - Thing name + * @param client_token - Client token for correlating messages (optional) + * + * @return std::unique_ptr pointing to a unique Jobs instance + */ + static std::unique_ptr Create(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token = util::String()); + + enum JobExecutionTopicType { + JOB_UNRECOGNIZED_TOPIC = 0, + JOB_GET_PENDING_TOPIC, + JOB_START_NEXT_TOPIC, + JOB_DESCRIBE_TOPIC, + JOB_UPDATE_TOPIC, + JOB_NOTIFY_TOPIC, + JOB_NOTIFY_NEXT_TOPIC, + JOB_WILDCARD_TOPIC + }; + + enum JobExecutionTopicReplyType { + JOB_UNRECOGNIZED_TOPIC_TYPE = 0, + JOB_REQUEST_TYPE, + JOB_ACCEPTED_REPLY_TYPE, + JOB_REJECTED_REPLY_TYPE, + JOB_WILDCARD_REPLY_TYPE + }; + + enum JobExecutionStatus { + JOB_EXECUTION_STATUS_NOT_SET = 0, + JOB_EXECUTION_QUEUED, + JOB_EXECUTION_IN_PROGRESS, + JOB_EXECUTION_FAILED, + JOB_EXECUTION_SUCCEEDED, + JOB_EXECUTION_CANCELED, + JOB_EXECUTION_REJECTED, + /*** + * Used for any status not in the supported list of statuses + */ + JOB_EXECUTION_UNKNOWN_STATUS = 99 + }; + + /** + * @brief GetJobTopic + * + * This function creates a job topic based on the provided parameters. + * + * @param topicType - Jobs topic type + * @param replyType - Topic reply type (optional) + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * + * @return nullptr on error, unique_ptr pointing to a topic string if successful + */ + std::unique_ptr GetJobTopic(JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType = JOB_REQUEST_TYPE, + const util::String &jobId = util::String()); + + /** + * @brief SendJobsQuery + * + * Send a query to the Jobs service using the provided mqtt client + * + * @param topicType - Jobs topic type for type of query + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsQuery(JobExecutionTopicType topicType, + const util::String &jobId = util::String()); + + /** + * @brief SendJobsStartNext + * + * Call Jobs start-next API to start the next pending job execution and trigger response + * + * @param statusDetails - Status details to be associated with started job execution (optional) + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsStartNext(const util::Map &statusDetailsMap = util::Map()); + + /** + * @brief SendJobsDescribe + * + * Send request for job execution details + * + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also + * be omitted to request all pending and in progress job executions + * @param executionNumber - Specific execution number to describe, omit to match latest + * @param includeJobDocument - Flag to indicate whether response should include job document + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsDescribe(const util::String &jobId = util::String(), + int64_t executionNumber = 0, // set to 0 to ignore + bool includeJobDocument = true); + + /** + * @brief SendJobsUpdate + * + * Send update for specified job + * + * @param jobId - Job id associated with job execution to be updated + * @param status - New job execution status + * @param statusDetailsMap - Status details to be associated with job execution (optional) + * @param expectedVersion - Optional expected current job execution number, error response if mismatched + * @param executionNumber - Specific execution number to update, omit to match latest + * @param includeJobExecutionState - Include job execution state in response (optional) + * @param includeJobDocument - Include job document in response (optional) + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsUpdate(const util::String &jobId, + JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, // set to 0 to ignore + int64_t executionNumber = 0, // set to 0 to ignore + bool includeJobExecutionState = false, + bool includeJobDocument = false); + + /** + * @brief CreateJobsSubscription + * + * Create a Jobs Subscription instance + * + * @param p_app_handler - Application Handler instance + * @param p_app_handler_data - Data to be passed to application handler. Can be nullptr + * @param topicType - Jobs topic type to subscribe to (defaults to JOB_WILDCARD_TOPIC) + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * @param replyType - Topic reply type (optional, defaults to JOB_REQUEST_TYPE which omits the reply type in the subscription) + * + * @return shared_ptr Subscription instance + */ + std::shared_ptr CreateJobsSubscription(mqtt::Subscription::ApplicationCallbackHandlerPtr p_app_handler, + std::shared_ptr p_app_handler_data, + JobExecutionTopicType topicType = JOB_WILDCARD_TOPIC, + JobExecutionTopicReplyType replyType = JOB_REQUEST_TYPE, + const util::String &jobId = util::String()); + protected: + std::shared_ptr p_mqtt_client_; + mqtt::QoS qos_; + util::String thing_name_; + util::String client_token_; + + /** + * @brief Jobs constructor + * + * Create Jobs object storing given parameters in created instance + * + * @param p_mqtt_client - mqtt client + * @param qos - QoS + * @param thing_name - Thing name + * @param client_token - Client token for correlating messages (optional) + */ + Jobs(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token); + + static bool BaseTopicRequiresJobId(JobExecutionTopicType topicType); + static const util::String GetOperationForBaseTopic(JobExecutionTopicType topicType); + static const util::String GetSuffixForTopicType(JobExecutionTopicReplyType replyType); + static const util::String GetExecutionStatus(JobExecutionStatus status); + static util::String Escape(const util::String &value); + static util::String SerializeStatusDetails(const util::Map &statusDetailsMap); + + util::String SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, + int64_t executionNumber = 0, + bool includeJobExecutionState = false, + bool includeJobDocument = false); + util::String SerializeDescribeJobExecutionPayload(int64_t executionNumber = 0, + bool includeJobDocument = true); + util::String SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap = util::Map()); + util::String SerializeClientTokenPayload(); + }; +} diff --git a/network/WebSocket/WebSocketConnection.cpp b/network/WebSocket/WebSocketConnection.cpp index 847638b..9214ffd 100644 --- a/network/WebSocket/WebSocketConnection.cpp +++ b/network/WebSocket/WebSocketConnection.cpp @@ -47,6 +47,8 @@ #define X_AMZ_DATE "X-Amz-Date" #define X_AMZ_EXPIRES "X-Amz-Expires" #define X_AMZ_SECURITY_TOKEN "X-Amz-Security-Token" +#define X_AMZ_CUSTOMAUTHORIZER_NAME "X-Amz-CustomAuthorizer-Name" +#define X_AMZ_CUSTOMAUTHORIZER_SIGNATURE "X-Amz-CustomAuthorizer-Signature" #define SIGNING_KEY "AWS4" #define LONG_DATE_FORMAT_STR "%Y%m%dT%H%M%SZ" #define SIMPLE_DATE_FORMAT_STR "%Y%m%d" @@ -99,6 +101,11 @@ namespace awsiotsdk { bool server_verification_flag) : openssl_connection_(endpoint, endpoint_port, root_ca_location, tls_handshake_timeout, tls_read_timeout, tls_write_timeout, server_verification_flag) { + custom_authorizer_name_.clear(); + custom_authorizer_signature_.clear(); + custom_authorizer_token_name_.clear(); + custom_authorizer_token_.clear(); + endpoint_ = endpoint; endpoint_port_ = endpoint_port; root_ca_location_ = root_ca_location; @@ -125,6 +132,21 @@ namespace awsiotsdk { wss_frame_write_ = std::unique_ptr(new wslay_frame_iocb()); } + WebSocketConnection::WebSocketConnection(util::String endpoint, uint16_t endpoint_port, util::String root_ca_location, + std::chrono::milliseconds tls_handshake_timeout, + std::chrono::milliseconds tls_read_timeout, + std::chrono::milliseconds tls_write_timeout, + util::String custom_authorizer_name, util::String custom_authorizer_signature, + util::String custom_authorizer_token_name, util::String custom_authorizer_token, + bool server_verification_flag) + : WebSocketConnection(endpoint, endpoint_port, root_ca_location, "", "", "", "", tls_handshake_timeout, tls_read_timeout, + tls_write_timeout, false) { + custom_authorizer_name_ = custom_authorizer_name; + custom_authorizer_signature_ = custom_authorizer_signature; + custom_authorizer_token_name_ = custom_authorizer_token_name; + custom_authorizer_token_ = custom_authorizer_token; + } + ResponseCode WebSocketConnection::ConnectInternal() { // Init Tls ResponseCode rc = openssl_connection_.Initialize(); @@ -563,17 +585,12 @@ namespace awsiotsdk { } ResponseCode WebSocketConnection::WssHandshake() { + ResponseCode rc; + util::OStringStream stringStream; + // Assuming: // 1. Ssl socket is ready to do read/write. - // Create canonical query string - util::String canonical_query_string; - canonical_query_string.reserve(CANONICAL_QUERY_BUF_LEN); - ResponseCode rc = InitializeCanonicalQueryString(canonical_query_string); - if (ResponseCode::SUCCESS != rc) { - return rc; - } - // Create Wss handshake Http request // -> Generate Wss client key char client_key_buf[WSS_CLIENT_KEY_MAX_LEN + 1]; @@ -583,15 +600,32 @@ namespace awsiotsdk { return rc; } - // -> Assemble Wss Http request - util::OStringStream stringStream; - stringStream << "GET /mqtt?" << canonical_query_string << " " << HTTP_1_1 << "\r\n" - << "Host: " << endpoint_ << "\r\n" + if (custom_authorizer_name_.empty()) { + // Create canonical query string + util::String canonical_query_string; + canonical_query_string.reserve(CANONICAL_QUERY_BUF_LEN); + rc = InitializeCanonicalQueryString(canonical_query_string); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + + // -> Assemble Wss Http request + stringStream << "GET /mqtt?" << canonical_query_string << " " << HTTP_1_1 << "\r\n"; + } else { + // -> Assemble Wss Http request + stringStream << "GET /mqtt " << HTTP_1_1 << "\r\n" + << X_AMZ_CUSTOMAUTHORIZER_NAME << ": " << custom_authorizer_name_ << "\r\n" + << X_AMZ_CUSTOMAUTHORIZER_SIGNATURE << ": " << custom_authorizer_signature_ << "\r\n" + << custom_authorizer_token_name_ << ": " << custom_authorizer_token_ << "\r\n"; + } + + stringStream << "Host: " << endpoint_ << "\r\n" << "Connection: " << UPGRADE << "\r\n" << "Upgrade: " << WEBSOCKET << "\r\n" << "Sec-WebSocket-Version: " << SEC_WEBSOCKET_VERSION_13 << "\r\n" << "sec-websocket-key: " << client_key_buf << "\r\n" << "Sec-WebSocket-Protocol: " << MQTT_PROTOCOL << "\r\n\r\n"; + util::String request_string = stringStream.str(); // Send out request diff --git a/network/WebSocket/WebSocketConnection.hpp b/network/WebSocket/WebSocketConnection.hpp index 761c8cc..fa8a491 100644 --- a/network/WebSocket/WebSocketConnection.hpp +++ b/network/WebSocket/WebSocketConnection.hpp @@ -50,6 +50,10 @@ namespace awsiotsdk { util::String aws_access_key_id_; ///< Pointer to string containing the AWS Access Key Id. util::String aws_secret_access_key_; ///< Pointer to sstring containing the AWS Secret Access Key. util::String aws_session_token_; ///< Pointer to string containing the AWS Session Token. + util::String custom_authorizer_name_; ///< Pointer to string containing the custom authorizer name. + util::String custom_authorizer_signature_; ///< Pointer to string containing the authorizer signature. + util::String custom_authorizer_token_name_; ///< Pointer to string containing the authorizer token name. + util::String custom_authorizer_token_; ///< Pointer to string containing the authorizer token. util::String aws_region_; ///< Region for this connection util::String endpoint_; ///< Endpoint for this connection uint16_t endpoint_port_; ///< Endpoint port @@ -210,6 +214,31 @@ namespace awsiotsdk { std::chrono::milliseconds tls_read_timeout, std::chrono::milliseconds tls_write_timeout, bool server_verification_flag); + /** + * @brief Constructor for the WebSocket for MQTT implementation using custom authentication + * + * Performs any initialization required by the WebSocket layer. + * + * @param util::String endpoint - The target endpoint to connect to + * @param uint16_t endpoint_port - The port on the target to connect to + * @param util::String root_ca_location - Path of the location of the Root CA + * @param std::chrono::milliseconds tls_handshake_timeout - The value to use for timeout of handshake operation + * @param std::chrono::milliseconds tls_read_timeout - The value to use for timeout of read operation + * @param std::chrono::milliseconds tls_write_timeout - The value to use for timeout of write operation + * @param util::String custom_authorizer_name - Name of the authorizer function + * @param util::String custom_authorizer_signature - Authorizer signature + * @param util::String custom_authorizer_token_name - Authorizer token name + * @param util::String custom_authorizer_token - Authorizer token + * @param bool server_verification_flag - used to decide whether server verification is needed or not + * + */ + WebSocketConnection(util::String endpoint, uint16_t endpoint_port, util::String root_ca_location, + std::chrono::milliseconds tls_handshake_timeout, + std::chrono::milliseconds tls_read_timeout, std::chrono::milliseconds tls_write_timeout, + util::String custom_authorizer_name, util::String custom_authorizer_signature, + util::String custom_authorizer_token_name, util::String custom_authorizer_token, + bool server_verification_flag); + /** * @brief Check if WebSocket layer is still connected * diff --git a/samples/Jobs/CMakeLists.txt b/samples/Jobs/CMakeLists.txt new file mode 100644 index 0000000..b11e1c8 --- /dev/null +++ b/samples/Jobs/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.2 FATAL_ERROR) +project(aws-iot-cpp-samples CXX) + +###################################### +# Section : Disable in-source builds # +###################################### + +if (${PROJECT_SOURCE_DIR} STREQUAL ${PROJECT_BINARY_DIR}) + message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt and CMakeFiles folder.") +endif () + +######################################## +# Section : Common Build setttings # +######################################## +# Set required compiler standard to standard c++11. Disable extensions. +set(CMAKE_CXX_STANDARD 11) # C++11... +set(CMAKE_CXX_STANDARD_REQUIRED ON) #...is required... +set(CMAKE_CXX_EXTENSIONS OFF) #...without compiler extensions like gnu++11 + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/archive) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +# Configure Compiler flags +if (UNIX AND NOT APPLE) + # Prefer pthread if found + set(THREADS_PREFER_PTHREAD_FLAG ON) + set(CUSTOM_COMPILER_FLAGS "-fno-exceptions -Wall -Werror") +elseif (APPLE) + set(CUSTOM_COMPILER_FLAGS "-fno-exceptions -Wall -Werror") +elseif (WIN32) + set(CUSTOM_COMPILER_FLAGS "/W4") +endif () + +################################ +# Target : Build Jobs sample # +################################ +set(JOBS_SAMPLE_TARGET_NAME jobs-sample) +# Add Target +add_executable(${JOBS_SAMPLE_TARGET_NAME} "${PROJECT_SOURCE_DIR}/JobsSample.cpp;${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.cpp") + +# Add Target specific includes +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../common) +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}) + +# Configure Threading library +find_package(Threads REQUIRED) + +# Add SDK includes +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${CMAKE_BINARY_DIR}/${DEPENDENCY_DIR}/rapidjson/src/include) +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../include) + +target_link_libraries(${JOBS_SAMPLE_TARGET_NAME} PUBLIC "Threads::Threads") +target_link_libraries(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${SDK_TARGET_NAME}) + +# Copy Json config file +add_custom_command(TARGET ${JOBS_SAMPLE_TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy ${PROJECT_SOURCE_DIR}/../../common/SampleConfig.json $/config/SampleConfig.json) +set_property(TARGET ${JOBS_SAMPLE_TARGET_NAME} APPEND_STRING PROPERTY COMPILE_FLAGS ${CUSTOM_COMPILER_FLAGS}) + +# Gather list of all .cert files in "/cert" +add_custom_command(TARGET ${JOBS_SAMPLE_TARGET_NAME} PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy_directory ${PROJECT_SOURCE_DIR}/../../certs $/certs) + +if (MSVC) + target_sources(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.hpp) + source_group("Header Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.hpp) + source_group("Source Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.cpp) + + target_sources(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/JobsSample.hpp) + source_group("Header Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/JobsSample.hpp) + source_group("Source Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/JobsSample.cpp) +endif () + +######################### +# Add Network libraries # +######################### + +set(NETWORK_WRAPPER_DEST_TARGET ${JOBS_SAMPLE_TARGET_NAME}) +include(${PROJECT_SOURCE_DIR}/../../network/CMakeLists.txt.in) diff --git a/samples/Jobs/JobsSample.cpp b/samples/Jobs/JobsSample.cpp new file mode 100644 index 0000000..a1f032c --- /dev/null +++ b/samples/Jobs/JobsSample.cpp @@ -0,0 +1,383 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsSample.cpp + * + * This example takes the parameters from the config/SampleConfig.json file and establishes + * a connection to the AWS IoT MQTT Platform. It performs several operations to + * demonstrate the basic capabilities of the AWS IoT Jobs platform. + * + * If all the certs are correct, you should see the list of pending Job Executions + * printed out by the GetPendingCallback callback. If there are any existing pending + * job executions each will be processed one at a time in the NextJobCallback callback. + * After all of the pending jobs have been processed the program will wait for + * notifications for new pending jobs and process them one at a time as they come in. + * + * In the Subscribe function you can see how each callback is registered for each corresponding + * Jobs topic. + * + */ + +#include +#include + +#ifdef USE_WEBSOCKETS +#include "WebSocketConnection.hpp" +#elif defined USE_MBEDTLS +#include "MbedTLSConnection.hpp" +#else +#include "OpenSSLConnection.hpp" +#endif + +#include "util/logging/Logging.hpp" +#include "util/logging/LogMacros.hpp" +#include "util/logging/ConsoleLogSystem.hpp" + +#include "ConfigCommon.hpp" +#include "jobs/Jobs.hpp" +#include "JobsSample.hpp" + +#define LOG_TAG_JOBS "[Sample - Jobs]" + +namespace awsiotsdk { + namespace samples { + ResponseCode JobsSample::GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "GetPendingCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Json Parse for GetPendingCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("inProgressJobs")) { + std::cout << "inProgressJobs : " << util::JsonParser::ToString(doc["inProgressJobs"]) << std::endl; + } + + if (doc.HasMember("queuedJobs")) { + std::cout << "queuedJobs : " << util::JsonParser::ToString(doc["queuedJobs"]) << std::endl; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "NextJobCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Json Parse for NextJobCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("execution")) { + std::cout << "execution : " << util::JsonParser::ToString(doc["execution"]) << std::endl; + + if (doc["execution"].HasMember("jobId")) { + util::Map statusDetailsMap; + + util::String jobId = doc["execution"]["jobId"].GetString(); + std::cout << "jobId : " << jobId << std::endl; + + if (doc["execution"].HasMember("jobDocument")) { + std::cout << "jobDocument : " << util::JsonParser::ToString(doc["execution"]["jobDocument"]) << std::endl; + statusDetailsMap.insert(std::make_pair("exampleDetail", "a value appropriate for your successful job")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_SUCCEEDED, statusDetailsMap); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "SendJobsUpdate failed. %s", ResponseHelper::ToString(rc).c_str()); + return rc; + } + } else { + statusDetailsMap.insert(std::make_pair("failureDetail", "Unable to process job document")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_FAILED, statusDetailsMap); + } + } + } else { + std::cout << "No job execution description found, nothing to do." << std::endl; + done_ = true; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::UpdateAcceptedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + std::cout << std::endl << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::UpdateRejectedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + std::cout << std::endl << "************" << std::endl; + + /* Do error handling here for when the update was rejected */ + + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::DisconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data) { + std::cout << "*******************************************" << std::endl + << client_id << " Disconnected!" << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::ReconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode reconnect_result) { + std::cout << "*******************************************" << std::endl + << client_id << " Reconnect Attempted. Result " << ResponseHelper::ToString(reconnect_result) + << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::ResubscribeCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode resubscribe_result) { + std::cout << "*******************************************" << std::endl + << client_id << " Resubscribe Attempted. Result" << ResponseHelper::ToString(resubscribe_result) + << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + + ResponseCode JobsSample::Subscribe() { + std::cout << "******** Subscribe ***************" << std::endl; + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_pending_handler = + std::bind(&JobsSample::GetPendingCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_next_handler = + std::bind(&JobsSample::NextJobCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_update_accepted_handler = + std::bind(&JobsSample::UpdateAcceptedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_update_rejected_handler = + std::bind(&JobsSample::UpdateRejectedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + util::Vector> topic_vector; + std::shared_ptr p_subscription; + + p_subscription = p_jobs_->CreateJobsSubscription(p_pending_handler, nullptr, Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_update_accepted_handler, nullptr, Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "+"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_update_rejected_handler, nullptr, Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, "+"); + topic_vector.push_back(p_subscription); + + ResponseCode rc = p_iot_client_->Subscribe(topic_vector, ConfigCommon::mqtt_command_timeout_); + return rc; + } + + ResponseCode JobsSample::InitializeTLS() { + ResponseCode rc = ResponseCode::SUCCESS; + +#ifdef USE_WEBSOCKETS + p_network_connection_ = std::shared_ptr( + new network::WebSocketConnection(ConfigCommon::endpoint_, ConfigCommon::endpoint_https_port_, + ConfigCommon::root_ca_path_, ConfigCommon::aws_region_, + ConfigCommon::aws_access_key_id_, + ConfigCommon::aws_secret_access_key_, + ConfigCommon::aws_session_token_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true)); + if (nullptr == p_network_connection_) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } +#elif defined USE_MBEDTLS + p_network_connection_ = std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, + true); + if (nullptr == p_network_connection_) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } +#else + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + rc = p_network_connection->Initialize(); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, + "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#endif + return rc; + } + + ResponseCode JobsSample::RunSample() { + done_ = false; + + ResponseCode rc = InitializeTLS(); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + + ClientCoreState::ApplicationDisconnectCallbackPtr p_disconnect_handler = + std::bind(&JobsSample::DisconnectCallback, this, std::placeholders::_1, std::placeholders::_2); + + ClientCoreState::ApplicationReconnectCallbackPtr p_reconnect_handler = + std::bind(&JobsSample::ReconnectCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + ClientCoreState::ApplicationResubscribeCallbackPtr p_resubscribe_handler = + std::bind(&JobsSample::ResubscribeCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + p_iot_client_ = std::shared_ptr(MqttClient::Create(p_network_connection_, + ConfigCommon::mqtt_command_timeout_, + p_disconnect_handler, nullptr, + p_reconnect_handler, nullptr, + p_resubscribe_handler, nullptr)); + if (nullptr == p_iot_client_) { + return ResponseCode::FAILURE; + } + + util::String client_id_tagged = ConfigCommon::base_client_id_; + client_id_tagged.append("_jobs_sample_"); + client_id_tagged.append(std::to_string(rand())); + std::unique_ptr client_id = Utf8String::Create(client_id_tagged); + + rc = p_iot_client_->Connect(ConfigCommon::mqtt_command_timeout_, ConfigCommon::is_clean_session_, + mqtt::Version::MQTT_3_1_1, ConfigCommon::keep_alive_timeout_secs_, + std::move(client_id), nullptr, nullptr, nullptr); + if (ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED != rc) { + return rc; + } + + p_jobs_ = Jobs::Create(p_iot_client_, mqtt::QoS::QOS1, ConfigCommon::thing_name_, client_id_tagged); + + rc = Subscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Subscribe failed. %s", ResponseHelper::ToString(rc).c_str()); + } else { + rc = p_jobs_->SendJobsQuery(Jobs::JOB_GET_PENDING_TOPIC); + + if (ResponseCode::SUCCESS == rc) { + rc = p_jobs_->SendJobsQuery(Jobs::JOB_DESCRIBE_TOPIC, "$next"); + } + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + } + } + + // Wait for job processing to complete + while (!done_) { + done_ = true; + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + + rc = p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Disconnect failed. %s", ResponseHelper::ToString(rc).c_str()); + } + + std::cout << "Exiting Sample!!!!" << std::endl; + return ResponseCode::SUCCESS; + } + } +} + +int main(int argc, char **argv) { + std::shared_ptr p_log_system = + std::make_shared(awsiotsdk::util::Logging::LogLevel::Info); + awsiotsdk::util::Logging::InitializeAWSLogging(p_log_system); + + std::unique_ptr + jobs_sample = std::unique_ptr(new awsiotsdk::samples::JobsSample()); + + awsiotsdk::ResponseCode rc = awsiotsdk::ConfigCommon::InitializeCommon("config/SampleConfig.json"); + if (awsiotsdk::ResponseCode::SUCCESS == rc) { + rc = jobs_sample->RunSample(); + } +#ifdef WIN32 + std::cout<<"Press any key to continue!!!!"<(rc); +} diff --git a/samples/Jobs/JobsSample.hpp b/samples/Jobs/JobsSample.hpp new file mode 100644 index 0000000..8080b61 --- /dev/null +++ b/samples/Jobs/JobsSample.hpp @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsSample.hpp + * @brief + * + */ + + +#pragma once + +#include "mqtt/Client.hpp" +#include "NetworkConnection.hpp" + +namespace awsiotsdk { + namespace samples { + class JobsSample { + protected: + std::shared_ptr p_network_connection_; + std::shared_ptr p_iot_client_; + std::shared_ptr p_jobs_; + std::atomic done_; + + ResponseCode DisconnectCallback(util::String topic_name, + std::shared_ptr p_app_handler_data); + ResponseCode ReconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode reconnect_result); + ResponseCode ResubscribeCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode resubscribe_result); + + ResponseCode GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode UpdateAcceptedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode UpdateRejectedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + + ResponseCode Subscribe(); + ResponseCode InitializeTLS(); + + public: + ResponseCode RunSample(); + }; + } +} + + diff --git a/samples/README.md b/samples/README.md index 3be0c30..00fdfb5 100644 --- a/samples/README.md +++ b/samples/README.md @@ -17,9 +17,15 @@ This sample demonstrates how various Shadow operations can be performed. * Code for this sample is located [here](./ShadowDelta) * Target for this sample is `shadow-delta-sample` - + Note: The shadow client token is set as the thing name by default in the sample. The shadow client token is limited to 64 bytes and will return an error if a token longer than 64 bytes is used (`"code":400,"message":"invalid client token"`, although receiving a 400 does not necessarily mean that it is due to the length of the client token). Modify the code [here](../ShadowDelta/ShadowDelta.cpp#L184) if your thing name is longer than 64 bytes to prevent this error. +### Jobs Sample +This sample demonstrates how various Jobs API operations can be performed including subscribing to Jobs notifications and publishing Job execution updates. + + * Code for this sample is located [here](./Jobs) + * Target for this sample is `jobs-sample` + ### Discovery Sample This sample demonstrates how the discovery operation can be performed to get the connectivity information to connect to a Greengrass Core (GGC). The configuration for this example is slightly different as the Discovery operation is a HTTP call, and uses port 8443, instead of port 8883 which is used for MQTT operations. The endpoint is the same IoT host endpoint used to connect the IoT thing to the cloud. diff --git a/src/ResponseCode.cpp b/src/ResponseCode.cpp index 2f3e21b..fad8420 100644 --- a/src/ResponseCode.cpp +++ b/src/ResponseCode.cpp @@ -343,6 +343,9 @@ namespace awsiotsdk { case ResponseCode::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR: os << awsiotsdk::ResponseHelper::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING; break; + case ResponseCode::JOBS_INVALID_TOPIC_ERROR: + os << awsiotsdk::ResponseHelper::JOBS_INVALID_TOPIC_ERROR_STRING; + break; } os << " : SDK Code " << static_cast(rc) << "."; return os; diff --git a/src/jobs/Jobs.cpp b/src/jobs/Jobs.cpp new file mode 100644 index 0000000..902ec91 --- /dev/null +++ b/src/jobs/Jobs.cpp @@ -0,0 +1,340 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.cpp + * @brief + * + */ + +#include "util/logging/LogMacros.hpp" + +#include "jobs/Jobs.hpp" + +#define BASE_THINGS_TOPIC "$aws/things/" + +#define NOTIFY_OPERATION "notify" +#define NOTIFY_NEXT_OPERATION "notify-next" +#define GET_OPERATION "get" +#define START_NEXT_OPERATION "start-next" +#define WILDCARD_OPERATION "+" +#define UPDATE_OPERATION "update" +#define ACCEPTED_REPLY "accepted" +#define REJECTED_REPLY "rejected" +#define WILDCARD_REPLY "#" + +namespace awsiotsdk { + std::unique_ptr Jobs::Create(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token) { + if (nullptr == p_mqtt_client) { + return nullptr; + } + + return std::unique_ptr(new Jobs(p_mqtt_client, qos, thing_name, client_token)); + } + + Jobs::Jobs(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token) { + p_mqtt_client_ = p_mqtt_client; + qos_ = qos; + thing_name_ = thing_name; + client_token_ = client_token; + }; + + bool Jobs::BaseTopicRequiresJobId(JobExecutionTopicType topicType) { + switch (topicType) { + case JOB_UPDATE_TOPIC: + case JOB_DESCRIBE_TOPIC: + return true; + case JOB_NOTIFY_TOPIC: + case JOB_NOTIFY_NEXT_TOPIC: + case JOB_START_NEXT_TOPIC: + case JOB_GET_PENDING_TOPIC: + case JOB_WILDCARD_TOPIC: + case JOB_UNRECOGNIZED_TOPIC: + default: + return false; + } + }; + + const util::String Jobs::GetOperationForBaseTopic(JobExecutionTopicType topicType) { + switch (topicType) { + case JOB_UPDATE_TOPIC: + return UPDATE_OPERATION; + case JOB_NOTIFY_TOPIC: + return NOTIFY_OPERATION; + case JOB_NOTIFY_NEXT_TOPIC: + return NOTIFY_NEXT_OPERATION; + case JOB_GET_PENDING_TOPIC: + case JOB_DESCRIBE_TOPIC: + return GET_OPERATION; + case JOB_START_NEXT_TOPIC: + return START_NEXT_OPERATION; + case JOB_WILDCARD_TOPIC: + return WILDCARD_OPERATION; + case JOB_UNRECOGNIZED_TOPIC: + default: + return ""; + } + }; + + const util::String Jobs::GetSuffixForTopicType(JobExecutionTopicReplyType replyType) { + switch (replyType) { + case JOB_REQUEST_TYPE: + return ""; + case JOB_ACCEPTED_REPLY_TYPE: + return "/" ACCEPTED_REPLY; + case JOB_REJECTED_REPLY_TYPE: + return "/" REJECTED_REPLY; + case JOB_WILDCARD_REPLY_TYPE: + return "/" WILDCARD_REPLY; + case JOB_UNRECOGNIZED_TOPIC_TYPE: + default: + return ""; + } + } + + const util::String Jobs::GetExecutionStatus(JobExecutionStatus status) { + switch (status) { + case JOB_EXECUTION_QUEUED: + return "QUEUED"; + case JOB_EXECUTION_IN_PROGRESS: + return "IN_PROGRESS"; + case JOB_EXECUTION_FAILED: + return "FAILED"; + case JOB_EXECUTION_SUCCEEDED: + return "SUCCEEDED"; + case JOB_EXECUTION_CANCELED: + return "CANCELED"; + case JOB_EXECUTION_REJECTED: + return "REJECTED"; + case JOB_EXECUTION_STATUS_NOT_SET: + case JOB_EXECUTION_UNKNOWN_STATUS: + default: + return ""; + } + } + + util::String Jobs::Escape(const util::String &value) { + util::String result = ""; + + for (int i = 0; i < value.length(); i++) { + switch(value[i]) { + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + case '"': result += "\\\""; break; + case '\\': result += "\\\\"; break; + default: result += value[i]; + } + } + return result; + } + + util::String Jobs::SerializeStatusDetails(const util::Map &statusDetailsMap) { + util::String result = "{"; + + util::Map::const_iterator itr = statusDetailsMap.begin(); + while (itr != statusDetailsMap.end()) { + result += (itr == statusDetailsMap.begin() ? "\"" : ",\""); + result += Escape(itr->first) + "\":\"" + Escape(itr->second) + "\""; + itr++; + } + + result += '}'; + return result; + } + + std::unique_ptr Jobs::GetJobTopic(JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType, + const util::String &jobId) { + if (thing_name_.empty()) { + return nullptr; + } + + if ((topicType == JOB_NOTIFY_TOPIC || topicType == JOB_NOTIFY_NEXT_TOPIC) && replyType != JOB_REQUEST_TYPE) { + return nullptr; + } + + if ((topicType == JOB_GET_PENDING_TOPIC || topicType == JOB_START_NEXT_TOPIC || + topicType == JOB_NOTIFY_TOPIC || topicType == JOB_NOTIFY_NEXT_TOPIC) && !jobId.empty()) { + return nullptr; + } + + const bool requireJobId = BaseTopicRequiresJobId(topicType); + if (jobId.empty() && requireJobId) { + return nullptr; + } + + const util::String operation = GetOperationForBaseTopic(topicType); + if (operation.empty()) { + return nullptr; + } + + const util::String suffix = GetSuffixForTopicType(replyType); + + if (requireJobId) { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/" + jobId + '/' + operation + suffix); + } else if (topicType == JOB_WILDCARD_TOPIC) { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/#"); + } else { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/" + operation + suffix); + } + }; + + util::String Jobs::SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap, + int64_t expectedVersion, // set to 0 to ignore + int64_t executionNumber, // set to 0 to ignore + bool includeJobExecutionState, + bool includeJobDocument) { + const util::String executionStatus = GetExecutionStatus(status); + + if (executionStatus.empty()) { + return ""; + } + + util::String result = "{\"status\":\"" + executionStatus + "\""; + if (!statusDetailsMap.empty()) { + result += ",\"statusDetails\":" + SerializeStatusDetails(statusDetailsMap); + } + if (expectedVersion > 0) { + result += ",\"expectedVersion\":\"" + std::to_string(expectedVersion) + "\""; + } + if (executionNumber > 0) { + result += ",\"executionNumber\":\"" + std::to_string(executionNumber) + "\""; + } + if (includeJobExecutionState) { + result += ",\"includeJobExecutionState\":\"true\""; + } + if (includeJobDocument) { + result += ",\"includeJobDocument\":\"true\""; + } + if (!client_token_.empty()) { + result += ",\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeDescribeJobExecutionPayload(int64_t executionNumber, // set to 0 to ignore + bool includeJobDocument) { + util::String result = "{\"includeJobDocument\":\""; + result += (includeJobDocument ? "true" : "false"); + result += "\""; + if (executionNumber > 0) { + result += ",\"executionNumber\":\"" + std::to_string(executionNumber) + "\""; + } + if (!client_token_.empty()) { + result += "\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap) { + util::String result = "{"; + if (!statusDetailsMap.empty()) { + result += "\"statusDetails\":" + SerializeStatusDetails(statusDetailsMap); + } + if (!client_token_.empty()) { + if (!statusDetailsMap.empty()) { + result += ','; + } + result += "\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeClientTokenPayload() { + if (!client_token_.empty()) { + return "{\"clientToken\":\"" + client_token_ + "\"}"; + } + + return "{}"; + }; + + ResponseCode Jobs::SendJobsQuery(JobExecutionTopicType topicType, + const util::String &jobId) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(topicType, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeClientTokenPayload(), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsStartNext(const util::Map &statusDetailsMap) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_START_NEXT_TOPIC, JOB_REQUEST_TYPE); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeStartNextPendingJobExecutionPayload(statusDetailsMap), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsDescribe(const util::String &jobId, + int64_t executionNumber, // set to 0 to ignore + bool includeJobDocument) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_DESCRIBE_TOPIC, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeDescribeJobExecutionPayload(executionNumber, includeJobDocument), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsUpdate(const util::String &jobId, + JobExecutionStatus status, + const util::Map &statusDetailsMap, + int64_t expectedVersion, // set to 0 to ignore + int64_t executionNumber, // set to 0 to ignore + bool includeJobExecutionState, + bool includeJobDocument) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_UPDATE_TOPIC, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, + SerializeJobExecutionUpdatePayload(status, statusDetailsMap, expectedVersion, executionNumber, + includeJobExecutionState, includeJobDocument), + nullptr, packet_id); + }; + + std::shared_ptr Jobs::CreateJobsSubscription(mqtt::Subscription::ApplicationCallbackHandlerPtr p_app_handler, + std::shared_ptr p_app_handler_data, + JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType, + const util::String &jobId) { + return mqtt::Subscription::Create(GetJobTopic(topicType, replyType, jobId), qos_, p_app_handler, p_app_handler_data); + }; +} diff --git a/tests/integration/include/JobsTest.hpp b/tests/integration/include/JobsTest.hpp new file mode 100644 index 0000000..e958fb7 --- /dev/null +++ b/tests/integration/include/JobsTest.hpp @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.hpp + * @brief + * + */ + + +#pragma once + +#include "mqtt/Client.hpp" +#include "NetworkConnection.hpp" +#include "jobs/Jobs.hpp" + +namespace awsiotsdk { + namespace tests { + namespace integration { + class JobsTest { + protected: + static const std::chrono::seconds keep_alive_timeout_; + + std::shared_ptr p_network_connection_; + std::shared_ptr p_iot_client_; + std::shared_ptr p_jobs_; + std::atomic done_; + + ResponseCode GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + + ResponseCode Subscribe(); + ResponseCode Unsubscribe(); + ResponseCode InitializeTLS(); + + public: + ResponseCode RunTest(); + }; + } + } +} + + diff --git a/tests/integration/src/IntegTestRunner.cpp b/tests/integration/src/IntegTestRunner.cpp index fc6c160..73eef3b 100644 --- a/tests/integration/src/IntegTestRunner.cpp +++ b/tests/integration/src/IntegTestRunner.cpp @@ -28,6 +28,7 @@ #include "ConfigCommon.hpp" #include "IntegTestRunner.hpp" #include "SdkTestConfig.hpp" +#include "JobsTest.hpp" #include "PubSub.hpp" #include "AutoReconnect.hpp" #include "MultipleClients.hpp" @@ -53,6 +54,17 @@ namespace awsiotsdk { ResponseCode IntegTestRunner::RunAllTests() { ResponseCode rc = ResponseCode::SUCCESS; // Each test runs in its own scope to ensure complete cleanup + /** + * Run Jobs Tests + */ + { + JobsTest jobs_test_runner; + rc = jobs_test_runner.RunTest(); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + } + /** * Run Subscribe Publish Tests */ diff --git a/tests/integration/src/JobsTest.cpp b/tests/integration/src/JobsTest.cpp new file mode 100644 index 0000000..3e913df --- /dev/null +++ b/tests/integration/src/JobsTest.cpp @@ -0,0 +1,327 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsTest.cpp + * @brief + * + */ + +#include "JobsTest.hpp" +#include "util/logging/LogMacros.hpp" + +#include +#include + +#ifdef USE_WEBSOCKETS +#include "WebSocketConnection.hpp" +#elif defined USE_MBEDTLS +#include "MbedTLSConnection.hpp" +#else +#include "OpenSSLConnection.hpp" +#endif + +#include "ConfigCommon.hpp" + +#define JOBS_INTEGRATION_TEST_TAG "[Integration Test - Jobs]" + +namespace awsiotsdk { + namespace tests { + namespace integration { + ResponseCode JobsTest::GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "GetPendingCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Json Parse for GetPendingCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("inProgressJobs")) { + std::cout << "inProgressJobs : " << util::JsonParser::ToString(doc["inProgressJobs"]) << std::endl; + } + + if (doc.HasMember("queuedJobs")) { + std::cout << "queuedJobs : " << util::JsonParser::ToString(doc["queuedJobs"]) << std::endl; + } + + std::cout << "************" << std::endl; + + rc = p_jobs_->SendJobsQuery(Jobs::JOB_DESCRIBE_TOPIC, "$next"); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + + return ResponseCode::FAILURE; + } + + return ResponseCode::SUCCESS; + } + + ResponseCode JobsTest::NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "NextJobCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Json Parse for NextJobCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("execution")) { + std::cout << "execution : " << util::JsonParser::ToString(doc["execution"]) << std::endl; + + if (doc["execution"].HasMember("jobId")) { + util::Map statusDetailsMap; + + util::String jobId = doc["execution"]["jobId"].GetString(); + std::cout << "jobId : " << jobId << std::endl; + + if (doc["execution"].HasMember("jobDocument")) { + std::cout << "jobDocument : " << util::JsonParser::ToString(doc["execution"]["jobDocument"]) << std::endl; + statusDetailsMap.insert(std::make_pair("exampleDetail", "a value appropriate for your successful job")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_SUCCEEDED, statusDetailsMap); + } else { + statusDetailsMap.insert(std::make_pair("failureDetail", "Unable to process job document")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_FAILED, statusDetailsMap); + } + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsUpdate failed. %s", ResponseHelper::ToString(rc).c_str()); + return rc; + } + } + } else { + std::cout << "No job execution description found, nothing to do." << std::endl; + done_ = true; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsTest::Subscribe() { + std::cout << "******** Subscribe ***************" << std::endl; + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_pending_handler = + std::bind(&JobsTest::GetPendingCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_next_handler = + std::bind(&JobsTest::NextJobCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + util::Vector> topic_vector; + std::shared_ptr p_subscription; + + p_subscription = p_jobs_->CreateJobsSubscription(p_pending_handler, nullptr, Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(p_subscription); + + ResponseCode rc = p_iot_client_->Subscribe(topic_vector, ConfigCommon::mqtt_command_timeout_); + std::this_thread::sleep_for(std::chrono::seconds(3)); + return rc; + } + + ResponseCode JobsTest::Unsubscribe() { + uint16_t packet_id = 0; + std::unique_ptr p_topic_name; + util::Vector> topic_vector; + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(std::move(p_topic_name)); + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(std::move(p_topic_name)); + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(std::move(p_topic_name)); + + ResponseCode rc = p_iot_client_->UnsubscribeAsync(std::move(topic_vector), nullptr, packet_id); + std::this_thread::sleep_for(std::chrono::seconds(1)); + return rc; + } + + ResponseCode JobsTest::InitializeTLS() { + ResponseCode rc = ResponseCode::SUCCESS; + +#ifdef USE_WEBSOCKETS + p_network_connection_ = std::shared_ptr( + new network::WebSocketConnection(ConfigCommon::endpoint_, ConfigCommon::endpoint_https_port_, + ConfigCommon::root_ca_path_, ConfigCommon::aws_region_, + ConfigCommon::aws_access_key_id_, + ConfigCommon::aws_secret_access_key_, + ConfigCommon::aws_session_token_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true)); +#elif defined USE_MBEDTLS + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + + if (ResponseCode::SUCCESS != rc) { + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#else + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + rc = p_network_connection->Initialize(); + + if (ResponseCode::SUCCESS != rc) { + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#endif + return rc; + } + + ResponseCode JobsTest::RunTest() { + done_ = false; + ResponseCode rc = InitializeTLS(); + + do { + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Failed to initialize TLS layer. %s", + ResponseHelper::ToString(rc).c_str()); + break; + } + + p_iot_client_ = std::shared_ptr( + MqttClient::Create(p_network_connection_, ConfigCommon::mqtt_command_timeout_)); + if (nullptr == p_iot_client_) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Failed to create MQTT Client Instance!!"); + rc = ResponseCode::FAILURE; + break; + } + + util::String client_id_tagged = ConfigCommon::base_client_id_; + client_id_tagged.append("_jobs_tester_"); + client_id_tagged.append(std::to_string(rand())); + std::unique_ptr client_id = Utf8String::Create(client_id_tagged); + + rc = p_iot_client_->Connect(ConfigCommon::mqtt_command_timeout_, ConfigCommon::is_clean_session_, + mqtt::Version::MQTT_3_1_1, ConfigCommon::keep_alive_timeout_secs_, + std::move(client_id), nullptr, nullptr, nullptr); + + p_jobs_ = Jobs::Create(p_iot_client_, mqtt::QoS::QOS1, ConfigCommon::thing_name_, client_id_tagged); + + if (ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "MQTT Connect failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + rc = Subscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Subscribe failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + break; + } + + rc = p_jobs_->SendJobsQuery(Jobs::JOB_GET_PENDING_TOPIC); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + } + + int retries = 5; + while (!done_ && retries-- > 0) { + std::this_thread::sleep_for(std::chrono::seconds(5)); + } + + if (!done_) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Not all jobs processed."); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + rc = ResponseCode::FAILURE; + break; + } + + rc = Unsubscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Unsubscribe failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + break; + } + + rc = p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Disconnect failed. %s", + ResponseHelper::ToString(rc).c_str()); + break; + } + } while (false); + + std::cout << std::endl; + if (ResponseCode::SUCCESS != rc) { + std::cout + << "Test Failed!!!! See above output for details!!" + << std::endl; + std::cout << "**********************************************************" << std::endl; + return ResponseCode::FAILURE; + } + + std::cout << "Test Successful!!!!" << std::endl; + std::cout << "**********************************************************" << std::endl; + return ResponseCode::SUCCESS; + } + } + } +} diff --git a/tests/unit/src/ResponseCodeTests.cpp b/tests/unit/src/ResponseCodeTests.cpp index 0d6f5cc..ada914e 100644 --- a/tests/unit/src/ResponseCodeTests.cpp +++ b/tests/unit/src/ResponseCodeTests.cpp @@ -570,6 +570,11 @@ namespace awsiotsdk { expected_string = ResponseCodeToString(ResponseHelper::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING, ResponseCode::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR); EXPECT_EQ(expected_string, response_string); + + response_string = ResponseHelper::ToString(ResponseCode::JOBS_INVALID_TOPIC_ERROR); + expected_string = ResponseCodeToString(ResponseHelper::JOBS_INVALID_TOPIC_ERROR_STRING, + ResponseCode::JOBS_INVALID_TOPIC_ERROR); + EXPECT_EQ(expected_string, response_string); } } } diff --git a/tests/unit/src/jobs/JobsTests.cpp b/tests/unit/src/jobs/JobsTests.cpp new file mode 100644 index 0000000..56b5161 --- /dev/null +++ b/tests/unit/src/jobs/JobsTests.cpp @@ -0,0 +1,264 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsTests.cpp + * @brief + * + */ + +#include + +#include + +#include "util/logging/LogMacros.hpp" + +#include "TestHelper.hpp" +#include "MockNetworkConnection.hpp" + +#include "jobs/Jobs.hpp" +#include "mqtt/ClientState.hpp" + +#define JOBS_TEST_LOG_TAG "[Jobs Unit Test]" + +namespace awsiotsdk { + namespace tests { + namespace unit { + class JobsTestWrapper : public Jobs { + protected: + static const util::String test_thing_name_; + static const util::String client_token_; + + public: + JobsTestWrapper(bool empty_thing_name, bool empty_client_token): + Jobs(nullptr, mqtt::QoS::QOS0, + empty_thing_name ? "" : test_thing_name_, + empty_client_token ? "" : client_token_) {} + + util::String SerializeStatusDetails(const util::Map &statusDetailsMap) { + return Jobs::SerializeStatusDetails(statusDetailsMap); + } + + util::String SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, + int64_t executionNumber = 0, + bool includeJobExecutionState = false, + bool includeJobDocument = false) { + return Jobs::SerializeJobExecutionUpdatePayload(status, statusDetailsMap, expectedVersion, executionNumber, includeJobExecutionState, includeJobDocument); + } + + util::String SerializeDescribeJobExecutionPayload(int64_t executionNumber = 0, + bool includeJobDocument = true) { + return Jobs::SerializeDescribeJobExecutionPayload(executionNumber, includeJobDocument); + } + + util::String SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap = util::Map()) { + return Jobs::SerializeStartNextPendingJobExecutionPayload(statusDetailsMap); + } + + util::String SerializeClientTokenPayload() { + return Jobs::SerializeClientTokenPayload(); + } + + util::String Escape(const util::String &value) { + return Jobs::Escape(value); + } + }; + + const util::String JobsTestWrapper::test_thing_name_ = "CppSdkTestClient"; + const util::String JobsTestWrapper::client_token_ = "CppSdkTestClientToken"; + + class JobsTester : public ::testing::Test { + protected: + static const util::String job_id_; + + std::shared_ptr p_jobs_; + std::shared_ptr p_jobs_empty_client_token_; + std::shared_ptr p_jobs_empty_thing_name_; + + JobsTester() { + p_jobs_ = std::shared_ptr(new JobsTestWrapper(false, false)); + p_jobs_empty_client_token_ = std::shared_ptr(new JobsTestWrapper(false, true)); + p_jobs_empty_thing_name_ = std::shared_ptr(new JobsTestWrapper(true, false)); + } + }; + + const util::String JobsTester::job_id_ = "TestJobId"; + + TEST_F(JobsTester, ValidTopicsTests) { + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/accepted", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/rejected", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/#", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/accepted", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/rejected", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/#", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/accepted", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/rejected", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/#", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/accepted", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/rejected", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/#", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/notify", p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/notify-next", p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + } + + TEST_F(JobsTester, InvalidTopicsTests) { + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + } + + + TEST_F(JobsTester, PayloadSerializationTests) { + util::Map statusDetailsMap; + statusDetailsMap.insert(std::make_pair("testKey", "testVal")); + + EXPECT_EQ("{}", p_jobs_empty_client_token_->SerializeClientTokenPayload()); + EXPECT_EQ("{\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeClientTokenPayload()); + + EXPECT_EQ("{}", p_jobs_empty_client_token_->SerializeStartNextPendingJobExecutionPayload()); + EXPECT_EQ("{\"statusDetails\":{\"testKey\":\"testVal\"}}", p_jobs_empty_client_token_->SerializeStartNextPendingJobExecutionPayload(statusDetailsMap)); + EXPECT_EQ("{\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeStartNextPendingJobExecutionPayload()); + EXPECT_EQ("{\"statusDetails\":{\"testKey\":\"testVal\"},\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeStartNextPendingJobExecutionPayload(statusDetailsMap)); + + EXPECT_EQ("{\"includeJobDocument\":\"true\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload()); + EXPECT_EQ("{\"includeJobDocument\":\"true\",\"executionNumber\":\"1\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload(1)); + EXPECT_EQ("{\"includeJobDocument\":\"false\",\"executionNumber\":\"1\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload(1, false)); + + EXPECT_EQ("{\"includeJobDocument\":\"true\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload()); + EXPECT_EQ("{\"includeJobDocument\":\"true\",\"executionNumber\":\"1\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload(1)); + EXPECT_EQ("{\"includeJobDocument\":\"false\",\"executionNumber\":\"1\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload(1, false)); + + EXPECT_EQ("", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_STATUS_NOT_SET)); + EXPECT_EQ("", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_UNKNOWN_STATUS)); + EXPECT_EQ("", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_STATUS_NOT_SET)); + EXPECT_EQ("", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_UNKNOWN_STATUS)); + + EXPECT_EQ("{\"status\":\"QUEUED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"}}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"includeJobDocument\":\"true\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true, true)); + + EXPECT_EQ("{\"status\":\"IN_PROGRESS\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_IN_PROGRESS)); + EXPECT_EQ("{\"status\":\"FAILED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_FAILED)); + EXPECT_EQ("{\"status\":\"SUCCEEDED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_SUCCEEDED)); + EXPECT_EQ("{\"status\":\"CANCELED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_CANCELED)); + EXPECT_EQ("{\"status\":\"REJECTED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_REJECTED)); + + EXPECT_EQ("{\"status\":\"QUEUED\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"includeJobDocument\":\"true\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true, true)); + + EXPECT_EQ("{\"status\":\"IN_PROGRESS\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_IN_PROGRESS)); + EXPECT_EQ("{\"status\":\"FAILED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_FAILED)); + EXPECT_EQ("{\"status\":\"SUCCEEDED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_SUCCEEDED)); + EXPECT_EQ("{\"status\":\"CANCELED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_CANCELED)); + EXPECT_EQ("{\"status\":\"REJECTED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_REJECTED)); + + statusDetailsMap.insert(std::make_pair("testEscapeKey \" \t \r \n \\ '!", "testEscapeVal \" \t \r \n \\ '!")); + EXPECT_EQ("{\"testEscapeKey \\\" \\t \\r \\n \\\\ '!\":\"testEscapeVal \\\" \\t \\r \\n \\\\ '!\",\"testKey\":\"testVal\"}", p_jobs_->SerializeStatusDetails(statusDetailsMap)); + } + } + } +} From b60537e0e126d59713b86a1ed3fbabea16d5fc20 Mon Sep 17 00:00:00 2001 From: Steve Harris Date: Mon, 2 Apr 2018 23:38:52 +0000 Subject: [PATCH 5/6] Jobs support with custom auth support Wrap SIGPIPE in ifndef block for Windows Allow Position Independent Code for Static Library (#73) Build the static library with the fPIC compiler flag, so that it can be linked into a shared library. Fix wildcard regex for special topics with $ symbols Adding standard files (#83) Pull requests to fix warnings on Windows Includes #75, #76 and #77. Also includes other changes related to - loading and storing of atomic variables - fix for UTF-8 character representation on Windows Update sample documentation with shadow client token limitation Make Shadow::HandleGetResponse call response handler on Rejected response Fixes #86 --- CMakeLists.txt | 2 + README.md | 3 + include/ResponseCode.hpp | 7 +- include/jobs/Jobs.hpp | 219 +++++++++++++ network/WebSocket/WebSocketConnection.cpp | 58 +++- network/WebSocket/WebSocketConnection.hpp | 29 ++ samples/Jobs/CMakeLists.txt | 82 +++++ samples/Jobs/JobsSample.cpp | 383 ++++++++++++++++++++++ samples/Jobs/JobsSample.hpp | 68 ++++ samples/README.md | 8 +- src/ResponseCode.cpp | 3 + src/jobs/Jobs.cpp | 340 +++++++++++++++++++ tests/integration/include/JobsTest.hpp | 59 ++++ tests/integration/src/IntegTestRunner.cpp | 12 + tests/integration/src/JobsTest.cpp | 327 ++++++++++++++++++ tests/unit/src/ResponseCodeTests.cpp | 5 + tests/unit/src/jobs/JobsTests.cpp | 264 +++++++++++++++ 17 files changed, 1855 insertions(+), 14 deletions(-) create mode 100644 include/jobs/Jobs.hpp create mode 100644 samples/Jobs/CMakeLists.txt create mode 100644 samples/Jobs/JobsSample.cpp create mode 100644 samples/Jobs/JobsSample.hpp create mode 100644 src/jobs/Jobs.cpp create mode 100644 tests/integration/include/JobsTest.hpp create mode 100644 tests/integration/src/JobsTest.cpp create mode 100644 tests/unit/src/jobs/JobsTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b2ce97a..b09e572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -162,6 +162,8 @@ add_subdirectory(tests/unit) add_subdirectory(samples/PubSub) +add_subdirectory(samples/Jobs) + add_subdirectory(samples/ShadowDelta) add_subdirectory(samples/Discovery EXCLUDE_FROM_ALL) diff --git a/README.md b/README.md index cc72cb3..89fe3be 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ The Device SDK provides functionality to create and maintain a MQTT Connection. ### Thing Shadow This SDK implements the specific protocol for Thing Shadows to retrieve, update and delete Thing Shadows adhering to the protocol that is implemented to ensure correct versioning and support for client tokens. It abstracts the necessary MQTT topic subscriptions by automatically subscribing to and unsubscribing from the reserved topics as needed for each API call. Inbound state change requests are automatically signalled via a configurable callback. +### Jobs +This SDK also implements the Jobs protocol to interact with the AWS IoT Jobs service. The IoT Job service manages deployment of IoT fleet wide tasks such as device software/firmware deployments and updates, rotation of security certificates, device reboots, and custom device specific management tasks. For additional information please see the [Jobs developer guide](https://docs.aws.amazon.com/iot/latest/developerguide/iot-jobs.html). + ## Design Goals of this SDK The C++ SDK was specifically designed for devices that are not resource constrained and required advanced features such as Message queueing, multi-threading support and the latest language features diff --git a/include/ResponseCode.hpp b/include/ResponseCode.hpp index 0a2e58c..2bfb7fa 100644 --- a/include/ResponseCode.hpp +++ b/include/ResponseCode.hpp @@ -193,7 +193,11 @@ namespace awsiotsdk { // Discovery Response Parsing Error Codes - DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR = -1200 ///< Discover Response Json is missing expected keys + DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR = -1200, ///< Discover Response Json is missing expected keys + + // Jobs Error Codes + + JOBS_INVALID_TOPIC_ERROR = -1300 ///< Jobs invalid topic }; /** @@ -314,6 +318,7 @@ namespace awsiotsdk { const util::String DISCOVER_ACTION_SERVER_ERROR_STRING("Server returned unknown error while performing the discovery action"); const util::String DISCOVER_ACTION_REQUEST_OVERLOAD_STRING("The discovery action is overloading the server, try again after some time"); const util::String DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING("The discover response JSON is incomplete "); + const util::String JOBS_INVALID_TOPIC_ERROR_STRING("Invalid jobs topic"); /** * Takes in a Response Code and returns the appropriate error/success string diff --git a/include/jobs/Jobs.hpp b/include/jobs/Jobs.hpp new file mode 100644 index 0000000..33c2130 --- /dev/null +++ b/include/jobs/Jobs.hpp @@ -0,0 +1,219 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.hpp + * @brief + * + */ + +#pragma once + +#include "mqtt/Client.hpp" + +namespace awsiotsdk { + class Jobs { + public: + // Disabling default and copy constructors. + Jobs() = delete; // Delete Default constructor + Jobs(const Jobs &) = delete; // Delete Copy constructor + Jobs(Jobs &&) = default; // Default Move constructor + Jobs &operator=(const Jobs &) & = delete; // Delete Copy assignment operator + Jobs &operator=(Jobs &&) & = default; // Default Move assignment operator + + /** + * @brief Create factory method. Returns a unique instance of Jobs + * + * @param p_mqtt_client - mqtt client + * @param qos - QoS + * @param thing_name - Thing name + * @param client_token - Client token for correlating messages (optional) + * + * @return std::unique_ptr pointing to a unique Jobs instance + */ + static std::unique_ptr Create(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token = util::String()); + + enum JobExecutionTopicType { + JOB_UNRECOGNIZED_TOPIC = 0, + JOB_GET_PENDING_TOPIC, + JOB_START_NEXT_TOPIC, + JOB_DESCRIBE_TOPIC, + JOB_UPDATE_TOPIC, + JOB_NOTIFY_TOPIC, + JOB_NOTIFY_NEXT_TOPIC, + JOB_WILDCARD_TOPIC + }; + + enum JobExecutionTopicReplyType { + JOB_UNRECOGNIZED_TOPIC_TYPE = 0, + JOB_REQUEST_TYPE, + JOB_ACCEPTED_REPLY_TYPE, + JOB_REJECTED_REPLY_TYPE, + JOB_WILDCARD_REPLY_TYPE + }; + + enum JobExecutionStatus { + JOB_EXECUTION_STATUS_NOT_SET = 0, + JOB_EXECUTION_QUEUED, + JOB_EXECUTION_IN_PROGRESS, + JOB_EXECUTION_FAILED, + JOB_EXECUTION_SUCCEEDED, + JOB_EXECUTION_CANCELED, + JOB_EXECUTION_REJECTED, + /*** + * Used for any status not in the supported list of statuses + */ + JOB_EXECUTION_UNKNOWN_STATUS = 99 + }; + + /** + * @brief GetJobTopic + * + * This function creates a job topic based on the provided parameters. + * + * @param topicType - Jobs topic type + * @param replyType - Topic reply type (optional) + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * + * @return nullptr on error, unique_ptr pointing to a topic string if successful + */ + std::unique_ptr GetJobTopic(JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType = JOB_REQUEST_TYPE, + const util::String &jobId = util::String()); + + /** + * @brief SendJobsQuery + * + * Send a query to the Jobs service using the provided mqtt client + * + * @param topicType - Jobs topic type for type of query + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsQuery(JobExecutionTopicType topicType, + const util::String &jobId = util::String()); + + /** + * @brief SendJobsStartNext + * + * Call Jobs start-next API to start the next pending job execution and trigger response + * + * @param statusDetails - Status details to be associated with started job execution (optional) + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsStartNext(const util::Map &statusDetailsMap = util::Map()); + + /** + * @brief SendJobsDescribe + * + * Send request for job execution details + * + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also + * be omitted to request all pending and in progress job executions + * @param executionNumber - Specific execution number to describe, omit to match latest + * @param includeJobDocument - Flag to indicate whether response should include job document + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsDescribe(const util::String &jobId = util::String(), + int64_t executionNumber = 0, // set to 0 to ignore + bool includeJobDocument = true); + + /** + * @brief SendJobsUpdate + * + * Send update for specified job + * + * @param jobId - Job id associated with job execution to be updated + * @param status - New job execution status + * @param statusDetailsMap - Status details to be associated with job execution (optional) + * @param expectedVersion - Optional expected current job execution number, error response if mismatched + * @param executionNumber - Specific execution number to update, omit to match latest + * @param includeJobExecutionState - Include job execution state in response (optional) + * @param includeJobDocument - Include job document in response (optional) + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsUpdate(const util::String &jobId, + JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, // set to 0 to ignore + int64_t executionNumber = 0, // set to 0 to ignore + bool includeJobExecutionState = false, + bool includeJobDocument = false); + + /** + * @brief CreateJobsSubscription + * + * Create a Jobs Subscription instance + * + * @param p_app_handler - Application Handler instance + * @param p_app_handler_data - Data to be passed to application handler. Can be nullptr + * @param topicType - Jobs topic type to subscribe to (defaults to JOB_WILDCARD_TOPIC) + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * @param replyType - Topic reply type (optional, defaults to JOB_REQUEST_TYPE which omits the reply type in the subscription) + * + * @return shared_ptr Subscription instance + */ + std::shared_ptr CreateJobsSubscription(mqtt::Subscription::ApplicationCallbackHandlerPtr p_app_handler, + std::shared_ptr p_app_handler_data, + JobExecutionTopicType topicType = JOB_WILDCARD_TOPIC, + JobExecutionTopicReplyType replyType = JOB_REQUEST_TYPE, + const util::String &jobId = util::String()); + protected: + std::shared_ptr p_mqtt_client_; + mqtt::QoS qos_; + util::String thing_name_; + util::String client_token_; + + /** + * @brief Jobs constructor + * + * Create Jobs object storing given parameters in created instance + * + * @param p_mqtt_client - mqtt client + * @param qos - QoS + * @param thing_name - Thing name + * @param client_token - Client token for correlating messages (optional) + */ + Jobs(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token); + + static bool BaseTopicRequiresJobId(JobExecutionTopicType topicType); + static const util::String GetOperationForBaseTopic(JobExecutionTopicType topicType); + static const util::String GetSuffixForTopicType(JobExecutionTopicReplyType replyType); + static const util::String GetExecutionStatus(JobExecutionStatus status); + static util::String Escape(const util::String &value); + static util::String SerializeStatusDetails(const util::Map &statusDetailsMap); + + util::String SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, + int64_t executionNumber = 0, + bool includeJobExecutionState = false, + bool includeJobDocument = false); + util::String SerializeDescribeJobExecutionPayload(int64_t executionNumber = 0, + bool includeJobDocument = true); + util::String SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap = util::Map()); + util::String SerializeClientTokenPayload(); + }; +} diff --git a/network/WebSocket/WebSocketConnection.cpp b/network/WebSocket/WebSocketConnection.cpp index 847638b..9214ffd 100644 --- a/network/WebSocket/WebSocketConnection.cpp +++ b/network/WebSocket/WebSocketConnection.cpp @@ -47,6 +47,8 @@ #define X_AMZ_DATE "X-Amz-Date" #define X_AMZ_EXPIRES "X-Amz-Expires" #define X_AMZ_SECURITY_TOKEN "X-Amz-Security-Token" +#define X_AMZ_CUSTOMAUTHORIZER_NAME "X-Amz-CustomAuthorizer-Name" +#define X_AMZ_CUSTOMAUTHORIZER_SIGNATURE "X-Amz-CustomAuthorizer-Signature" #define SIGNING_KEY "AWS4" #define LONG_DATE_FORMAT_STR "%Y%m%dT%H%M%SZ" #define SIMPLE_DATE_FORMAT_STR "%Y%m%d" @@ -99,6 +101,11 @@ namespace awsiotsdk { bool server_verification_flag) : openssl_connection_(endpoint, endpoint_port, root_ca_location, tls_handshake_timeout, tls_read_timeout, tls_write_timeout, server_verification_flag) { + custom_authorizer_name_.clear(); + custom_authorizer_signature_.clear(); + custom_authorizer_token_name_.clear(); + custom_authorizer_token_.clear(); + endpoint_ = endpoint; endpoint_port_ = endpoint_port; root_ca_location_ = root_ca_location; @@ -125,6 +132,21 @@ namespace awsiotsdk { wss_frame_write_ = std::unique_ptr(new wslay_frame_iocb()); } + WebSocketConnection::WebSocketConnection(util::String endpoint, uint16_t endpoint_port, util::String root_ca_location, + std::chrono::milliseconds tls_handshake_timeout, + std::chrono::milliseconds tls_read_timeout, + std::chrono::milliseconds tls_write_timeout, + util::String custom_authorizer_name, util::String custom_authorizer_signature, + util::String custom_authorizer_token_name, util::String custom_authorizer_token, + bool server_verification_flag) + : WebSocketConnection(endpoint, endpoint_port, root_ca_location, "", "", "", "", tls_handshake_timeout, tls_read_timeout, + tls_write_timeout, false) { + custom_authorizer_name_ = custom_authorizer_name; + custom_authorizer_signature_ = custom_authorizer_signature; + custom_authorizer_token_name_ = custom_authorizer_token_name; + custom_authorizer_token_ = custom_authorizer_token; + } + ResponseCode WebSocketConnection::ConnectInternal() { // Init Tls ResponseCode rc = openssl_connection_.Initialize(); @@ -563,17 +585,12 @@ namespace awsiotsdk { } ResponseCode WebSocketConnection::WssHandshake() { + ResponseCode rc; + util::OStringStream stringStream; + // Assuming: // 1. Ssl socket is ready to do read/write. - // Create canonical query string - util::String canonical_query_string; - canonical_query_string.reserve(CANONICAL_QUERY_BUF_LEN); - ResponseCode rc = InitializeCanonicalQueryString(canonical_query_string); - if (ResponseCode::SUCCESS != rc) { - return rc; - } - // Create Wss handshake Http request // -> Generate Wss client key char client_key_buf[WSS_CLIENT_KEY_MAX_LEN + 1]; @@ -583,15 +600,32 @@ namespace awsiotsdk { return rc; } - // -> Assemble Wss Http request - util::OStringStream stringStream; - stringStream << "GET /mqtt?" << canonical_query_string << " " << HTTP_1_1 << "\r\n" - << "Host: " << endpoint_ << "\r\n" + if (custom_authorizer_name_.empty()) { + // Create canonical query string + util::String canonical_query_string; + canonical_query_string.reserve(CANONICAL_QUERY_BUF_LEN); + rc = InitializeCanonicalQueryString(canonical_query_string); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + + // -> Assemble Wss Http request + stringStream << "GET /mqtt?" << canonical_query_string << " " << HTTP_1_1 << "\r\n"; + } else { + // -> Assemble Wss Http request + stringStream << "GET /mqtt " << HTTP_1_1 << "\r\n" + << X_AMZ_CUSTOMAUTHORIZER_NAME << ": " << custom_authorizer_name_ << "\r\n" + << X_AMZ_CUSTOMAUTHORIZER_SIGNATURE << ": " << custom_authorizer_signature_ << "\r\n" + << custom_authorizer_token_name_ << ": " << custom_authorizer_token_ << "\r\n"; + } + + stringStream << "Host: " << endpoint_ << "\r\n" << "Connection: " << UPGRADE << "\r\n" << "Upgrade: " << WEBSOCKET << "\r\n" << "Sec-WebSocket-Version: " << SEC_WEBSOCKET_VERSION_13 << "\r\n" << "sec-websocket-key: " << client_key_buf << "\r\n" << "Sec-WebSocket-Protocol: " << MQTT_PROTOCOL << "\r\n\r\n"; + util::String request_string = stringStream.str(); // Send out request diff --git a/network/WebSocket/WebSocketConnection.hpp b/network/WebSocket/WebSocketConnection.hpp index 761c8cc..fa8a491 100644 --- a/network/WebSocket/WebSocketConnection.hpp +++ b/network/WebSocket/WebSocketConnection.hpp @@ -50,6 +50,10 @@ namespace awsiotsdk { util::String aws_access_key_id_; ///< Pointer to string containing the AWS Access Key Id. util::String aws_secret_access_key_; ///< Pointer to sstring containing the AWS Secret Access Key. util::String aws_session_token_; ///< Pointer to string containing the AWS Session Token. + util::String custom_authorizer_name_; ///< Pointer to string containing the custom authorizer name. + util::String custom_authorizer_signature_; ///< Pointer to string containing the authorizer signature. + util::String custom_authorizer_token_name_; ///< Pointer to string containing the authorizer token name. + util::String custom_authorizer_token_; ///< Pointer to string containing the authorizer token. util::String aws_region_; ///< Region for this connection util::String endpoint_; ///< Endpoint for this connection uint16_t endpoint_port_; ///< Endpoint port @@ -210,6 +214,31 @@ namespace awsiotsdk { std::chrono::milliseconds tls_read_timeout, std::chrono::milliseconds tls_write_timeout, bool server_verification_flag); + /** + * @brief Constructor for the WebSocket for MQTT implementation using custom authentication + * + * Performs any initialization required by the WebSocket layer. + * + * @param util::String endpoint - The target endpoint to connect to + * @param uint16_t endpoint_port - The port on the target to connect to + * @param util::String root_ca_location - Path of the location of the Root CA + * @param std::chrono::milliseconds tls_handshake_timeout - The value to use for timeout of handshake operation + * @param std::chrono::milliseconds tls_read_timeout - The value to use for timeout of read operation + * @param std::chrono::milliseconds tls_write_timeout - The value to use for timeout of write operation + * @param util::String custom_authorizer_name - Name of the authorizer function + * @param util::String custom_authorizer_signature - Authorizer signature + * @param util::String custom_authorizer_token_name - Authorizer token name + * @param util::String custom_authorizer_token - Authorizer token + * @param bool server_verification_flag - used to decide whether server verification is needed or not + * + */ + WebSocketConnection(util::String endpoint, uint16_t endpoint_port, util::String root_ca_location, + std::chrono::milliseconds tls_handshake_timeout, + std::chrono::milliseconds tls_read_timeout, std::chrono::milliseconds tls_write_timeout, + util::String custom_authorizer_name, util::String custom_authorizer_signature, + util::String custom_authorizer_token_name, util::String custom_authorizer_token, + bool server_verification_flag); + /** * @brief Check if WebSocket layer is still connected * diff --git a/samples/Jobs/CMakeLists.txt b/samples/Jobs/CMakeLists.txt new file mode 100644 index 0000000..b11e1c8 --- /dev/null +++ b/samples/Jobs/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.2 FATAL_ERROR) +project(aws-iot-cpp-samples CXX) + +###################################### +# Section : Disable in-source builds # +###################################### + +if (${PROJECT_SOURCE_DIR} STREQUAL ${PROJECT_BINARY_DIR}) + message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt and CMakeFiles folder.") +endif () + +######################################## +# Section : Common Build setttings # +######################################## +# Set required compiler standard to standard c++11. Disable extensions. +set(CMAKE_CXX_STANDARD 11) # C++11... +set(CMAKE_CXX_STANDARD_REQUIRED ON) #...is required... +set(CMAKE_CXX_EXTENSIONS OFF) #...without compiler extensions like gnu++11 + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/archive) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +# Configure Compiler flags +if (UNIX AND NOT APPLE) + # Prefer pthread if found + set(THREADS_PREFER_PTHREAD_FLAG ON) + set(CUSTOM_COMPILER_FLAGS "-fno-exceptions -Wall -Werror") +elseif (APPLE) + set(CUSTOM_COMPILER_FLAGS "-fno-exceptions -Wall -Werror") +elseif (WIN32) + set(CUSTOM_COMPILER_FLAGS "/W4") +endif () + +################################ +# Target : Build Jobs sample # +################################ +set(JOBS_SAMPLE_TARGET_NAME jobs-sample) +# Add Target +add_executable(${JOBS_SAMPLE_TARGET_NAME} "${PROJECT_SOURCE_DIR}/JobsSample.cpp;${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.cpp") + +# Add Target specific includes +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../common) +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}) + +# Configure Threading library +find_package(Threads REQUIRED) + +# Add SDK includes +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${CMAKE_BINARY_DIR}/${DEPENDENCY_DIR}/rapidjson/src/include) +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../include) + +target_link_libraries(${JOBS_SAMPLE_TARGET_NAME} PUBLIC "Threads::Threads") +target_link_libraries(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${SDK_TARGET_NAME}) + +# Copy Json config file +add_custom_command(TARGET ${JOBS_SAMPLE_TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy ${PROJECT_SOURCE_DIR}/../../common/SampleConfig.json $/config/SampleConfig.json) +set_property(TARGET ${JOBS_SAMPLE_TARGET_NAME} APPEND_STRING PROPERTY COMPILE_FLAGS ${CUSTOM_COMPILER_FLAGS}) + +# Gather list of all .cert files in "/cert" +add_custom_command(TARGET ${JOBS_SAMPLE_TARGET_NAME} PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy_directory ${PROJECT_SOURCE_DIR}/../../certs $/certs) + +if (MSVC) + target_sources(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.hpp) + source_group("Header Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.hpp) + source_group("Source Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.cpp) + + target_sources(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/JobsSample.hpp) + source_group("Header Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/JobsSample.hpp) + source_group("Source Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/JobsSample.cpp) +endif () + +######################### +# Add Network libraries # +######################### + +set(NETWORK_WRAPPER_DEST_TARGET ${JOBS_SAMPLE_TARGET_NAME}) +include(${PROJECT_SOURCE_DIR}/../../network/CMakeLists.txt.in) diff --git a/samples/Jobs/JobsSample.cpp b/samples/Jobs/JobsSample.cpp new file mode 100644 index 0000000..a1f032c --- /dev/null +++ b/samples/Jobs/JobsSample.cpp @@ -0,0 +1,383 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsSample.cpp + * + * This example takes the parameters from the config/SampleConfig.json file and establishes + * a connection to the AWS IoT MQTT Platform. It performs several operations to + * demonstrate the basic capabilities of the AWS IoT Jobs platform. + * + * If all the certs are correct, you should see the list of pending Job Executions + * printed out by the GetPendingCallback callback. If there are any existing pending + * job executions each will be processed one at a time in the NextJobCallback callback. + * After all of the pending jobs have been processed the program will wait for + * notifications for new pending jobs and process them one at a time as they come in. + * + * In the Subscribe function you can see how each callback is registered for each corresponding + * Jobs topic. + * + */ + +#include +#include + +#ifdef USE_WEBSOCKETS +#include "WebSocketConnection.hpp" +#elif defined USE_MBEDTLS +#include "MbedTLSConnection.hpp" +#else +#include "OpenSSLConnection.hpp" +#endif + +#include "util/logging/Logging.hpp" +#include "util/logging/LogMacros.hpp" +#include "util/logging/ConsoleLogSystem.hpp" + +#include "ConfigCommon.hpp" +#include "jobs/Jobs.hpp" +#include "JobsSample.hpp" + +#define LOG_TAG_JOBS "[Sample - Jobs]" + +namespace awsiotsdk { + namespace samples { + ResponseCode JobsSample::GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "GetPendingCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Json Parse for GetPendingCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("inProgressJobs")) { + std::cout << "inProgressJobs : " << util::JsonParser::ToString(doc["inProgressJobs"]) << std::endl; + } + + if (doc.HasMember("queuedJobs")) { + std::cout << "queuedJobs : " << util::JsonParser::ToString(doc["queuedJobs"]) << std::endl; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "NextJobCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Json Parse for NextJobCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("execution")) { + std::cout << "execution : " << util::JsonParser::ToString(doc["execution"]) << std::endl; + + if (doc["execution"].HasMember("jobId")) { + util::Map statusDetailsMap; + + util::String jobId = doc["execution"]["jobId"].GetString(); + std::cout << "jobId : " << jobId << std::endl; + + if (doc["execution"].HasMember("jobDocument")) { + std::cout << "jobDocument : " << util::JsonParser::ToString(doc["execution"]["jobDocument"]) << std::endl; + statusDetailsMap.insert(std::make_pair("exampleDetail", "a value appropriate for your successful job")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_SUCCEEDED, statusDetailsMap); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "SendJobsUpdate failed. %s", ResponseHelper::ToString(rc).c_str()); + return rc; + } + } else { + statusDetailsMap.insert(std::make_pair("failureDetail", "Unable to process job document")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_FAILED, statusDetailsMap); + } + } + } else { + std::cout << "No job execution description found, nothing to do." << std::endl; + done_ = true; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::UpdateAcceptedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + std::cout << std::endl << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::UpdateRejectedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + std::cout << std::endl << "************" << std::endl; + + /* Do error handling here for when the update was rejected */ + + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::DisconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data) { + std::cout << "*******************************************" << std::endl + << client_id << " Disconnected!" << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::ReconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode reconnect_result) { + std::cout << "*******************************************" << std::endl + << client_id << " Reconnect Attempted. Result " << ResponseHelper::ToString(reconnect_result) + << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::ResubscribeCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode resubscribe_result) { + std::cout << "*******************************************" << std::endl + << client_id << " Resubscribe Attempted. Result" << ResponseHelper::ToString(resubscribe_result) + << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + + ResponseCode JobsSample::Subscribe() { + std::cout << "******** Subscribe ***************" << std::endl; + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_pending_handler = + std::bind(&JobsSample::GetPendingCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_next_handler = + std::bind(&JobsSample::NextJobCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_update_accepted_handler = + std::bind(&JobsSample::UpdateAcceptedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_update_rejected_handler = + std::bind(&JobsSample::UpdateRejectedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + util::Vector> topic_vector; + std::shared_ptr p_subscription; + + p_subscription = p_jobs_->CreateJobsSubscription(p_pending_handler, nullptr, Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_update_accepted_handler, nullptr, Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "+"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_update_rejected_handler, nullptr, Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, "+"); + topic_vector.push_back(p_subscription); + + ResponseCode rc = p_iot_client_->Subscribe(topic_vector, ConfigCommon::mqtt_command_timeout_); + return rc; + } + + ResponseCode JobsSample::InitializeTLS() { + ResponseCode rc = ResponseCode::SUCCESS; + +#ifdef USE_WEBSOCKETS + p_network_connection_ = std::shared_ptr( + new network::WebSocketConnection(ConfigCommon::endpoint_, ConfigCommon::endpoint_https_port_, + ConfigCommon::root_ca_path_, ConfigCommon::aws_region_, + ConfigCommon::aws_access_key_id_, + ConfigCommon::aws_secret_access_key_, + ConfigCommon::aws_session_token_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true)); + if (nullptr == p_network_connection_) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } +#elif defined USE_MBEDTLS + p_network_connection_ = std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, + true); + if (nullptr == p_network_connection_) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } +#else + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + rc = p_network_connection->Initialize(); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, + "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#endif + return rc; + } + + ResponseCode JobsSample::RunSample() { + done_ = false; + + ResponseCode rc = InitializeTLS(); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + + ClientCoreState::ApplicationDisconnectCallbackPtr p_disconnect_handler = + std::bind(&JobsSample::DisconnectCallback, this, std::placeholders::_1, std::placeholders::_2); + + ClientCoreState::ApplicationReconnectCallbackPtr p_reconnect_handler = + std::bind(&JobsSample::ReconnectCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + ClientCoreState::ApplicationResubscribeCallbackPtr p_resubscribe_handler = + std::bind(&JobsSample::ResubscribeCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + p_iot_client_ = std::shared_ptr(MqttClient::Create(p_network_connection_, + ConfigCommon::mqtt_command_timeout_, + p_disconnect_handler, nullptr, + p_reconnect_handler, nullptr, + p_resubscribe_handler, nullptr)); + if (nullptr == p_iot_client_) { + return ResponseCode::FAILURE; + } + + util::String client_id_tagged = ConfigCommon::base_client_id_; + client_id_tagged.append("_jobs_sample_"); + client_id_tagged.append(std::to_string(rand())); + std::unique_ptr client_id = Utf8String::Create(client_id_tagged); + + rc = p_iot_client_->Connect(ConfigCommon::mqtt_command_timeout_, ConfigCommon::is_clean_session_, + mqtt::Version::MQTT_3_1_1, ConfigCommon::keep_alive_timeout_secs_, + std::move(client_id), nullptr, nullptr, nullptr); + if (ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED != rc) { + return rc; + } + + p_jobs_ = Jobs::Create(p_iot_client_, mqtt::QoS::QOS1, ConfigCommon::thing_name_, client_id_tagged); + + rc = Subscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Subscribe failed. %s", ResponseHelper::ToString(rc).c_str()); + } else { + rc = p_jobs_->SendJobsQuery(Jobs::JOB_GET_PENDING_TOPIC); + + if (ResponseCode::SUCCESS == rc) { + rc = p_jobs_->SendJobsQuery(Jobs::JOB_DESCRIBE_TOPIC, "$next"); + } + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + } + } + + // Wait for job processing to complete + while (!done_) { + done_ = true; + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + + rc = p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Disconnect failed. %s", ResponseHelper::ToString(rc).c_str()); + } + + std::cout << "Exiting Sample!!!!" << std::endl; + return ResponseCode::SUCCESS; + } + } +} + +int main(int argc, char **argv) { + std::shared_ptr p_log_system = + std::make_shared(awsiotsdk::util::Logging::LogLevel::Info); + awsiotsdk::util::Logging::InitializeAWSLogging(p_log_system); + + std::unique_ptr + jobs_sample = std::unique_ptr(new awsiotsdk::samples::JobsSample()); + + awsiotsdk::ResponseCode rc = awsiotsdk::ConfigCommon::InitializeCommon("config/SampleConfig.json"); + if (awsiotsdk::ResponseCode::SUCCESS == rc) { + rc = jobs_sample->RunSample(); + } +#ifdef WIN32 + std::cout<<"Press any key to continue!!!!"<(rc); +} diff --git a/samples/Jobs/JobsSample.hpp b/samples/Jobs/JobsSample.hpp new file mode 100644 index 0000000..8080b61 --- /dev/null +++ b/samples/Jobs/JobsSample.hpp @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsSample.hpp + * @brief + * + */ + + +#pragma once + +#include "mqtt/Client.hpp" +#include "NetworkConnection.hpp" + +namespace awsiotsdk { + namespace samples { + class JobsSample { + protected: + std::shared_ptr p_network_connection_; + std::shared_ptr p_iot_client_; + std::shared_ptr p_jobs_; + std::atomic done_; + + ResponseCode DisconnectCallback(util::String topic_name, + std::shared_ptr p_app_handler_data); + ResponseCode ReconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode reconnect_result); + ResponseCode ResubscribeCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode resubscribe_result); + + ResponseCode GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode UpdateAcceptedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode UpdateRejectedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + + ResponseCode Subscribe(); + ResponseCode InitializeTLS(); + + public: + ResponseCode RunSample(); + }; + } +} + + diff --git a/samples/README.md b/samples/README.md index 3be0c30..00fdfb5 100644 --- a/samples/README.md +++ b/samples/README.md @@ -17,9 +17,15 @@ This sample demonstrates how various Shadow operations can be performed. * Code for this sample is located [here](./ShadowDelta) * Target for this sample is `shadow-delta-sample` - + Note: The shadow client token is set as the thing name by default in the sample. The shadow client token is limited to 64 bytes and will return an error if a token longer than 64 bytes is used (`"code":400,"message":"invalid client token"`, although receiving a 400 does not necessarily mean that it is due to the length of the client token). Modify the code [here](../ShadowDelta/ShadowDelta.cpp#L184) if your thing name is longer than 64 bytes to prevent this error. +### Jobs Sample +This sample demonstrates how various Jobs API operations can be performed including subscribing to Jobs notifications and publishing Job execution updates. + + * Code for this sample is located [here](./Jobs) + * Target for this sample is `jobs-sample` + ### Discovery Sample This sample demonstrates how the discovery operation can be performed to get the connectivity information to connect to a Greengrass Core (GGC). The configuration for this example is slightly different as the Discovery operation is a HTTP call, and uses port 8443, instead of port 8883 which is used for MQTT operations. The endpoint is the same IoT host endpoint used to connect the IoT thing to the cloud. diff --git a/src/ResponseCode.cpp b/src/ResponseCode.cpp index 2f3e21b..fad8420 100644 --- a/src/ResponseCode.cpp +++ b/src/ResponseCode.cpp @@ -343,6 +343,9 @@ namespace awsiotsdk { case ResponseCode::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR: os << awsiotsdk::ResponseHelper::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING; break; + case ResponseCode::JOBS_INVALID_TOPIC_ERROR: + os << awsiotsdk::ResponseHelper::JOBS_INVALID_TOPIC_ERROR_STRING; + break; } os << " : SDK Code " << static_cast(rc) << "."; return os; diff --git a/src/jobs/Jobs.cpp b/src/jobs/Jobs.cpp new file mode 100644 index 0000000..902ec91 --- /dev/null +++ b/src/jobs/Jobs.cpp @@ -0,0 +1,340 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.cpp + * @brief + * + */ + +#include "util/logging/LogMacros.hpp" + +#include "jobs/Jobs.hpp" + +#define BASE_THINGS_TOPIC "$aws/things/" + +#define NOTIFY_OPERATION "notify" +#define NOTIFY_NEXT_OPERATION "notify-next" +#define GET_OPERATION "get" +#define START_NEXT_OPERATION "start-next" +#define WILDCARD_OPERATION "+" +#define UPDATE_OPERATION "update" +#define ACCEPTED_REPLY "accepted" +#define REJECTED_REPLY "rejected" +#define WILDCARD_REPLY "#" + +namespace awsiotsdk { + std::unique_ptr Jobs::Create(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token) { + if (nullptr == p_mqtt_client) { + return nullptr; + } + + return std::unique_ptr(new Jobs(p_mqtt_client, qos, thing_name, client_token)); + } + + Jobs::Jobs(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token) { + p_mqtt_client_ = p_mqtt_client; + qos_ = qos; + thing_name_ = thing_name; + client_token_ = client_token; + }; + + bool Jobs::BaseTopicRequiresJobId(JobExecutionTopicType topicType) { + switch (topicType) { + case JOB_UPDATE_TOPIC: + case JOB_DESCRIBE_TOPIC: + return true; + case JOB_NOTIFY_TOPIC: + case JOB_NOTIFY_NEXT_TOPIC: + case JOB_START_NEXT_TOPIC: + case JOB_GET_PENDING_TOPIC: + case JOB_WILDCARD_TOPIC: + case JOB_UNRECOGNIZED_TOPIC: + default: + return false; + } + }; + + const util::String Jobs::GetOperationForBaseTopic(JobExecutionTopicType topicType) { + switch (topicType) { + case JOB_UPDATE_TOPIC: + return UPDATE_OPERATION; + case JOB_NOTIFY_TOPIC: + return NOTIFY_OPERATION; + case JOB_NOTIFY_NEXT_TOPIC: + return NOTIFY_NEXT_OPERATION; + case JOB_GET_PENDING_TOPIC: + case JOB_DESCRIBE_TOPIC: + return GET_OPERATION; + case JOB_START_NEXT_TOPIC: + return START_NEXT_OPERATION; + case JOB_WILDCARD_TOPIC: + return WILDCARD_OPERATION; + case JOB_UNRECOGNIZED_TOPIC: + default: + return ""; + } + }; + + const util::String Jobs::GetSuffixForTopicType(JobExecutionTopicReplyType replyType) { + switch (replyType) { + case JOB_REQUEST_TYPE: + return ""; + case JOB_ACCEPTED_REPLY_TYPE: + return "/" ACCEPTED_REPLY; + case JOB_REJECTED_REPLY_TYPE: + return "/" REJECTED_REPLY; + case JOB_WILDCARD_REPLY_TYPE: + return "/" WILDCARD_REPLY; + case JOB_UNRECOGNIZED_TOPIC_TYPE: + default: + return ""; + } + } + + const util::String Jobs::GetExecutionStatus(JobExecutionStatus status) { + switch (status) { + case JOB_EXECUTION_QUEUED: + return "QUEUED"; + case JOB_EXECUTION_IN_PROGRESS: + return "IN_PROGRESS"; + case JOB_EXECUTION_FAILED: + return "FAILED"; + case JOB_EXECUTION_SUCCEEDED: + return "SUCCEEDED"; + case JOB_EXECUTION_CANCELED: + return "CANCELED"; + case JOB_EXECUTION_REJECTED: + return "REJECTED"; + case JOB_EXECUTION_STATUS_NOT_SET: + case JOB_EXECUTION_UNKNOWN_STATUS: + default: + return ""; + } + } + + util::String Jobs::Escape(const util::String &value) { + util::String result = ""; + + for (int i = 0; i < value.length(); i++) { + switch(value[i]) { + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + case '"': result += "\\\""; break; + case '\\': result += "\\\\"; break; + default: result += value[i]; + } + } + return result; + } + + util::String Jobs::SerializeStatusDetails(const util::Map &statusDetailsMap) { + util::String result = "{"; + + util::Map::const_iterator itr = statusDetailsMap.begin(); + while (itr != statusDetailsMap.end()) { + result += (itr == statusDetailsMap.begin() ? "\"" : ",\""); + result += Escape(itr->first) + "\":\"" + Escape(itr->second) + "\""; + itr++; + } + + result += '}'; + return result; + } + + std::unique_ptr Jobs::GetJobTopic(JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType, + const util::String &jobId) { + if (thing_name_.empty()) { + return nullptr; + } + + if ((topicType == JOB_NOTIFY_TOPIC || topicType == JOB_NOTIFY_NEXT_TOPIC) && replyType != JOB_REQUEST_TYPE) { + return nullptr; + } + + if ((topicType == JOB_GET_PENDING_TOPIC || topicType == JOB_START_NEXT_TOPIC || + topicType == JOB_NOTIFY_TOPIC || topicType == JOB_NOTIFY_NEXT_TOPIC) && !jobId.empty()) { + return nullptr; + } + + const bool requireJobId = BaseTopicRequiresJobId(topicType); + if (jobId.empty() && requireJobId) { + return nullptr; + } + + const util::String operation = GetOperationForBaseTopic(topicType); + if (operation.empty()) { + return nullptr; + } + + const util::String suffix = GetSuffixForTopicType(replyType); + + if (requireJobId) { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/" + jobId + '/' + operation + suffix); + } else if (topicType == JOB_WILDCARD_TOPIC) { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/#"); + } else { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/" + operation + suffix); + } + }; + + util::String Jobs::SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap, + int64_t expectedVersion, // set to 0 to ignore + int64_t executionNumber, // set to 0 to ignore + bool includeJobExecutionState, + bool includeJobDocument) { + const util::String executionStatus = GetExecutionStatus(status); + + if (executionStatus.empty()) { + return ""; + } + + util::String result = "{\"status\":\"" + executionStatus + "\""; + if (!statusDetailsMap.empty()) { + result += ",\"statusDetails\":" + SerializeStatusDetails(statusDetailsMap); + } + if (expectedVersion > 0) { + result += ",\"expectedVersion\":\"" + std::to_string(expectedVersion) + "\""; + } + if (executionNumber > 0) { + result += ",\"executionNumber\":\"" + std::to_string(executionNumber) + "\""; + } + if (includeJobExecutionState) { + result += ",\"includeJobExecutionState\":\"true\""; + } + if (includeJobDocument) { + result += ",\"includeJobDocument\":\"true\""; + } + if (!client_token_.empty()) { + result += ",\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeDescribeJobExecutionPayload(int64_t executionNumber, // set to 0 to ignore + bool includeJobDocument) { + util::String result = "{\"includeJobDocument\":\""; + result += (includeJobDocument ? "true" : "false"); + result += "\""; + if (executionNumber > 0) { + result += ",\"executionNumber\":\"" + std::to_string(executionNumber) + "\""; + } + if (!client_token_.empty()) { + result += "\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap) { + util::String result = "{"; + if (!statusDetailsMap.empty()) { + result += "\"statusDetails\":" + SerializeStatusDetails(statusDetailsMap); + } + if (!client_token_.empty()) { + if (!statusDetailsMap.empty()) { + result += ','; + } + result += "\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeClientTokenPayload() { + if (!client_token_.empty()) { + return "{\"clientToken\":\"" + client_token_ + "\"}"; + } + + return "{}"; + }; + + ResponseCode Jobs::SendJobsQuery(JobExecutionTopicType topicType, + const util::String &jobId) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(topicType, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeClientTokenPayload(), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsStartNext(const util::Map &statusDetailsMap) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_START_NEXT_TOPIC, JOB_REQUEST_TYPE); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeStartNextPendingJobExecutionPayload(statusDetailsMap), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsDescribe(const util::String &jobId, + int64_t executionNumber, // set to 0 to ignore + bool includeJobDocument) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_DESCRIBE_TOPIC, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeDescribeJobExecutionPayload(executionNumber, includeJobDocument), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsUpdate(const util::String &jobId, + JobExecutionStatus status, + const util::Map &statusDetailsMap, + int64_t expectedVersion, // set to 0 to ignore + int64_t executionNumber, // set to 0 to ignore + bool includeJobExecutionState, + bool includeJobDocument) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_UPDATE_TOPIC, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, + SerializeJobExecutionUpdatePayload(status, statusDetailsMap, expectedVersion, executionNumber, + includeJobExecutionState, includeJobDocument), + nullptr, packet_id); + }; + + std::shared_ptr Jobs::CreateJobsSubscription(mqtt::Subscription::ApplicationCallbackHandlerPtr p_app_handler, + std::shared_ptr p_app_handler_data, + JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType, + const util::String &jobId) { + return mqtt::Subscription::Create(GetJobTopic(topicType, replyType, jobId), qos_, p_app_handler, p_app_handler_data); + }; +} diff --git a/tests/integration/include/JobsTest.hpp b/tests/integration/include/JobsTest.hpp new file mode 100644 index 0000000..e958fb7 --- /dev/null +++ b/tests/integration/include/JobsTest.hpp @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.hpp + * @brief + * + */ + + +#pragma once + +#include "mqtt/Client.hpp" +#include "NetworkConnection.hpp" +#include "jobs/Jobs.hpp" + +namespace awsiotsdk { + namespace tests { + namespace integration { + class JobsTest { + protected: + static const std::chrono::seconds keep_alive_timeout_; + + std::shared_ptr p_network_connection_; + std::shared_ptr p_iot_client_; + std::shared_ptr p_jobs_; + std::atomic done_; + + ResponseCode GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + + ResponseCode Subscribe(); + ResponseCode Unsubscribe(); + ResponseCode InitializeTLS(); + + public: + ResponseCode RunTest(); + }; + } + } +} + + diff --git a/tests/integration/src/IntegTestRunner.cpp b/tests/integration/src/IntegTestRunner.cpp index fc6c160..73eef3b 100644 --- a/tests/integration/src/IntegTestRunner.cpp +++ b/tests/integration/src/IntegTestRunner.cpp @@ -28,6 +28,7 @@ #include "ConfigCommon.hpp" #include "IntegTestRunner.hpp" #include "SdkTestConfig.hpp" +#include "JobsTest.hpp" #include "PubSub.hpp" #include "AutoReconnect.hpp" #include "MultipleClients.hpp" @@ -53,6 +54,17 @@ namespace awsiotsdk { ResponseCode IntegTestRunner::RunAllTests() { ResponseCode rc = ResponseCode::SUCCESS; // Each test runs in its own scope to ensure complete cleanup + /** + * Run Jobs Tests + */ + { + JobsTest jobs_test_runner; + rc = jobs_test_runner.RunTest(); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + } + /** * Run Subscribe Publish Tests */ diff --git a/tests/integration/src/JobsTest.cpp b/tests/integration/src/JobsTest.cpp new file mode 100644 index 0000000..3e913df --- /dev/null +++ b/tests/integration/src/JobsTest.cpp @@ -0,0 +1,327 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsTest.cpp + * @brief + * + */ + +#include "JobsTest.hpp" +#include "util/logging/LogMacros.hpp" + +#include +#include + +#ifdef USE_WEBSOCKETS +#include "WebSocketConnection.hpp" +#elif defined USE_MBEDTLS +#include "MbedTLSConnection.hpp" +#else +#include "OpenSSLConnection.hpp" +#endif + +#include "ConfigCommon.hpp" + +#define JOBS_INTEGRATION_TEST_TAG "[Integration Test - Jobs]" + +namespace awsiotsdk { + namespace tests { + namespace integration { + ResponseCode JobsTest::GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "GetPendingCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Json Parse for GetPendingCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("inProgressJobs")) { + std::cout << "inProgressJobs : " << util::JsonParser::ToString(doc["inProgressJobs"]) << std::endl; + } + + if (doc.HasMember("queuedJobs")) { + std::cout << "queuedJobs : " << util::JsonParser::ToString(doc["queuedJobs"]) << std::endl; + } + + std::cout << "************" << std::endl; + + rc = p_jobs_->SendJobsQuery(Jobs::JOB_DESCRIBE_TOPIC, "$next"); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + + return ResponseCode::FAILURE; + } + + return ResponseCode::SUCCESS; + } + + ResponseCode JobsTest::NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "NextJobCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Json Parse for NextJobCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("execution")) { + std::cout << "execution : " << util::JsonParser::ToString(doc["execution"]) << std::endl; + + if (doc["execution"].HasMember("jobId")) { + util::Map statusDetailsMap; + + util::String jobId = doc["execution"]["jobId"].GetString(); + std::cout << "jobId : " << jobId << std::endl; + + if (doc["execution"].HasMember("jobDocument")) { + std::cout << "jobDocument : " << util::JsonParser::ToString(doc["execution"]["jobDocument"]) << std::endl; + statusDetailsMap.insert(std::make_pair("exampleDetail", "a value appropriate for your successful job")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_SUCCEEDED, statusDetailsMap); + } else { + statusDetailsMap.insert(std::make_pair("failureDetail", "Unable to process job document")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_FAILED, statusDetailsMap); + } + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsUpdate failed. %s", ResponseHelper::ToString(rc).c_str()); + return rc; + } + } + } else { + std::cout << "No job execution description found, nothing to do." << std::endl; + done_ = true; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsTest::Subscribe() { + std::cout << "******** Subscribe ***************" << std::endl; + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_pending_handler = + std::bind(&JobsTest::GetPendingCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_next_handler = + std::bind(&JobsTest::NextJobCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + util::Vector> topic_vector; + std::shared_ptr p_subscription; + + p_subscription = p_jobs_->CreateJobsSubscription(p_pending_handler, nullptr, Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(p_subscription); + + ResponseCode rc = p_iot_client_->Subscribe(topic_vector, ConfigCommon::mqtt_command_timeout_); + std::this_thread::sleep_for(std::chrono::seconds(3)); + return rc; + } + + ResponseCode JobsTest::Unsubscribe() { + uint16_t packet_id = 0; + std::unique_ptr p_topic_name; + util::Vector> topic_vector; + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(std::move(p_topic_name)); + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(std::move(p_topic_name)); + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(std::move(p_topic_name)); + + ResponseCode rc = p_iot_client_->UnsubscribeAsync(std::move(topic_vector), nullptr, packet_id); + std::this_thread::sleep_for(std::chrono::seconds(1)); + return rc; + } + + ResponseCode JobsTest::InitializeTLS() { + ResponseCode rc = ResponseCode::SUCCESS; + +#ifdef USE_WEBSOCKETS + p_network_connection_ = std::shared_ptr( + new network::WebSocketConnection(ConfigCommon::endpoint_, ConfigCommon::endpoint_https_port_, + ConfigCommon::root_ca_path_, ConfigCommon::aws_region_, + ConfigCommon::aws_access_key_id_, + ConfigCommon::aws_secret_access_key_, + ConfigCommon::aws_session_token_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true)); +#elif defined USE_MBEDTLS + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + + if (ResponseCode::SUCCESS != rc) { + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#else + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + rc = p_network_connection->Initialize(); + + if (ResponseCode::SUCCESS != rc) { + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#endif + return rc; + } + + ResponseCode JobsTest::RunTest() { + done_ = false; + ResponseCode rc = InitializeTLS(); + + do { + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Failed to initialize TLS layer. %s", + ResponseHelper::ToString(rc).c_str()); + break; + } + + p_iot_client_ = std::shared_ptr( + MqttClient::Create(p_network_connection_, ConfigCommon::mqtt_command_timeout_)); + if (nullptr == p_iot_client_) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Failed to create MQTT Client Instance!!"); + rc = ResponseCode::FAILURE; + break; + } + + util::String client_id_tagged = ConfigCommon::base_client_id_; + client_id_tagged.append("_jobs_tester_"); + client_id_tagged.append(std::to_string(rand())); + std::unique_ptr client_id = Utf8String::Create(client_id_tagged); + + rc = p_iot_client_->Connect(ConfigCommon::mqtt_command_timeout_, ConfigCommon::is_clean_session_, + mqtt::Version::MQTT_3_1_1, ConfigCommon::keep_alive_timeout_secs_, + std::move(client_id), nullptr, nullptr, nullptr); + + p_jobs_ = Jobs::Create(p_iot_client_, mqtt::QoS::QOS1, ConfigCommon::thing_name_, client_id_tagged); + + if (ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "MQTT Connect failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + rc = Subscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Subscribe failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + break; + } + + rc = p_jobs_->SendJobsQuery(Jobs::JOB_GET_PENDING_TOPIC); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + } + + int retries = 5; + while (!done_ && retries-- > 0) { + std::this_thread::sleep_for(std::chrono::seconds(5)); + } + + if (!done_) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Not all jobs processed."); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + rc = ResponseCode::FAILURE; + break; + } + + rc = Unsubscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Unsubscribe failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + break; + } + + rc = p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Disconnect failed. %s", + ResponseHelper::ToString(rc).c_str()); + break; + } + } while (false); + + std::cout << std::endl; + if (ResponseCode::SUCCESS != rc) { + std::cout + << "Test Failed!!!! See above output for details!!" + << std::endl; + std::cout << "**********************************************************" << std::endl; + return ResponseCode::FAILURE; + } + + std::cout << "Test Successful!!!!" << std::endl; + std::cout << "**********************************************************" << std::endl; + return ResponseCode::SUCCESS; + } + } + } +} diff --git a/tests/unit/src/ResponseCodeTests.cpp b/tests/unit/src/ResponseCodeTests.cpp index 0d6f5cc..ada914e 100644 --- a/tests/unit/src/ResponseCodeTests.cpp +++ b/tests/unit/src/ResponseCodeTests.cpp @@ -570,6 +570,11 @@ namespace awsiotsdk { expected_string = ResponseCodeToString(ResponseHelper::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING, ResponseCode::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR); EXPECT_EQ(expected_string, response_string); + + response_string = ResponseHelper::ToString(ResponseCode::JOBS_INVALID_TOPIC_ERROR); + expected_string = ResponseCodeToString(ResponseHelper::JOBS_INVALID_TOPIC_ERROR_STRING, + ResponseCode::JOBS_INVALID_TOPIC_ERROR); + EXPECT_EQ(expected_string, response_string); } } } diff --git a/tests/unit/src/jobs/JobsTests.cpp b/tests/unit/src/jobs/JobsTests.cpp new file mode 100644 index 0000000..56b5161 --- /dev/null +++ b/tests/unit/src/jobs/JobsTests.cpp @@ -0,0 +1,264 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsTests.cpp + * @brief + * + */ + +#include + +#include + +#include "util/logging/LogMacros.hpp" + +#include "TestHelper.hpp" +#include "MockNetworkConnection.hpp" + +#include "jobs/Jobs.hpp" +#include "mqtt/ClientState.hpp" + +#define JOBS_TEST_LOG_TAG "[Jobs Unit Test]" + +namespace awsiotsdk { + namespace tests { + namespace unit { + class JobsTestWrapper : public Jobs { + protected: + static const util::String test_thing_name_; + static const util::String client_token_; + + public: + JobsTestWrapper(bool empty_thing_name, bool empty_client_token): + Jobs(nullptr, mqtt::QoS::QOS0, + empty_thing_name ? "" : test_thing_name_, + empty_client_token ? "" : client_token_) {} + + util::String SerializeStatusDetails(const util::Map &statusDetailsMap) { + return Jobs::SerializeStatusDetails(statusDetailsMap); + } + + util::String SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, + int64_t executionNumber = 0, + bool includeJobExecutionState = false, + bool includeJobDocument = false) { + return Jobs::SerializeJobExecutionUpdatePayload(status, statusDetailsMap, expectedVersion, executionNumber, includeJobExecutionState, includeJobDocument); + } + + util::String SerializeDescribeJobExecutionPayload(int64_t executionNumber = 0, + bool includeJobDocument = true) { + return Jobs::SerializeDescribeJobExecutionPayload(executionNumber, includeJobDocument); + } + + util::String SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap = util::Map()) { + return Jobs::SerializeStartNextPendingJobExecutionPayload(statusDetailsMap); + } + + util::String SerializeClientTokenPayload() { + return Jobs::SerializeClientTokenPayload(); + } + + util::String Escape(const util::String &value) { + return Jobs::Escape(value); + } + }; + + const util::String JobsTestWrapper::test_thing_name_ = "CppSdkTestClient"; + const util::String JobsTestWrapper::client_token_ = "CppSdkTestClientToken"; + + class JobsTester : public ::testing::Test { + protected: + static const util::String job_id_; + + std::shared_ptr p_jobs_; + std::shared_ptr p_jobs_empty_client_token_; + std::shared_ptr p_jobs_empty_thing_name_; + + JobsTester() { + p_jobs_ = std::shared_ptr(new JobsTestWrapper(false, false)); + p_jobs_empty_client_token_ = std::shared_ptr(new JobsTestWrapper(false, true)); + p_jobs_empty_thing_name_ = std::shared_ptr(new JobsTestWrapper(true, false)); + } + }; + + const util::String JobsTester::job_id_ = "TestJobId"; + + TEST_F(JobsTester, ValidTopicsTests) { + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/accepted", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/rejected", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/#", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/accepted", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/rejected", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/#", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/accepted", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/rejected", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/#", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/accepted", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/rejected", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/#", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/notify", p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/notify-next", p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + } + + TEST_F(JobsTester, InvalidTopicsTests) { + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + } + + + TEST_F(JobsTester, PayloadSerializationTests) { + util::Map statusDetailsMap; + statusDetailsMap.insert(std::make_pair("testKey", "testVal")); + + EXPECT_EQ("{}", p_jobs_empty_client_token_->SerializeClientTokenPayload()); + EXPECT_EQ("{\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeClientTokenPayload()); + + EXPECT_EQ("{}", p_jobs_empty_client_token_->SerializeStartNextPendingJobExecutionPayload()); + EXPECT_EQ("{\"statusDetails\":{\"testKey\":\"testVal\"}}", p_jobs_empty_client_token_->SerializeStartNextPendingJobExecutionPayload(statusDetailsMap)); + EXPECT_EQ("{\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeStartNextPendingJobExecutionPayload()); + EXPECT_EQ("{\"statusDetails\":{\"testKey\":\"testVal\"},\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeStartNextPendingJobExecutionPayload(statusDetailsMap)); + + EXPECT_EQ("{\"includeJobDocument\":\"true\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload()); + EXPECT_EQ("{\"includeJobDocument\":\"true\",\"executionNumber\":\"1\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload(1)); + EXPECT_EQ("{\"includeJobDocument\":\"false\",\"executionNumber\":\"1\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload(1, false)); + + EXPECT_EQ("{\"includeJobDocument\":\"true\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload()); + EXPECT_EQ("{\"includeJobDocument\":\"true\",\"executionNumber\":\"1\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload(1)); + EXPECT_EQ("{\"includeJobDocument\":\"false\",\"executionNumber\":\"1\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload(1, false)); + + EXPECT_EQ("", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_STATUS_NOT_SET)); + EXPECT_EQ("", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_UNKNOWN_STATUS)); + EXPECT_EQ("", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_STATUS_NOT_SET)); + EXPECT_EQ("", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_UNKNOWN_STATUS)); + + EXPECT_EQ("{\"status\":\"QUEUED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"}}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"includeJobDocument\":\"true\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true, true)); + + EXPECT_EQ("{\"status\":\"IN_PROGRESS\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_IN_PROGRESS)); + EXPECT_EQ("{\"status\":\"FAILED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_FAILED)); + EXPECT_EQ("{\"status\":\"SUCCEEDED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_SUCCEEDED)); + EXPECT_EQ("{\"status\":\"CANCELED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_CANCELED)); + EXPECT_EQ("{\"status\":\"REJECTED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_REJECTED)); + + EXPECT_EQ("{\"status\":\"QUEUED\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"includeJobDocument\":\"true\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true, true)); + + EXPECT_EQ("{\"status\":\"IN_PROGRESS\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_IN_PROGRESS)); + EXPECT_EQ("{\"status\":\"FAILED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_FAILED)); + EXPECT_EQ("{\"status\":\"SUCCEEDED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_SUCCEEDED)); + EXPECT_EQ("{\"status\":\"CANCELED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_CANCELED)); + EXPECT_EQ("{\"status\":\"REJECTED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_REJECTED)); + + statusDetailsMap.insert(std::make_pair("testEscapeKey \" \t \r \n \\ '!", "testEscapeVal \" \t \r \n \\ '!")); + EXPECT_EQ("{\"testEscapeKey \\\" \\t \\r \\n \\\\ '!\":\"testEscapeVal \\\" \\t \\r \\n \\\\ '!\",\"testKey\":\"testVal\"}", p_jobs_->SerializeStatusDetails(statusDetailsMap)); + } + } + } +} From e90ff3fca3332e7097ecbbb847b97cf01f9ca684 Mon Sep 17 00:00:00 2001 From: Neel Kowdley Date: Tue, 20 Mar 2018 16:38:20 -0400 Subject: [PATCH 6/6] Allow Position Independent Code for Static Library (#73) Build the static library with the fPIC compiler flag, so that it can be linked into a shared library. Fix wildcard regex for special topics with $ symbols Adding standard files (#83) Pull requests to fix warnings on Windows Includes #75, #76 and #77. Also includes other changes related to - loading and storing of atomic variables - fix for UTF-8 character representation on Windows Update sample documentation with shadow client token limitation Make Shadow::HandleGetResponse call response handler on Rejected response Fixes #86 Make Shadow::HandleGetResponse call response handler on malformed payload Jobs support with custom auth support Wrap SIGPIPE in ifndef block for Windows Allow Position Independent Code for Static Library (#73) Build the static library with the fPIC compiler flag, so that it can be linked into a shared library. Fix wildcard regex for special topics with $ symbols Adding standard files (#83) Pull requests to fix warnings on Windows Includes #75, #76 and #77. Also includes other changes related to - loading and storing of atomic variables - fix for UTF-8 character representation on Windows Update sample documentation with shadow client token limitation Make Shadow::HandleGetResponse call response handler on Rejected response Fixes #86 Jobs support with custom auth support Wrap SIGPIPE in ifndef block for Windows Allow Position Independent Code for Static Library (#73) Build the static library with the fPIC compiler flag, so that it can be linked into a shared library. Fix wildcard regex for special topics with $ symbols Adding standard files (#83) Pull requests to fix warnings on Windows Includes #75, #76 and #77. Also includes other changes related to - loading and storing of atomic variables - fix for UTF-8 character representation on Windows Update sample documentation with shadow client token limitation Make Shadow::HandleGetResponse call response handler on Rejected response Fixes #86 Make Shadow::HandleGetResponse call response handler on malformed payload Jobs support with custom auth support Wrap SIGPIPE in ifndef block for Windows Allow Position Independent Code for Static Library (#73) Build the static library with the fPIC compiler flag, so that it can be linked into a shared library. Fix wildcard regex for special topics with $ symbols Adding standard files (#83) Pull requests to fix warnings on Windows Includes #75, #76 and #77. Also includes other changes related to - loading and storing of atomic variables - fix for UTF-8 character representation on Windows Update sample documentation with shadow client token limitation Make Shadow::HandleGetResponse call response handler on Rejected response Fixes #86 Make Shadow::HandleGetResponse call response handler on malformed payload indentation fixup Wrap SIGPIPE in ifndef block for Windows Allow Position Independent Code for Static Library (#73) Build the static library with the fPIC compiler flag, so that it can be linked into a shared library. Fix wildcard regex for special topics with $ symbols Adding standard files (#83) Pull requests to fix warnings on Windows Includes #75, #76 and #77. Also includes other changes related to - loading and storing of atomic variables - fix for UTF-8 character representation on Windows Update sample documentation with shadow client token limitation Make Shadow::HandleGetResponse call response handler on Rejected response Fixes #86 Make Shadow::HandleGetResponse call response handler on malformed payload --- .github/PULL_REQUEST_TEMPLATE.md | 6 + CMakeLists.txt | 3 + CODE_OF_CONDUCT.md | 4 + CONTRIBUTING.md | 61 ++++ README.md | 3 + include/ResponseCode.hpp | 7 +- include/jobs/Jobs.hpp | 219 +++++++++++++ include/mqtt/Packet.hpp | 8 +- include/mqtt/Publish.hpp | 5 +- network/OpenSSL/OpenSSLConnection.cpp | 4 +- network/WebSocket/WebSocketConnection.cpp | 58 +++- network/WebSocket/WebSocketConnection.hpp | 29 ++ samples/Jobs/CMakeLists.txt | 82 +++++ samples/Jobs/JobsSample.cpp | 383 ++++++++++++++++++++++ samples/Jobs/JobsSample.hpp | 68 ++++ samples/README.md | 10 +- src/ResponseCode.cpp | 3 + src/jobs/Jobs.cpp | 340 +++++++++++++++++++ src/mqtt/Common.cpp | 5 +- src/mqtt/Publish.cpp | 4 +- src/shadow/Shadow.cpp | 10 +- tests/integration/include/JobsTest.hpp | 59 ++++ tests/integration/src/IntegTestRunner.cpp | 12 + tests/integration/src/JobsTest.cpp | 327 ++++++++++++++++++ tests/unit/src/ResponseCodeTests.cpp | 5 + tests/unit/src/jobs/JobsTests.cpp | 264 +++++++++++++++ tests/unit/src/mqtt/SubscribeTests.cpp | 12 +- 27 files changed, 1956 insertions(+), 35 deletions(-) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 include/jobs/Jobs.hpp create mode 100644 samples/Jobs/CMakeLists.txt create mode 100644 samples/Jobs/JobsSample.cpp create mode 100644 samples/Jobs/JobsSample.hpp create mode 100644 src/jobs/Jobs.cpp create mode 100644 tests/integration/include/JobsTest.hpp create mode 100644 tests/integration/src/JobsTest.cpp create mode 100644 tests/unit/src/jobs/JobsTests.cpp diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ab40d21 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,6 @@ +*Issue #, if available:* + +*Description of changes:* + + +By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. diff --git a/CMakeLists.txt b/CMakeLists.txt index ad8a4e1..b09e572 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ if (BUILD_SHARED_LIBRARY) set_target_properties(${SDK_TARGET_NAME} PROPERTIES SUFFIX ".so") else() add_library(${SDK_TARGET_NAME} "") + set_target_properties(${SDK_TARGET_NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() # Download and include rapidjson, not optional @@ -161,6 +162,8 @@ add_subdirectory(tests/unit) add_subdirectory(samples/PubSub) +add_subdirectory(samples/Jobs) + add_subdirectory(samples/ShadowDelta) add_subdirectory(samples/Discovery EXCLUDE_FROM_ALL) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3b64466 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,4 @@ +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fbe001e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing Guidelines + +Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional +documentation, we greatly value feedback and contributions from our community. + +Please read through this document before submitting any issues or pull requests to ensure we have all the necessary +information to effectively respond to your bug report or contribution. + + +## Reporting Bugs/Feature Requests + +We welcome you to use the GitHub issue tracker to report bugs or suggest features. + +When filing an issue, please check [existing open](https://github.com/aws/aws-iot-device-sdk-cpp/issues), or [recently closed](https://github.com/aws/aws-iot-device-sdk-cpp/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already +reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: + +* A reproducible test case or series of steps +* The version of our code being used +* Any modifications you've made relevant to the bug +* Anything unusual about your environment or deployment + + +## Contributing via Pull Requests +Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: + +1. You are working against the latest source on the *master* branch. +2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. +3. You open an issue to discuss any significant work - we would hate for your time to be wasted. + +To send us a pull request, please: + +1. Fork the repository. +2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. +3. Ensure local tests pass. +4. Commit to your fork using clear commit messages. +5. Send us a pull request, answering any default questions in the pull request interface. +6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. + +GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and +[creating a pull request](https://help.github.com/articles/creating-a-pull-request/). + + +## Finding contributions to work on +Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws/aws-iot-device-sdk-cpp/labels/help%20wanted) issues is a great place to start. + + +## Code of Conduct +This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). +For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact +opensource-codeofconduct@amazon.com with any additional questions or comments. + + +## Security issue notifications +If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. + + +## Licensing + +See the [LICENSE](https://github.com/aws/aws-iot-device-sdk-cpp/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. + +We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. diff --git a/README.md b/README.md index cc72cb3..89fe3be 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,9 @@ The Device SDK provides functionality to create and maintain a MQTT Connection. ### Thing Shadow This SDK implements the specific protocol for Thing Shadows to retrieve, update and delete Thing Shadows adhering to the protocol that is implemented to ensure correct versioning and support for client tokens. It abstracts the necessary MQTT topic subscriptions by automatically subscribing to and unsubscribing from the reserved topics as needed for each API call. Inbound state change requests are automatically signalled via a configurable callback. +### Jobs +This SDK also implements the Jobs protocol to interact with the AWS IoT Jobs service. The IoT Job service manages deployment of IoT fleet wide tasks such as device software/firmware deployments and updates, rotation of security certificates, device reboots, and custom device specific management tasks. For additional information please see the [Jobs developer guide](https://docs.aws.amazon.com/iot/latest/developerguide/iot-jobs.html). + ## Design Goals of this SDK The C++ SDK was specifically designed for devices that are not resource constrained and required advanced features such as Message queueing, multi-threading support and the latest language features diff --git a/include/ResponseCode.hpp b/include/ResponseCode.hpp index 0a2e58c..2bfb7fa 100644 --- a/include/ResponseCode.hpp +++ b/include/ResponseCode.hpp @@ -193,7 +193,11 @@ namespace awsiotsdk { // Discovery Response Parsing Error Codes - DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR = -1200 ///< Discover Response Json is missing expected keys + DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR = -1200, ///< Discover Response Json is missing expected keys + + // Jobs Error Codes + + JOBS_INVALID_TOPIC_ERROR = -1300 ///< Jobs invalid topic }; /** @@ -314,6 +318,7 @@ namespace awsiotsdk { const util::String DISCOVER_ACTION_SERVER_ERROR_STRING("Server returned unknown error while performing the discovery action"); const util::String DISCOVER_ACTION_REQUEST_OVERLOAD_STRING("The discovery action is overloading the server, try again after some time"); const util::String DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING("The discover response JSON is incomplete "); + const util::String JOBS_INVALID_TOPIC_ERROR_STRING("Invalid jobs topic"); /** * Takes in a Response Code and returns the appropriate error/success string diff --git a/include/jobs/Jobs.hpp b/include/jobs/Jobs.hpp new file mode 100644 index 0000000..33c2130 --- /dev/null +++ b/include/jobs/Jobs.hpp @@ -0,0 +1,219 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.hpp + * @brief + * + */ + +#pragma once + +#include "mqtt/Client.hpp" + +namespace awsiotsdk { + class Jobs { + public: + // Disabling default and copy constructors. + Jobs() = delete; // Delete Default constructor + Jobs(const Jobs &) = delete; // Delete Copy constructor + Jobs(Jobs &&) = default; // Default Move constructor + Jobs &operator=(const Jobs &) & = delete; // Delete Copy assignment operator + Jobs &operator=(Jobs &&) & = default; // Default Move assignment operator + + /** + * @brief Create factory method. Returns a unique instance of Jobs + * + * @param p_mqtt_client - mqtt client + * @param qos - QoS + * @param thing_name - Thing name + * @param client_token - Client token for correlating messages (optional) + * + * @return std::unique_ptr pointing to a unique Jobs instance + */ + static std::unique_ptr Create(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token = util::String()); + + enum JobExecutionTopicType { + JOB_UNRECOGNIZED_TOPIC = 0, + JOB_GET_PENDING_TOPIC, + JOB_START_NEXT_TOPIC, + JOB_DESCRIBE_TOPIC, + JOB_UPDATE_TOPIC, + JOB_NOTIFY_TOPIC, + JOB_NOTIFY_NEXT_TOPIC, + JOB_WILDCARD_TOPIC + }; + + enum JobExecutionTopicReplyType { + JOB_UNRECOGNIZED_TOPIC_TYPE = 0, + JOB_REQUEST_TYPE, + JOB_ACCEPTED_REPLY_TYPE, + JOB_REJECTED_REPLY_TYPE, + JOB_WILDCARD_REPLY_TYPE + }; + + enum JobExecutionStatus { + JOB_EXECUTION_STATUS_NOT_SET = 0, + JOB_EXECUTION_QUEUED, + JOB_EXECUTION_IN_PROGRESS, + JOB_EXECUTION_FAILED, + JOB_EXECUTION_SUCCEEDED, + JOB_EXECUTION_CANCELED, + JOB_EXECUTION_REJECTED, + /*** + * Used for any status not in the supported list of statuses + */ + JOB_EXECUTION_UNKNOWN_STATUS = 99 + }; + + /** + * @brief GetJobTopic + * + * This function creates a job topic based on the provided parameters. + * + * @param topicType - Jobs topic type + * @param replyType - Topic reply type (optional) + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * + * @return nullptr on error, unique_ptr pointing to a topic string if successful + */ + std::unique_ptr GetJobTopic(JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType = JOB_REQUEST_TYPE, + const util::String &jobId = util::String()); + + /** + * @brief SendJobsQuery + * + * Send a query to the Jobs service using the provided mqtt client + * + * @param topicType - Jobs topic type for type of query + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsQuery(JobExecutionTopicType topicType, + const util::String &jobId = util::String()); + + /** + * @brief SendJobsStartNext + * + * Call Jobs start-next API to start the next pending job execution and trigger response + * + * @param statusDetails - Status details to be associated with started job execution (optional) + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsStartNext(const util::Map &statusDetailsMap = util::Map()); + + /** + * @brief SendJobsDescribe + * + * Send request for job execution details + * + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also + * be omitted to request all pending and in progress job executions + * @param executionNumber - Specific execution number to describe, omit to match latest + * @param includeJobDocument - Flag to indicate whether response should include job document + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsDescribe(const util::String &jobId = util::String(), + int64_t executionNumber = 0, // set to 0 to ignore + bool includeJobDocument = true); + + /** + * @brief SendJobsUpdate + * + * Send update for specified job + * + * @param jobId - Job id associated with job execution to be updated + * @param status - New job execution status + * @param statusDetailsMap - Status details to be associated with job execution (optional) + * @param expectedVersion - Optional expected current job execution number, error response if mismatched + * @param executionNumber - Specific execution number to update, omit to match latest + * @param includeJobExecutionState - Include job execution state in response (optional) + * @param includeJobDocument - Include job document in response (optional) + * + * @return ResponseCode indicating status of publish request + */ + ResponseCode SendJobsUpdate(const util::String &jobId, + JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, // set to 0 to ignore + int64_t executionNumber = 0, // set to 0 to ignore + bool includeJobExecutionState = false, + bool includeJobDocument = false); + + /** + * @brief CreateJobsSubscription + * + * Create a Jobs Subscription instance + * + * @param p_app_handler - Application Handler instance + * @param p_app_handler_data - Data to be passed to application handler. Can be nullptr + * @param topicType - Jobs topic type to subscribe to (defaults to JOB_WILDCARD_TOPIC) + * @param jobId - Job id, can be $next to indicate next queued or in process job, can also be omitted if N/A + * @param replyType - Topic reply type (optional, defaults to JOB_REQUEST_TYPE which omits the reply type in the subscription) + * + * @return shared_ptr Subscription instance + */ + std::shared_ptr CreateJobsSubscription(mqtt::Subscription::ApplicationCallbackHandlerPtr p_app_handler, + std::shared_ptr p_app_handler_data, + JobExecutionTopicType topicType = JOB_WILDCARD_TOPIC, + JobExecutionTopicReplyType replyType = JOB_REQUEST_TYPE, + const util::String &jobId = util::String()); + protected: + std::shared_ptr p_mqtt_client_; + mqtt::QoS qos_; + util::String thing_name_; + util::String client_token_; + + /** + * @brief Jobs constructor + * + * Create Jobs object storing given parameters in created instance + * + * @param p_mqtt_client - mqtt client + * @param qos - QoS + * @param thing_name - Thing name + * @param client_token - Client token for correlating messages (optional) + */ + Jobs(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token); + + static bool BaseTopicRequiresJobId(JobExecutionTopicType topicType); + static const util::String GetOperationForBaseTopic(JobExecutionTopicType topicType); + static const util::String GetSuffixForTopicType(JobExecutionTopicReplyType replyType); + static const util::String GetExecutionStatus(JobExecutionStatus status); + static util::String Escape(const util::String &value); + static util::String SerializeStatusDetails(const util::Map &statusDetailsMap); + + util::String SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, + int64_t executionNumber = 0, + bool includeJobExecutionState = false, + bool includeJobDocument = false); + util::String SerializeDescribeJobExecutionPayload(int64_t executionNumber = 0, + bool includeJobDocument = true); + util::String SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap = util::Map()); + util::String SerializeClientTokenPayload(); + }; +} diff --git a/include/mqtt/Packet.hpp b/include/mqtt/Packet.hpp index 767699f..c09058c 100644 --- a/include/mqtt/Packet.hpp +++ b/include/mqtt/Packet.hpp @@ -118,12 +118,12 @@ namespace awsiotsdk { std::atomic_uint_fast16_t packet_id_; ///< Message sequence identifier. Handled automatically by the MQTT client public: - uint16_t GetActionId() { return packet_id_; } - void SetActionId(uint16_t action_id) { packet_id_ = action_id; } + uint16_t GetActionId() { return (uint16_t) packet_id_.load(std::memory_order_relaxed); } + void SetActionId(uint16_t action_id) { packet_id_.store(action_id, std::memory_order_relaxed); } bool isPacketDataValid(); - uint16_t GetPacketId() { return packet_id_; } - void SetPacketId(uint16_t packet_id) { packet_id_ = packet_id; } + uint16_t GetPacketId() { return (uint16_t) packet_id_.load(std::memory_order_relaxed); } + void SetPacketId(uint16_t packet_id) { packet_id_.store(packet_id, std::memory_order_relaxed); } size_t Size() { return serialized_packet_length_; } diff --git a/include/mqtt/Publish.hpp b/include/mqtt/Publish.hpp index 202550f..c51058c 100644 --- a/include/mqtt/Publish.hpp +++ b/include/mqtt/Publish.hpp @@ -201,8 +201,9 @@ namespace awsiotsdk { */ util::String ToString(); - uint16_t GetPublishPacketId() { return publish_packet_id_; } - void SetPublishPacketId(uint16_t publish_packet_id) { publish_packet_id_ = publish_packet_id; } + uint16_t GetPublishPacketId() { return (uint16_t) publish_packet_id_.load(std::memory_order_relaxed); } + void SetPublishPacketId(uint16_t publish_packet_id) { publish_packet_id_.store(publish_packet_id, + std::memory_order_relaxed); } }; /** diff --git a/network/OpenSSL/OpenSSLConnection.cpp b/network/OpenSSL/OpenSSLConnection.cpp index 6c6ff8b..05f11f6 100644 --- a/network/OpenSSL/OpenSSLConnection.cpp +++ b/network/OpenSSL/OpenSSLConnection.cpp @@ -130,7 +130,7 @@ namespace awsiotsdk { WSADATA wsa_data; int result; bool was_wsa_initialized = true; - int s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + UINT_PTR s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(INVALID_SOCKET == s) { if(WSANOTINITIALISED == WSAGetLastError()) { was_wsa_initialized = false; @@ -338,7 +338,7 @@ namespace awsiotsdk { // Configure a non-zero callback if desired SSL_set_verify(p_ssl_handle_, SSL_VERIFY_PEER, nullptr); - server_tcp_socket_fd_ = socket(AF_INET, SOCK_STREAM, 0); + server_tcp_socket_fd_ = (int)socket(AF_INET, SOCK_STREAM, 0); if (-1 == server_tcp_socket_fd_) { return ResponseCode::NETWORK_TCP_SETUP_ERROR; } diff --git a/network/WebSocket/WebSocketConnection.cpp b/network/WebSocket/WebSocketConnection.cpp index 847638b..9214ffd 100644 --- a/network/WebSocket/WebSocketConnection.cpp +++ b/network/WebSocket/WebSocketConnection.cpp @@ -47,6 +47,8 @@ #define X_AMZ_DATE "X-Amz-Date" #define X_AMZ_EXPIRES "X-Amz-Expires" #define X_AMZ_SECURITY_TOKEN "X-Amz-Security-Token" +#define X_AMZ_CUSTOMAUTHORIZER_NAME "X-Amz-CustomAuthorizer-Name" +#define X_AMZ_CUSTOMAUTHORIZER_SIGNATURE "X-Amz-CustomAuthorizer-Signature" #define SIGNING_KEY "AWS4" #define LONG_DATE_FORMAT_STR "%Y%m%dT%H%M%SZ" #define SIMPLE_DATE_FORMAT_STR "%Y%m%d" @@ -99,6 +101,11 @@ namespace awsiotsdk { bool server_verification_flag) : openssl_connection_(endpoint, endpoint_port, root_ca_location, tls_handshake_timeout, tls_read_timeout, tls_write_timeout, server_verification_flag) { + custom_authorizer_name_.clear(); + custom_authorizer_signature_.clear(); + custom_authorizer_token_name_.clear(); + custom_authorizer_token_.clear(); + endpoint_ = endpoint; endpoint_port_ = endpoint_port; root_ca_location_ = root_ca_location; @@ -125,6 +132,21 @@ namespace awsiotsdk { wss_frame_write_ = std::unique_ptr(new wslay_frame_iocb()); } + WebSocketConnection::WebSocketConnection(util::String endpoint, uint16_t endpoint_port, util::String root_ca_location, + std::chrono::milliseconds tls_handshake_timeout, + std::chrono::milliseconds tls_read_timeout, + std::chrono::milliseconds tls_write_timeout, + util::String custom_authorizer_name, util::String custom_authorizer_signature, + util::String custom_authorizer_token_name, util::String custom_authorizer_token, + bool server_verification_flag) + : WebSocketConnection(endpoint, endpoint_port, root_ca_location, "", "", "", "", tls_handshake_timeout, tls_read_timeout, + tls_write_timeout, false) { + custom_authorizer_name_ = custom_authorizer_name; + custom_authorizer_signature_ = custom_authorizer_signature; + custom_authorizer_token_name_ = custom_authorizer_token_name; + custom_authorizer_token_ = custom_authorizer_token; + } + ResponseCode WebSocketConnection::ConnectInternal() { // Init Tls ResponseCode rc = openssl_connection_.Initialize(); @@ -563,17 +585,12 @@ namespace awsiotsdk { } ResponseCode WebSocketConnection::WssHandshake() { + ResponseCode rc; + util::OStringStream stringStream; + // Assuming: // 1. Ssl socket is ready to do read/write. - // Create canonical query string - util::String canonical_query_string; - canonical_query_string.reserve(CANONICAL_QUERY_BUF_LEN); - ResponseCode rc = InitializeCanonicalQueryString(canonical_query_string); - if (ResponseCode::SUCCESS != rc) { - return rc; - } - // Create Wss handshake Http request // -> Generate Wss client key char client_key_buf[WSS_CLIENT_KEY_MAX_LEN + 1]; @@ -583,15 +600,32 @@ namespace awsiotsdk { return rc; } - // -> Assemble Wss Http request - util::OStringStream stringStream; - stringStream << "GET /mqtt?" << canonical_query_string << " " << HTTP_1_1 << "\r\n" - << "Host: " << endpoint_ << "\r\n" + if (custom_authorizer_name_.empty()) { + // Create canonical query string + util::String canonical_query_string; + canonical_query_string.reserve(CANONICAL_QUERY_BUF_LEN); + rc = InitializeCanonicalQueryString(canonical_query_string); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + + // -> Assemble Wss Http request + stringStream << "GET /mqtt?" << canonical_query_string << " " << HTTP_1_1 << "\r\n"; + } else { + // -> Assemble Wss Http request + stringStream << "GET /mqtt " << HTTP_1_1 << "\r\n" + << X_AMZ_CUSTOMAUTHORIZER_NAME << ": " << custom_authorizer_name_ << "\r\n" + << X_AMZ_CUSTOMAUTHORIZER_SIGNATURE << ": " << custom_authorizer_signature_ << "\r\n" + << custom_authorizer_token_name_ << ": " << custom_authorizer_token_ << "\r\n"; + } + + stringStream << "Host: " << endpoint_ << "\r\n" << "Connection: " << UPGRADE << "\r\n" << "Upgrade: " << WEBSOCKET << "\r\n" << "Sec-WebSocket-Version: " << SEC_WEBSOCKET_VERSION_13 << "\r\n" << "sec-websocket-key: " << client_key_buf << "\r\n" << "Sec-WebSocket-Protocol: " << MQTT_PROTOCOL << "\r\n\r\n"; + util::String request_string = stringStream.str(); // Send out request diff --git a/network/WebSocket/WebSocketConnection.hpp b/network/WebSocket/WebSocketConnection.hpp index 761c8cc..fa8a491 100644 --- a/network/WebSocket/WebSocketConnection.hpp +++ b/network/WebSocket/WebSocketConnection.hpp @@ -50,6 +50,10 @@ namespace awsiotsdk { util::String aws_access_key_id_; ///< Pointer to string containing the AWS Access Key Id. util::String aws_secret_access_key_; ///< Pointer to sstring containing the AWS Secret Access Key. util::String aws_session_token_; ///< Pointer to string containing the AWS Session Token. + util::String custom_authorizer_name_; ///< Pointer to string containing the custom authorizer name. + util::String custom_authorizer_signature_; ///< Pointer to string containing the authorizer signature. + util::String custom_authorizer_token_name_; ///< Pointer to string containing the authorizer token name. + util::String custom_authorizer_token_; ///< Pointer to string containing the authorizer token. util::String aws_region_; ///< Region for this connection util::String endpoint_; ///< Endpoint for this connection uint16_t endpoint_port_; ///< Endpoint port @@ -210,6 +214,31 @@ namespace awsiotsdk { std::chrono::milliseconds tls_read_timeout, std::chrono::milliseconds tls_write_timeout, bool server_verification_flag); + /** + * @brief Constructor for the WebSocket for MQTT implementation using custom authentication + * + * Performs any initialization required by the WebSocket layer. + * + * @param util::String endpoint - The target endpoint to connect to + * @param uint16_t endpoint_port - The port on the target to connect to + * @param util::String root_ca_location - Path of the location of the Root CA + * @param std::chrono::milliseconds tls_handshake_timeout - The value to use for timeout of handshake operation + * @param std::chrono::milliseconds tls_read_timeout - The value to use for timeout of read operation + * @param std::chrono::milliseconds tls_write_timeout - The value to use for timeout of write operation + * @param util::String custom_authorizer_name - Name of the authorizer function + * @param util::String custom_authorizer_signature - Authorizer signature + * @param util::String custom_authorizer_token_name - Authorizer token name + * @param util::String custom_authorizer_token - Authorizer token + * @param bool server_verification_flag - used to decide whether server verification is needed or not + * + */ + WebSocketConnection(util::String endpoint, uint16_t endpoint_port, util::String root_ca_location, + std::chrono::milliseconds tls_handshake_timeout, + std::chrono::milliseconds tls_read_timeout, std::chrono::milliseconds tls_write_timeout, + util::String custom_authorizer_name, util::String custom_authorizer_signature, + util::String custom_authorizer_token_name, util::String custom_authorizer_token, + bool server_verification_flag); + /** * @brief Check if WebSocket layer is still connected * diff --git a/samples/Jobs/CMakeLists.txt b/samples/Jobs/CMakeLists.txt new file mode 100644 index 0000000..b11e1c8 --- /dev/null +++ b/samples/Jobs/CMakeLists.txt @@ -0,0 +1,82 @@ +cmake_minimum_required(VERSION 3.2 FATAL_ERROR) +project(aws-iot-cpp-samples CXX) + +###################################### +# Section : Disable in-source builds # +###################################### + +if (${PROJECT_SOURCE_DIR} STREQUAL ${PROJECT_BINARY_DIR}) + message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there. You may need to remove CMakeCache.txt and CMakeFiles folder.") +endif () + +######################################## +# Section : Common Build setttings # +######################################## +# Set required compiler standard to standard c++11. Disable extensions. +set(CMAKE_CXX_STANDARD 11) # C++11... +set(CMAKE_CXX_STANDARD_REQUIRED ON) #...is required... +set(CMAKE_CXX_EXTENSIONS OFF) #...without compiler extensions like gnu++11 + +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/archive) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) + +# Configure Compiler flags +if (UNIX AND NOT APPLE) + # Prefer pthread if found + set(THREADS_PREFER_PTHREAD_FLAG ON) + set(CUSTOM_COMPILER_FLAGS "-fno-exceptions -Wall -Werror") +elseif (APPLE) + set(CUSTOM_COMPILER_FLAGS "-fno-exceptions -Wall -Werror") +elseif (WIN32) + set(CUSTOM_COMPILER_FLAGS "/W4") +endif () + +################################ +# Target : Build Jobs sample # +################################ +set(JOBS_SAMPLE_TARGET_NAME jobs-sample) +# Add Target +add_executable(${JOBS_SAMPLE_TARGET_NAME} "${PROJECT_SOURCE_DIR}/JobsSample.cpp;${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.cpp") + +# Add Target specific includes +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../common) +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}) + +# Configure Threading library +find_package(Threads REQUIRED) + +# Add SDK includes +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${CMAKE_BINARY_DIR}/${DEPENDENCY_DIR}/rapidjson/src/include) +target_include_directories(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../include) + +target_link_libraries(${JOBS_SAMPLE_TARGET_NAME} PUBLIC "Threads::Threads") +target_link_libraries(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${SDK_TARGET_NAME}) + +# Copy Json config file +add_custom_command(TARGET ${JOBS_SAMPLE_TARGET_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy ${PROJECT_SOURCE_DIR}/../../common/SampleConfig.json $/config/SampleConfig.json) +set_property(TARGET ${JOBS_SAMPLE_TARGET_NAME} APPEND_STRING PROPERTY COMPILE_FLAGS ${CUSTOM_COMPILER_FLAGS}) + +# Gather list of all .cert files in "/cert" +add_custom_command(TARGET ${JOBS_SAMPLE_TARGET_NAME} PRE_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy_directory ${PROJECT_SOURCE_DIR}/../../certs $/certs) + +if (MSVC) + target_sources(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.hpp) + source_group("Header Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.hpp) + source_group("Source Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/../../common/ConfigCommon.cpp) + + target_sources(${JOBS_SAMPLE_TARGET_NAME} PUBLIC ${PROJECT_SOURCE_DIR}/JobsSample.hpp) + source_group("Header Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/JobsSample.hpp) + source_group("Source Files\\Samples\\Jobs" FILES ${PROJECT_SOURCE_DIR}/JobsSample.cpp) +endif () + +######################### +# Add Network libraries # +######################### + +set(NETWORK_WRAPPER_DEST_TARGET ${JOBS_SAMPLE_TARGET_NAME}) +include(${PROJECT_SOURCE_DIR}/../../network/CMakeLists.txt.in) diff --git a/samples/Jobs/JobsSample.cpp b/samples/Jobs/JobsSample.cpp new file mode 100644 index 0000000..a1f032c --- /dev/null +++ b/samples/Jobs/JobsSample.cpp @@ -0,0 +1,383 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsSample.cpp + * + * This example takes the parameters from the config/SampleConfig.json file and establishes + * a connection to the AWS IoT MQTT Platform. It performs several operations to + * demonstrate the basic capabilities of the AWS IoT Jobs platform. + * + * If all the certs are correct, you should see the list of pending Job Executions + * printed out by the GetPendingCallback callback. If there are any existing pending + * job executions each will be processed one at a time in the NextJobCallback callback. + * After all of the pending jobs have been processed the program will wait for + * notifications for new pending jobs and process them one at a time as they come in. + * + * In the Subscribe function you can see how each callback is registered for each corresponding + * Jobs topic. + * + */ + +#include +#include + +#ifdef USE_WEBSOCKETS +#include "WebSocketConnection.hpp" +#elif defined USE_MBEDTLS +#include "MbedTLSConnection.hpp" +#else +#include "OpenSSLConnection.hpp" +#endif + +#include "util/logging/Logging.hpp" +#include "util/logging/LogMacros.hpp" +#include "util/logging/ConsoleLogSystem.hpp" + +#include "ConfigCommon.hpp" +#include "jobs/Jobs.hpp" +#include "JobsSample.hpp" + +#define LOG_TAG_JOBS "[Sample - Jobs]" + +namespace awsiotsdk { + namespace samples { + ResponseCode JobsSample::GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "GetPendingCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Json Parse for GetPendingCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("inProgressJobs")) { + std::cout << "inProgressJobs : " << util::JsonParser::ToString(doc["inProgressJobs"]) << std::endl; + } + + if (doc.HasMember("queuedJobs")) { + std::cout << "queuedJobs : " << util::JsonParser::ToString(doc["queuedJobs"]) << std::endl; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "NextJobCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Json Parse for NextJobCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("execution")) { + std::cout << "execution : " << util::JsonParser::ToString(doc["execution"]) << std::endl; + + if (doc["execution"].HasMember("jobId")) { + util::Map statusDetailsMap; + + util::String jobId = doc["execution"]["jobId"].GetString(); + std::cout << "jobId : " << jobId << std::endl; + + if (doc["execution"].HasMember("jobDocument")) { + std::cout << "jobDocument : " << util::JsonParser::ToString(doc["execution"]["jobDocument"]) << std::endl; + statusDetailsMap.insert(std::make_pair("exampleDetail", "a value appropriate for your successful job")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_SUCCEEDED, statusDetailsMap); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "SendJobsUpdate failed. %s", ResponseHelper::ToString(rc).c_str()); + return rc; + } + } else { + statusDetailsMap.insert(std::make_pair("failureDetail", "Unable to process job document")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_FAILED, statusDetailsMap); + } + } + } else { + std::cout << "No job execution description found, nothing to do." << std::endl; + done_ = true; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::UpdateAcceptedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + std::cout << std::endl << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::UpdateRejectedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + std::cout << std::endl << "************" << std::endl; + + /* Do error handling here for when the update was rejected */ + + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::DisconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data) { + std::cout << "*******************************************" << std::endl + << client_id << " Disconnected!" << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::ReconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode reconnect_result) { + std::cout << "*******************************************" << std::endl + << client_id << " Reconnect Attempted. Result " << ResponseHelper::ToString(reconnect_result) + << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsSample::ResubscribeCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode resubscribe_result) { + std::cout << "*******************************************" << std::endl + << client_id << " Resubscribe Attempted. Result" << ResponseHelper::ToString(resubscribe_result) + << std::endl + << "*******************************************" << std::endl; + return ResponseCode::SUCCESS; + } + + + ResponseCode JobsSample::Subscribe() { + std::cout << "******** Subscribe ***************" << std::endl; + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_pending_handler = + std::bind(&JobsSample::GetPendingCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_next_handler = + std::bind(&JobsSample::NextJobCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_update_accepted_handler = + std::bind(&JobsSample::UpdateAcceptedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_update_rejected_handler = + std::bind(&JobsSample::UpdateRejectedCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + util::Vector> topic_vector; + std::shared_ptr p_subscription; + + p_subscription = p_jobs_->CreateJobsSubscription(p_pending_handler, nullptr, Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_update_accepted_handler, nullptr, Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "+"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_update_rejected_handler, nullptr, Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, "+"); + topic_vector.push_back(p_subscription); + + ResponseCode rc = p_iot_client_->Subscribe(topic_vector, ConfigCommon::mqtt_command_timeout_); + return rc; + } + + ResponseCode JobsSample::InitializeTLS() { + ResponseCode rc = ResponseCode::SUCCESS; + +#ifdef USE_WEBSOCKETS + p_network_connection_ = std::shared_ptr( + new network::WebSocketConnection(ConfigCommon::endpoint_, ConfigCommon::endpoint_https_port_, + ConfigCommon::root_ca_path_, ConfigCommon::aws_region_, + ConfigCommon::aws_access_key_id_, + ConfigCommon::aws_secret_access_key_, + ConfigCommon::aws_session_token_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true)); + if (nullptr == p_network_connection_) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } +#elif defined USE_MBEDTLS + p_network_connection_ = std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, + true); + if (nullptr == p_network_connection_) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } +#else + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + rc = p_network_connection->Initialize(); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, + "Failed to initialize Network Connection. %s", + ResponseHelper::ToString(rc).c_str()); + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#endif + return rc; + } + + ResponseCode JobsSample::RunSample() { + done_ = false; + + ResponseCode rc = InitializeTLS(); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + + ClientCoreState::ApplicationDisconnectCallbackPtr p_disconnect_handler = + std::bind(&JobsSample::DisconnectCallback, this, std::placeholders::_1, std::placeholders::_2); + + ClientCoreState::ApplicationReconnectCallbackPtr p_reconnect_handler = + std::bind(&JobsSample::ReconnectCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + ClientCoreState::ApplicationResubscribeCallbackPtr p_resubscribe_handler = + std::bind(&JobsSample::ResubscribeCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + p_iot_client_ = std::shared_ptr(MqttClient::Create(p_network_connection_, + ConfigCommon::mqtt_command_timeout_, + p_disconnect_handler, nullptr, + p_reconnect_handler, nullptr, + p_resubscribe_handler, nullptr)); + if (nullptr == p_iot_client_) { + return ResponseCode::FAILURE; + } + + util::String client_id_tagged = ConfigCommon::base_client_id_; + client_id_tagged.append("_jobs_sample_"); + client_id_tagged.append(std::to_string(rand())); + std::unique_ptr client_id = Utf8String::Create(client_id_tagged); + + rc = p_iot_client_->Connect(ConfigCommon::mqtt_command_timeout_, ConfigCommon::is_clean_session_, + mqtt::Version::MQTT_3_1_1, ConfigCommon::keep_alive_timeout_secs_, + std::move(client_id), nullptr, nullptr, nullptr); + if (ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED != rc) { + return rc; + } + + p_jobs_ = Jobs::Create(p_iot_client_, mqtt::QoS::QOS1, ConfigCommon::thing_name_, client_id_tagged); + + rc = Subscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Subscribe failed. %s", ResponseHelper::ToString(rc).c_str()); + } else { + rc = p_jobs_->SendJobsQuery(Jobs::JOB_GET_PENDING_TOPIC); + + if (ResponseCode::SUCCESS == rc) { + rc = p_jobs_->SendJobsQuery(Jobs::JOB_DESCRIBE_TOPIC, "$next"); + } + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + } + } + + // Wait for job processing to complete + while (!done_) { + done_ = true; + std::this_thread::sleep_for(std::chrono::seconds(2)); + } + + rc = p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(LOG_TAG_JOBS, "Disconnect failed. %s", ResponseHelper::ToString(rc).c_str()); + } + + std::cout << "Exiting Sample!!!!" << std::endl; + return ResponseCode::SUCCESS; + } + } +} + +int main(int argc, char **argv) { + std::shared_ptr p_log_system = + std::make_shared(awsiotsdk::util::Logging::LogLevel::Info); + awsiotsdk::util::Logging::InitializeAWSLogging(p_log_system); + + std::unique_ptr + jobs_sample = std::unique_ptr(new awsiotsdk::samples::JobsSample()); + + awsiotsdk::ResponseCode rc = awsiotsdk::ConfigCommon::InitializeCommon("config/SampleConfig.json"); + if (awsiotsdk::ResponseCode::SUCCESS == rc) { + rc = jobs_sample->RunSample(); + } +#ifdef WIN32 + std::cout<<"Press any key to continue!!!!"<(rc); +} diff --git a/samples/Jobs/JobsSample.hpp b/samples/Jobs/JobsSample.hpp new file mode 100644 index 0000000..8080b61 --- /dev/null +++ b/samples/Jobs/JobsSample.hpp @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsSample.hpp + * @brief + * + */ + + +#pragma once + +#include "mqtt/Client.hpp" +#include "NetworkConnection.hpp" + +namespace awsiotsdk { + namespace samples { + class JobsSample { + protected: + std::shared_ptr p_network_connection_; + std::shared_ptr p_iot_client_; + std::shared_ptr p_jobs_; + std::atomic done_; + + ResponseCode DisconnectCallback(util::String topic_name, + std::shared_ptr p_app_handler_data); + ResponseCode ReconnectCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode reconnect_result); + ResponseCode ResubscribeCallback(util::String client_id, + std::shared_ptr p_app_handler_data, + ResponseCode resubscribe_result); + + ResponseCode GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode UpdateAcceptedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode UpdateRejectedCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + + ResponseCode Subscribe(); + ResponseCode InitializeTLS(); + + public: + ResponseCode RunSample(); + }; + } +} + + diff --git a/samples/README.md b/samples/README.md index 1b97d6b..00fdfb5 100644 --- a/samples/README.md +++ b/samples/README.md @@ -17,7 +17,15 @@ This sample demonstrates how various Shadow operations can be performed. * Code for this sample is located [here](./ShadowDelta) * Target for this sample is `shadow-delta-sample` - + +Note: The shadow client token is set as the thing name by default in the sample. The shadow client token is limited to 64 bytes and will return an error if a token longer than 64 bytes is used (`"code":400,"message":"invalid client token"`, although receiving a 400 does not necessarily mean that it is due to the length of the client token). Modify the code [here](../ShadowDelta/ShadowDelta.cpp#L184) if your thing name is longer than 64 bytes to prevent this error. + +### Jobs Sample +This sample demonstrates how various Jobs API operations can be performed including subscribing to Jobs notifications and publishing Job execution updates. + + * Code for this sample is located [here](./Jobs) + * Target for this sample is `jobs-sample` + ### Discovery Sample This sample demonstrates how the discovery operation can be performed to get the connectivity information to connect to a Greengrass Core (GGC). The configuration for this example is slightly different as the Discovery operation is a HTTP call, and uses port 8443, instead of port 8883 which is used for MQTT operations. The endpoint is the same IoT host endpoint used to connect the IoT thing to the cloud. diff --git a/src/ResponseCode.cpp b/src/ResponseCode.cpp index 2f3e21b..fad8420 100644 --- a/src/ResponseCode.cpp +++ b/src/ResponseCode.cpp @@ -343,6 +343,9 @@ namespace awsiotsdk { case ResponseCode::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR: os << awsiotsdk::ResponseHelper::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING; break; + case ResponseCode::JOBS_INVALID_TOPIC_ERROR: + os << awsiotsdk::ResponseHelper::JOBS_INVALID_TOPIC_ERROR_STRING; + break; } os << " : SDK Code " << static_cast(rc) << "."; return os; diff --git a/src/jobs/Jobs.cpp b/src/jobs/Jobs.cpp new file mode 100644 index 0000000..902ec91 --- /dev/null +++ b/src/jobs/Jobs.cpp @@ -0,0 +1,340 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.cpp + * @brief + * + */ + +#include "util/logging/LogMacros.hpp" + +#include "jobs/Jobs.hpp" + +#define BASE_THINGS_TOPIC "$aws/things/" + +#define NOTIFY_OPERATION "notify" +#define NOTIFY_NEXT_OPERATION "notify-next" +#define GET_OPERATION "get" +#define START_NEXT_OPERATION "start-next" +#define WILDCARD_OPERATION "+" +#define UPDATE_OPERATION "update" +#define ACCEPTED_REPLY "accepted" +#define REJECTED_REPLY "rejected" +#define WILDCARD_REPLY "#" + +namespace awsiotsdk { + std::unique_ptr Jobs::Create(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token) { + if (nullptr == p_mqtt_client) { + return nullptr; + } + + return std::unique_ptr(new Jobs(p_mqtt_client, qos, thing_name, client_token)); + } + + Jobs::Jobs(std::shared_ptr p_mqtt_client, + mqtt::QoS qos, + const util::String &thing_name, + const util::String &client_token) { + p_mqtt_client_ = p_mqtt_client; + qos_ = qos; + thing_name_ = thing_name; + client_token_ = client_token; + }; + + bool Jobs::BaseTopicRequiresJobId(JobExecutionTopicType topicType) { + switch (topicType) { + case JOB_UPDATE_TOPIC: + case JOB_DESCRIBE_TOPIC: + return true; + case JOB_NOTIFY_TOPIC: + case JOB_NOTIFY_NEXT_TOPIC: + case JOB_START_NEXT_TOPIC: + case JOB_GET_PENDING_TOPIC: + case JOB_WILDCARD_TOPIC: + case JOB_UNRECOGNIZED_TOPIC: + default: + return false; + } + }; + + const util::String Jobs::GetOperationForBaseTopic(JobExecutionTopicType topicType) { + switch (topicType) { + case JOB_UPDATE_TOPIC: + return UPDATE_OPERATION; + case JOB_NOTIFY_TOPIC: + return NOTIFY_OPERATION; + case JOB_NOTIFY_NEXT_TOPIC: + return NOTIFY_NEXT_OPERATION; + case JOB_GET_PENDING_TOPIC: + case JOB_DESCRIBE_TOPIC: + return GET_OPERATION; + case JOB_START_NEXT_TOPIC: + return START_NEXT_OPERATION; + case JOB_WILDCARD_TOPIC: + return WILDCARD_OPERATION; + case JOB_UNRECOGNIZED_TOPIC: + default: + return ""; + } + }; + + const util::String Jobs::GetSuffixForTopicType(JobExecutionTopicReplyType replyType) { + switch (replyType) { + case JOB_REQUEST_TYPE: + return ""; + case JOB_ACCEPTED_REPLY_TYPE: + return "/" ACCEPTED_REPLY; + case JOB_REJECTED_REPLY_TYPE: + return "/" REJECTED_REPLY; + case JOB_WILDCARD_REPLY_TYPE: + return "/" WILDCARD_REPLY; + case JOB_UNRECOGNIZED_TOPIC_TYPE: + default: + return ""; + } + } + + const util::String Jobs::GetExecutionStatus(JobExecutionStatus status) { + switch (status) { + case JOB_EXECUTION_QUEUED: + return "QUEUED"; + case JOB_EXECUTION_IN_PROGRESS: + return "IN_PROGRESS"; + case JOB_EXECUTION_FAILED: + return "FAILED"; + case JOB_EXECUTION_SUCCEEDED: + return "SUCCEEDED"; + case JOB_EXECUTION_CANCELED: + return "CANCELED"; + case JOB_EXECUTION_REJECTED: + return "REJECTED"; + case JOB_EXECUTION_STATUS_NOT_SET: + case JOB_EXECUTION_UNKNOWN_STATUS: + default: + return ""; + } + } + + util::String Jobs::Escape(const util::String &value) { + util::String result = ""; + + for (int i = 0; i < value.length(); i++) { + switch(value[i]) { + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + case '"': result += "\\\""; break; + case '\\': result += "\\\\"; break; + default: result += value[i]; + } + } + return result; + } + + util::String Jobs::SerializeStatusDetails(const util::Map &statusDetailsMap) { + util::String result = "{"; + + util::Map::const_iterator itr = statusDetailsMap.begin(); + while (itr != statusDetailsMap.end()) { + result += (itr == statusDetailsMap.begin() ? "\"" : ",\""); + result += Escape(itr->first) + "\":\"" + Escape(itr->second) + "\""; + itr++; + } + + result += '}'; + return result; + } + + std::unique_ptr Jobs::GetJobTopic(JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType, + const util::String &jobId) { + if (thing_name_.empty()) { + return nullptr; + } + + if ((topicType == JOB_NOTIFY_TOPIC || topicType == JOB_NOTIFY_NEXT_TOPIC) && replyType != JOB_REQUEST_TYPE) { + return nullptr; + } + + if ((topicType == JOB_GET_PENDING_TOPIC || topicType == JOB_START_NEXT_TOPIC || + topicType == JOB_NOTIFY_TOPIC || topicType == JOB_NOTIFY_NEXT_TOPIC) && !jobId.empty()) { + return nullptr; + } + + const bool requireJobId = BaseTopicRequiresJobId(topicType); + if (jobId.empty() && requireJobId) { + return nullptr; + } + + const util::String operation = GetOperationForBaseTopic(topicType); + if (operation.empty()) { + return nullptr; + } + + const util::String suffix = GetSuffixForTopicType(replyType); + + if (requireJobId) { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/" + jobId + '/' + operation + suffix); + } else if (topicType == JOB_WILDCARD_TOPIC) { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/#"); + } else { + return Utf8String::Create(BASE_THINGS_TOPIC + thing_name_ + "/jobs/" + operation + suffix); + } + }; + + util::String Jobs::SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap, + int64_t expectedVersion, // set to 0 to ignore + int64_t executionNumber, // set to 0 to ignore + bool includeJobExecutionState, + bool includeJobDocument) { + const util::String executionStatus = GetExecutionStatus(status); + + if (executionStatus.empty()) { + return ""; + } + + util::String result = "{\"status\":\"" + executionStatus + "\""; + if (!statusDetailsMap.empty()) { + result += ",\"statusDetails\":" + SerializeStatusDetails(statusDetailsMap); + } + if (expectedVersion > 0) { + result += ",\"expectedVersion\":\"" + std::to_string(expectedVersion) + "\""; + } + if (executionNumber > 0) { + result += ",\"executionNumber\":\"" + std::to_string(executionNumber) + "\""; + } + if (includeJobExecutionState) { + result += ",\"includeJobExecutionState\":\"true\""; + } + if (includeJobDocument) { + result += ",\"includeJobDocument\":\"true\""; + } + if (!client_token_.empty()) { + result += ",\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeDescribeJobExecutionPayload(int64_t executionNumber, // set to 0 to ignore + bool includeJobDocument) { + util::String result = "{\"includeJobDocument\":\""; + result += (includeJobDocument ? "true" : "false"); + result += "\""; + if (executionNumber > 0) { + result += ",\"executionNumber\":\"" + std::to_string(executionNumber) + "\""; + } + if (!client_token_.empty()) { + result += "\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap) { + util::String result = "{"; + if (!statusDetailsMap.empty()) { + result += "\"statusDetails\":" + SerializeStatusDetails(statusDetailsMap); + } + if (!client_token_.empty()) { + if (!statusDetailsMap.empty()) { + result += ','; + } + result += "\"clientToken\":\"" + client_token_ + "\""; + } + result += '}'; + + return result; + }; + + util::String Jobs::SerializeClientTokenPayload() { + if (!client_token_.empty()) { + return "{\"clientToken\":\"" + client_token_ + "\"}"; + } + + return "{}"; + }; + + ResponseCode Jobs::SendJobsQuery(JobExecutionTopicType topicType, + const util::String &jobId) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(topicType, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeClientTokenPayload(), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsStartNext(const util::Map &statusDetailsMap) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_START_NEXT_TOPIC, JOB_REQUEST_TYPE); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeStartNextPendingJobExecutionPayload(statusDetailsMap), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsDescribe(const util::String &jobId, + int64_t executionNumber, // set to 0 to ignore + bool includeJobDocument) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_DESCRIBE_TOPIC, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, SerializeDescribeJobExecutionPayload(executionNumber, includeJobDocument), nullptr, packet_id); + }; + + ResponseCode Jobs::SendJobsUpdate(const util::String &jobId, + JobExecutionStatus status, + const util::Map &statusDetailsMap, + int64_t expectedVersion, // set to 0 to ignore + int64_t executionNumber, // set to 0 to ignore + bool includeJobExecutionState, + bool includeJobDocument) { + uint16_t packet_id = 0; + std::unique_ptr jobTopic = GetJobTopic(JOB_UPDATE_TOPIC, JOB_REQUEST_TYPE, jobId); + + if (jobTopic == nullptr) { + return ResponseCode::JOBS_INVALID_TOPIC_ERROR; + } + + return p_mqtt_client_->PublishAsync(std::move(jobTopic), false, false, qos_, + SerializeJobExecutionUpdatePayload(status, statusDetailsMap, expectedVersion, executionNumber, + includeJobExecutionState, includeJobDocument), + nullptr, packet_id); + }; + + std::shared_ptr Jobs::CreateJobsSubscription(mqtt::Subscription::ApplicationCallbackHandlerPtr p_app_handler, + std::shared_ptr p_app_handler_data, + JobExecutionTopicType topicType, + JobExecutionTopicReplyType replyType, + const util::String &jobId) { + return mqtt::Subscription::Create(GetJobTopic(topicType, replyType, jobId), qos_, p_app_handler, p_app_handler_data); + }; +} diff --git a/src/mqtt/Common.cpp b/src/mqtt/Common.cpp index 9356517..4d4c947 100644 --- a/src/mqtt/Common.cpp +++ b/src/mqtt/Common.cpp @@ -26,7 +26,7 @@ #define MULTI_LEVEL_WILDCARD '#' #define RESERVED_TOPIC '$' #define SINGLE_LEVEL_REGEX_STRING "[^/]*" // Single level regex to allow all UTF-8 character except '\' -#define MULTI_LEVEL_REGEX_STRING "[^\uc1bf]*" // Placeholder for the multilevel regex to allow all UTF-8 character +#define MULTI_LEVEL_REGEX_STRING u8"[^\uc1bf]*" // Placeholder for the multilevel regex to allow all UTF-8 character namespace awsiotsdk { namespace mqtt { @@ -178,6 +178,9 @@ namespace awsiotsdk { p_topic_regex_.append(SINGLE_LEVEL_REGEX_STRING); } else if (it == MULTI_LEVEL_WILDCARD) { p_topic_regex_.append(MULTI_LEVEL_REGEX_STRING); + } else if (it == RESERVED_TOPIC) { + p_topic_regex_ += "\\"; + p_topic_regex_ += RESERVED_TOPIC; } else { p_topic_regex_ += it; } diff --git a/src/mqtt/Publish.cpp b/src/mqtt/Publish.cpp index 1835d6f..3baab37 100644 --- a/src/mqtt/Publish.cpp +++ b/src/mqtt/Publish.cpp @@ -143,7 +143,7 @@ namespace awsiotsdk { ******************************************/ PubackPacket::PubackPacket(uint16_t publish_packet_id) { packet_size_ = 2; // Packet ID requires 2 bytes in case of QoS1 and QoS2 - publish_packet_id_ = publish_packet_id; + publish_packet_id_.store(publish_packet_id, std::memory_order_relaxed); fixed_header_.Initialize(MessageTypes::PUBACK, false, QoS::QOS0, false, packet_size_); serialized_packet_length_ = packet_size_ + fixed_header_.Length(); } @@ -156,7 +156,7 @@ namespace awsiotsdk { util::String buf; buf.reserve(serialized_packet_length_); fixed_header_.AppendToBuffer(buf); - AppendUInt16ToBuffer(buf, publish_packet_id_); + AppendUInt16ToBuffer(buf, publish_packet_id_.load(std::memory_order_relaxed)); return buf; } diff --git a/src/shadow/Shadow.cpp b/src/shadow/Shadow.cpp index f16036c..9c0cd5d 100644 --- a/src/shadow/Shadow.cpp +++ b/src/shadow/Shadow.cpp @@ -172,16 +172,14 @@ namespace awsiotsdk { return ResponseCode::SHADOW_UNEXPECTED_RESPONSE_TYPE; } - // Validate payload - if (!payload.IsObject() - || !payload.HasMember(SHADOW_DOCUMENT_STATE_KEY)) { - return ResponseCode::SHADOW_UNEXPECTED_RESPONSE_PAYLOAD; - } - ResponseCode rc = ResponseCode::SHADOW_REQUEST_ACCEPTED; if (ShadowResponseType::Rejected == response_type) { AWS_LOG_WARN(SHADOW_LOG_TAG, "Get request rejected for shadow : %s", thing_name_.c_str()); rc = ResponseCode::SHADOW_REQUEST_REJECTED; + } else if (!payload.IsObject() + || !payload.HasMember(SHADOW_DOCUMENT_STATE_KEY)) { + // Invalid payload + rc = ResponseCode::SHADOW_UNEXPECTED_RESPONSE_PAYLOAD; } else { AWS_LOG_DEBUG(SHADOW_LOG_TAG, "Get request accepted for shadow : %s", thing_name_.c_str()); cur_server_state_document_.RemoveAllMembers(); diff --git a/tests/integration/include/JobsTest.hpp b/tests/integration/include/JobsTest.hpp new file mode 100644 index 0000000..e958fb7 --- /dev/null +++ b/tests/integration/include/JobsTest.hpp @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file Jobs.hpp + * @brief + * + */ + + +#pragma once + +#include "mqtt/Client.hpp" +#include "NetworkConnection.hpp" +#include "jobs/Jobs.hpp" + +namespace awsiotsdk { + namespace tests { + namespace integration { + class JobsTest { + protected: + static const std::chrono::seconds keep_alive_timeout_; + + std::shared_ptr p_network_connection_; + std::shared_ptr p_iot_client_; + std::shared_ptr p_jobs_; + std::atomic done_; + + ResponseCode GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + ResponseCode NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data); + + ResponseCode Subscribe(); + ResponseCode Unsubscribe(); + ResponseCode InitializeTLS(); + + public: + ResponseCode RunTest(); + }; + } + } +} + + diff --git a/tests/integration/src/IntegTestRunner.cpp b/tests/integration/src/IntegTestRunner.cpp index fc6c160..73eef3b 100644 --- a/tests/integration/src/IntegTestRunner.cpp +++ b/tests/integration/src/IntegTestRunner.cpp @@ -28,6 +28,7 @@ #include "ConfigCommon.hpp" #include "IntegTestRunner.hpp" #include "SdkTestConfig.hpp" +#include "JobsTest.hpp" #include "PubSub.hpp" #include "AutoReconnect.hpp" #include "MultipleClients.hpp" @@ -53,6 +54,17 @@ namespace awsiotsdk { ResponseCode IntegTestRunner::RunAllTests() { ResponseCode rc = ResponseCode::SUCCESS; // Each test runs in its own scope to ensure complete cleanup + /** + * Run Jobs Tests + */ + { + JobsTest jobs_test_runner; + rc = jobs_test_runner.RunTest(); + if (ResponseCode::SUCCESS != rc) { + return rc; + } + } + /** * Run Subscribe Publish Tests */ diff --git a/tests/integration/src/JobsTest.cpp b/tests/integration/src/JobsTest.cpp new file mode 100644 index 0000000..3e913df --- /dev/null +++ b/tests/integration/src/JobsTest.cpp @@ -0,0 +1,327 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsTest.cpp + * @brief + * + */ + +#include "JobsTest.hpp" +#include "util/logging/LogMacros.hpp" + +#include +#include + +#ifdef USE_WEBSOCKETS +#include "WebSocketConnection.hpp" +#elif defined USE_MBEDTLS +#include "MbedTLSConnection.hpp" +#else +#include "OpenSSLConnection.hpp" +#endif + +#include "ConfigCommon.hpp" + +#define JOBS_INTEGRATION_TEST_TAG "[Integration Test - Jobs]" + +namespace awsiotsdk { + namespace tests { + namespace integration { + ResponseCode JobsTest::GetPendingCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "GetPendingCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + done_ = false; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Json Parse for GetPendingCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("inProgressJobs")) { + std::cout << "inProgressJobs : " << util::JsonParser::ToString(doc["inProgressJobs"]) << std::endl; + } + + if (doc.HasMember("queuedJobs")) { + std::cout << "queuedJobs : " << util::JsonParser::ToString(doc["queuedJobs"]) << std::endl; + } + + std::cout << "************" << std::endl; + + rc = p_jobs_->SendJobsQuery(Jobs::JOB_DESCRIBE_TOPIC, "$next"); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + + return ResponseCode::FAILURE; + } + + return ResponseCode::SUCCESS; + } + + ResponseCode JobsTest::NextJobCallback(util::String topic_name, + util::String payload, + std::shared_ptr p_app_handler_data) { + std::cout << std::endl << "************" << std::endl; + std::cout << "NextJobCallback called" << std::endl; + std::cout << "Received message on topic : " << topic_name << std::endl; + std::cout << "Payload Length : " << payload.length() << std::endl; + std::cout << "Payload : " << payload << std::endl; + + ResponseCode rc; + util::JsonDocument doc; + + rc = util::JsonParser::InitializeFromJsonString(doc, payload); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Json Parse for NextJobCallback failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + if (doc.HasMember("execution")) { + std::cout << "execution : " << util::JsonParser::ToString(doc["execution"]) << std::endl; + + if (doc["execution"].HasMember("jobId")) { + util::Map statusDetailsMap; + + util::String jobId = doc["execution"]["jobId"].GetString(); + std::cout << "jobId : " << jobId << std::endl; + + if (doc["execution"].HasMember("jobDocument")) { + std::cout << "jobDocument : " << util::JsonParser::ToString(doc["execution"]["jobDocument"]) << std::endl; + statusDetailsMap.insert(std::make_pair("exampleDetail", "a value appropriate for your successful job")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_SUCCEEDED, statusDetailsMap); + } else { + statusDetailsMap.insert(std::make_pair("failureDetail", "Unable to process job document")); + rc = p_jobs_->SendJobsUpdate(jobId, Jobs::JOB_EXECUTION_FAILED, statusDetailsMap); + } + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsUpdate failed. %s", ResponseHelper::ToString(rc).c_str()); + return rc; + } + } + } else { + std::cout << "No job execution description found, nothing to do." << std::endl; + done_ = true; + } + + std::cout << "************" << std::endl; + return ResponseCode::SUCCESS; + } + + ResponseCode JobsTest::Subscribe() { + std::cout << "******** Subscribe ***************" << std::endl; + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_pending_handler = + std::bind(&JobsTest::GetPendingCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + mqtt::Subscription::ApplicationCallbackHandlerPtr p_next_handler = + std::bind(&JobsTest::NextJobCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + + util::Vector> topic_vector; + std::shared_ptr p_subscription; + + p_subscription = p_jobs_->CreateJobsSubscription(p_pending_handler, nullptr, Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(p_subscription); + + p_subscription = p_jobs_->CreateJobsSubscription(p_next_handler, nullptr, Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(p_subscription); + + ResponseCode rc = p_iot_client_->Subscribe(topic_vector, ConfigCommon::mqtt_command_timeout_); + std::this_thread::sleep_for(std::chrono::seconds(3)); + return rc; + } + + ResponseCode JobsTest::Unsubscribe() { + uint16_t packet_id = 0; + std::unique_ptr p_topic_name; + util::Vector> topic_vector; + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE); + topic_vector.push_back(std::move(p_topic_name)); + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, "$next"); + topic_vector.push_back(std::move(p_topic_name)); + + p_topic_name = p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC); + topic_vector.push_back(std::move(p_topic_name)); + + ResponseCode rc = p_iot_client_->UnsubscribeAsync(std::move(topic_vector), nullptr, packet_id); + std::this_thread::sleep_for(std::chrono::seconds(1)); + return rc; + } + + ResponseCode JobsTest::InitializeTLS() { + ResponseCode rc = ResponseCode::SUCCESS; + +#ifdef USE_WEBSOCKETS + p_network_connection_ = std::shared_ptr( + new network::WebSocketConnection(ConfigCommon::endpoint_, ConfigCommon::endpoint_https_port_, + ConfigCommon::root_ca_path_, ConfigCommon::aws_region_, + ConfigCommon::aws_access_key_id_, + ConfigCommon::aws_secret_access_key_, + ConfigCommon::aws_session_token_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true)); +#elif defined USE_MBEDTLS + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + + if (ResponseCode::SUCCESS != rc) { + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#else + std::shared_ptr p_network_connection = + std::make_shared(ConfigCommon::endpoint_, + ConfigCommon::endpoint_mqtt_port_, + ConfigCommon::root_ca_path_, + ConfigCommon::client_cert_path_, + ConfigCommon::client_key_path_, + ConfigCommon::tls_handshake_timeout_, + ConfigCommon::tls_read_timeout_, + ConfigCommon::tls_write_timeout_, true); + rc = p_network_connection->Initialize(); + + if (ResponseCode::SUCCESS != rc) { + rc = ResponseCode::FAILURE; + } else { + p_network_connection_ = std::dynamic_pointer_cast(p_network_connection); + } +#endif + return rc; + } + + ResponseCode JobsTest::RunTest() { + done_ = false; + ResponseCode rc = InitializeTLS(); + + do { + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Failed to initialize TLS layer. %s", + ResponseHelper::ToString(rc).c_str()); + break; + } + + p_iot_client_ = std::shared_ptr( + MqttClient::Create(p_network_connection_, ConfigCommon::mqtt_command_timeout_)); + if (nullptr == p_iot_client_) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Failed to create MQTT Client Instance!!"); + rc = ResponseCode::FAILURE; + break; + } + + util::String client_id_tagged = ConfigCommon::base_client_id_; + client_id_tagged.append("_jobs_tester_"); + client_id_tagged.append(std::to_string(rand())); + std::unique_ptr client_id = Utf8String::Create(client_id_tagged); + + rc = p_iot_client_->Connect(ConfigCommon::mqtt_command_timeout_, ConfigCommon::is_clean_session_, + mqtt::Version::MQTT_3_1_1, ConfigCommon::keep_alive_timeout_secs_, + std::move(client_id), nullptr, nullptr, nullptr); + + p_jobs_ = Jobs::Create(p_iot_client_, mqtt::QoS::QOS1, ConfigCommon::thing_name_, client_id_tagged); + + if (ResponseCode::MQTT_CONNACK_CONNECTION_ACCEPTED != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "MQTT Connect failed. %s", + ResponseHelper::ToString(rc).c_str()); + return rc; + } + + rc = Subscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Subscribe failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + break; + } + + rc = p_jobs_->SendJobsQuery(Jobs::JOB_GET_PENDING_TOPIC); + + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "SendJobsQuery failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + } + + int retries = 5; + while (!done_ && retries-- > 0) { + std::this_thread::sleep_for(std::chrono::seconds(5)); + } + + if (!done_) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Not all jobs processed."); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + rc = ResponseCode::FAILURE; + break; + } + + rc = Unsubscribe(); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Unsubscribe failed. %s", + ResponseHelper::ToString(rc).c_str()); + p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + break; + } + + rc = p_iot_client_->Disconnect(ConfigCommon::mqtt_command_timeout_); + if (ResponseCode::SUCCESS != rc) { + AWS_LOG_ERROR(JOBS_INTEGRATION_TEST_TAG, "Disconnect failed. %s", + ResponseHelper::ToString(rc).c_str()); + break; + } + } while (false); + + std::cout << std::endl; + if (ResponseCode::SUCCESS != rc) { + std::cout + << "Test Failed!!!! See above output for details!!" + << std::endl; + std::cout << "**********************************************************" << std::endl; + return ResponseCode::FAILURE; + } + + std::cout << "Test Successful!!!!" << std::endl; + std::cout << "**********************************************************" << std::endl; + return ResponseCode::SUCCESS; + } + } + } +} diff --git a/tests/unit/src/ResponseCodeTests.cpp b/tests/unit/src/ResponseCodeTests.cpp index 0d6f5cc..ada914e 100644 --- a/tests/unit/src/ResponseCodeTests.cpp +++ b/tests/unit/src/ResponseCodeTests.cpp @@ -570,6 +570,11 @@ namespace awsiotsdk { expected_string = ResponseCodeToString(ResponseHelper::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR_STRING, ResponseCode::DISCOVER_RESPONSE_UNEXPECTED_JSON_STRUCTURE_ERROR); EXPECT_EQ(expected_string, response_string); + + response_string = ResponseHelper::ToString(ResponseCode::JOBS_INVALID_TOPIC_ERROR); + expected_string = ResponseCodeToString(ResponseHelper::JOBS_INVALID_TOPIC_ERROR_STRING, + ResponseCode::JOBS_INVALID_TOPIC_ERROR); + EXPECT_EQ(expected_string, response_string); } } } diff --git a/tests/unit/src/jobs/JobsTests.cpp b/tests/unit/src/jobs/JobsTests.cpp new file mode 100644 index 0000000..56b5161 --- /dev/null +++ b/tests/unit/src/jobs/JobsTests.cpp @@ -0,0 +1,264 @@ +/* + * Copyright 2010-2018 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. + */ + +/** + * @file JobsTests.cpp + * @brief + * + */ + +#include + +#include + +#include "util/logging/LogMacros.hpp" + +#include "TestHelper.hpp" +#include "MockNetworkConnection.hpp" + +#include "jobs/Jobs.hpp" +#include "mqtt/ClientState.hpp" + +#define JOBS_TEST_LOG_TAG "[Jobs Unit Test]" + +namespace awsiotsdk { + namespace tests { + namespace unit { + class JobsTestWrapper : public Jobs { + protected: + static const util::String test_thing_name_; + static const util::String client_token_; + + public: + JobsTestWrapper(bool empty_thing_name, bool empty_client_token): + Jobs(nullptr, mqtt::QoS::QOS0, + empty_thing_name ? "" : test_thing_name_, + empty_client_token ? "" : client_token_) {} + + util::String SerializeStatusDetails(const util::Map &statusDetailsMap) { + return Jobs::SerializeStatusDetails(statusDetailsMap); + } + + util::String SerializeJobExecutionUpdatePayload(JobExecutionStatus status, + const util::Map &statusDetailsMap = util::Map(), + int64_t expectedVersion = 0, + int64_t executionNumber = 0, + bool includeJobExecutionState = false, + bool includeJobDocument = false) { + return Jobs::SerializeJobExecutionUpdatePayload(status, statusDetailsMap, expectedVersion, executionNumber, includeJobExecutionState, includeJobDocument); + } + + util::String SerializeDescribeJobExecutionPayload(int64_t executionNumber = 0, + bool includeJobDocument = true) { + return Jobs::SerializeDescribeJobExecutionPayload(executionNumber, includeJobDocument); + } + + util::String SerializeStartNextPendingJobExecutionPayload(const util::Map &statusDetailsMap = util::Map()) { + return Jobs::SerializeStartNextPendingJobExecutionPayload(statusDetailsMap); + } + + util::String SerializeClientTokenPayload() { + return Jobs::SerializeClientTokenPayload(); + } + + util::String Escape(const util::String &value) { + return Jobs::Escape(value); + } + }; + + const util::String JobsTestWrapper::test_thing_name_ = "CppSdkTestClient"; + const util::String JobsTestWrapper::client_token_ = "CppSdkTestClientToken"; + + class JobsTester : public ::testing::Test { + protected: + static const util::String job_id_; + + std::shared_ptr p_jobs_; + std::shared_ptr p_jobs_empty_client_token_; + std::shared_ptr p_jobs_empty_thing_name_; + + JobsTester() { + p_jobs_ = std::shared_ptr(new JobsTestWrapper(false, false)); + p_jobs_empty_client_token_ = std::shared_ptr(new JobsTestWrapper(false, true)); + p_jobs_empty_thing_name_ = std::shared_ptr(new JobsTestWrapper(true, false)); + } + }; + + const util::String JobsTester::job_id_ = "TestJobId"; + + TEST_F(JobsTester, ValidTopicsTests) { + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/accepted", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/rejected", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/get/#", p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/accepted", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/rejected", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/get/#", p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/accepted", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/rejected", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/start-next/#", p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/accepted", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/rejected", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/TestJobId/update/#", p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/notify", p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/notify-next", p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC)->ToStdString()); + + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)->ToStdString()); + EXPECT_EQ("$aws/things/CppSdkTestClient/jobs/#", p_jobs_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)->ToStdString()); + } + + TEST_F(JobsTester, InvalidTopicsTests) { + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_WILDCARD_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_empty_thing_name_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UNRECOGNIZED_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_GET_PENDING_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_DESCRIBE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_UPDATE_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REQUEST_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_NOTIFY_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_ACCEPTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_REJECTED_REPLY_TYPE, job_id_)); + EXPECT_EQ(nullptr, p_jobs_->GetJobTopic(Jobs::JOB_START_NEXT_TOPIC, Jobs::JOB_WILDCARD_REPLY_TYPE, job_id_)); + } + + + TEST_F(JobsTester, PayloadSerializationTests) { + util::Map statusDetailsMap; + statusDetailsMap.insert(std::make_pair("testKey", "testVal")); + + EXPECT_EQ("{}", p_jobs_empty_client_token_->SerializeClientTokenPayload()); + EXPECT_EQ("{\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeClientTokenPayload()); + + EXPECT_EQ("{}", p_jobs_empty_client_token_->SerializeStartNextPendingJobExecutionPayload()); + EXPECT_EQ("{\"statusDetails\":{\"testKey\":\"testVal\"}}", p_jobs_empty_client_token_->SerializeStartNextPendingJobExecutionPayload(statusDetailsMap)); + EXPECT_EQ("{\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeStartNextPendingJobExecutionPayload()); + EXPECT_EQ("{\"statusDetails\":{\"testKey\":\"testVal\"},\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeStartNextPendingJobExecutionPayload(statusDetailsMap)); + + EXPECT_EQ("{\"includeJobDocument\":\"true\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload()); + EXPECT_EQ("{\"includeJobDocument\":\"true\",\"executionNumber\":\"1\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload(1)); + EXPECT_EQ("{\"includeJobDocument\":\"false\",\"executionNumber\":\"1\"}", p_jobs_empty_client_token_->SerializeDescribeJobExecutionPayload(1, false)); + + EXPECT_EQ("{\"includeJobDocument\":\"true\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload()); + EXPECT_EQ("{\"includeJobDocument\":\"true\",\"executionNumber\":\"1\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload(1)); + EXPECT_EQ("{\"includeJobDocument\":\"false\",\"executionNumber\":\"1\"\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeDescribeJobExecutionPayload(1, false)); + + EXPECT_EQ("", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_STATUS_NOT_SET)); + EXPECT_EQ("", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_UNKNOWN_STATUS)); + EXPECT_EQ("", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_STATUS_NOT_SET)); + EXPECT_EQ("", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_UNKNOWN_STATUS)); + + EXPECT_EQ("{\"status\":\"QUEUED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"}}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"includeJobDocument\":\"true\"}", + p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true, true)); + + EXPECT_EQ("{\"status\":\"IN_PROGRESS\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_IN_PROGRESS)); + EXPECT_EQ("{\"status\":\"FAILED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_FAILED)); + EXPECT_EQ("{\"status\":\"SUCCEEDED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_SUCCEEDED)); + EXPECT_EQ("{\"status\":\"CANCELED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_CANCELED)); + EXPECT_EQ("{\"status\":\"REJECTED\"}", p_jobs_empty_client_token_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_REJECTED)); + + EXPECT_EQ("{\"status\":\"QUEUED\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true)); + EXPECT_EQ("{\"status\":\"QUEUED\",\"statusDetails\":{\"testKey\":\"testVal\"},\"expectedVersion\":\"1\",\"executionNumber\":\"1\",\"includeJobExecutionState\":\"true\",\"includeJobDocument\":\"true\",\"clientToken\":\"CppSdkTestClientToken\"}", + p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_QUEUED, statusDetailsMap, 1, 1, true, true)); + + EXPECT_EQ("{\"status\":\"IN_PROGRESS\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_IN_PROGRESS)); + EXPECT_EQ("{\"status\":\"FAILED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_FAILED)); + EXPECT_EQ("{\"status\":\"SUCCEEDED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_SUCCEEDED)); + EXPECT_EQ("{\"status\":\"CANCELED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_CANCELED)); + EXPECT_EQ("{\"status\":\"REJECTED\",\"clientToken\":\"CppSdkTestClientToken\"}", p_jobs_->SerializeJobExecutionUpdatePayload(Jobs::JOB_EXECUTION_REJECTED)); + + statusDetailsMap.insert(std::make_pair("testEscapeKey \" \t \r \n \\ '!", "testEscapeVal \" \t \r \n \\ '!")); + EXPECT_EQ("{\"testEscapeKey \\\" \\t \\r \\n \\\\ '!\":\"testEscapeVal \\\" \\t \\r \\n \\\\ '!\",\"testKey\":\"testVal\"}", p_jobs_->SerializeStatusDetails(statusDetailsMap)); + } + } + } +} diff --git a/tests/unit/src/mqtt/SubscribeTests.cpp b/tests/unit/src/mqtt/SubscribeTests.cpp index 4e33a49..fd5d618 100644 --- a/tests/unit/src/mqtt/SubscribeTests.cpp +++ b/tests/unit/src/mqtt/SubscribeTests.cpp @@ -30,7 +30,7 @@ #define K 1024 #define LARGE_PAYLOAD_SIZE 127 * K -#define VALID_WILDCARD_TOPICS 6 +#define VALID_WILDCARD_TOPICS 8 #define INVALID_WILDCARD_TOPICS 4 #define WILDCARD_TEST_TOPICS 10 #define UNMATCHED_WILDCARD_TEST_TOPICS 2 @@ -97,7 +97,9 @@ namespace awsiotsdk { "+/+", "/+", "sport/tennis/#", - "+/tennis/#" + "+/tennis/#", + "$/tennis/#", + "$sport/tennis/+" }; const util::String SubUnsubActionTester::valid_topic_regexes[VALID_WILDCARD_TOPICS] = { @@ -105,8 +107,10 @@ namespace awsiotsdk { "sport/[^/]*/player1", "[^/]*/[^/]*", "/[^/]*", - "sport/tennis/[^\uc1bf]*", - "[^/]*/tennis/[^\uc1bf]*" + u8"sport/tennis/[^\uc1bf]*", + u8"[^/]*/tennis/[^\uc1bf]*", + u8"\\$/tennis/[^\uc1bf]*", + "\\$sport/tennis/[^/]*" }; const util::String SubUnsubActionTester::invalid_wildcard_test_topics[INVALID_WILDCARD_TOPICS] = {