From b7f01b47aa54ae32d2a57b9441ecc82c1e59fe9c Mon Sep 17 00:00:00 2001 From: Alfred Gedeon Date: Thu, 30 Nov 2023 23:27:17 -0800 Subject: [PATCH] Shadow update tests --- .builder/actions/build_samples.py | 1 + .github/workflows/ci.yml | 26 ++++ samples/utils/CommandLineUtils.cpp | 5 + samples/utils/CommandLineUtils.h | 2 + .../test_cases/mqtt3_named_shadow_cfg.json | 40 ++++++ servicetests/test_cases/mqtt3_shadow_cfg.json | 36 +++++ .../test_cases/mqtt5_named_shadow_cfg.json | 40 ++++++ servicetests/test_cases/mqtt5_shadow_cfg.json | 36 +++++ servicetests/test_cases/test_shadow_update.py | 127 ++++++++++++++++++ 9 files changed, 313 insertions(+) create mode 100644 servicetests/test_cases/mqtt3_named_shadow_cfg.json create mode 100644 servicetests/test_cases/mqtt3_shadow_cfg.json create mode 100644 servicetests/test_cases/mqtt5_named_shadow_cfg.json create mode 100644 servicetests/test_cases/mqtt5_shadow_cfg.json create mode 100644 servicetests/test_cases/test_shadow_update.py diff --git a/.builder/actions/build_samples.py b/.builder/actions/build_samples.py index e8f481ab1..60c5de025 100644 --- a/.builder/actions/build_samples.py +++ b/.builder/actions/build_samples.py @@ -53,6 +53,7 @@ def run(self, env): servicetests = [ 'servicetests/tests/JobsExecution/', + 'servicetests/tests/ShadowUpdate/', ] for sample_path in samples: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0819fa65..c940dfc56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,7 @@ env: CI_MQTT5_ROLE: arn:aws:iam::180635532705:role/CI_MQTT5_Role CI_GREENGRASS_ROLE: arn:aws:iam::180635532705:role/CI_Greengrass_Role CI_JOBS_SERVICE_CLIENT_ROLE: arn:aws:iam::180635532705:role/CI_JobsServiceClient_Role + CI_SHADOW_SERVICE_CLIENT_ROLE: arn:aws:iam::180635532705:role/CI_ShadowServiceClient_Role jobs: linux-compat: @@ -476,6 +477,31 @@ jobs: run: | export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-cpp-v2/utils python3 ./test_cases/test_jobs_execution.py --config-file ${{ env.CI_SERVICE_TESTS_CFG_FOLDER }}/mqtt5_jobs_cfg.json + - name: configure AWS credentials (Shadow) + uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: ${{ env.CI_SHADOW_SERVICE_CLIENT_ROLE }} + aws-region: ${{ env.AWS_DEFAULT_REGION }} + - name: run Shadow service client test for MQTT311 + working-directory: ./aws-iot-device-sdk-cpp-v2/servicetests + run: | + export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-js-v2/utils + python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt3_shadow_cfg.json + - name: run Shadow service client test for MQTT5 + working-directory: ./aws-iot-device-sdk-cpp-v2/servicetests + run: | + export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-js-v2/utils + python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt5_shadow_cfg.json + - name: run Named Shadow service client test for MQTT311 + working-directory: ./aws-iot-device-sdk-cpp-v2/servicetests + run: | + export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-js-v2/utils + python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt3_named_shadow_cfg.json + - name: run Named Shadow service client test for MQTT5 + working-directory: ./aws-iot-device-sdk-cpp-v2/servicetests + run: | + export PYTHONPATH=${{ github.workspace }}/aws-iot-device-sdk-cpp-v2/utils + python3 ./test_cases/test_shadow_update.py --config-file test_cases/mqtt5_named_shadow_cfg.json - name: configure AWS credentials (Connect and PubSub) uses: aws-actions/configure-aws-credentials@v1 diff --git a/samples/utils/CommandLineUtils.cpp b/samples/utils/CommandLineUtils.cpp index 72a7c7387..6e917f18c 100644 --- a/samples/utils/CommandLineUtils.cpp +++ b/samples/utils/CommandLineUtils.cpp @@ -66,6 +66,8 @@ namespace Utils static const char *m_cmd_proxy_user_name = "proxy_user_name"; static const char *m_cmd_proxy_password = "proxy_password"; static const char *m_cmd_shadow_property = "shadow_property"; + static const char *m_cmd_shadow_name = "shadow_name"; + static const char *m_cmd_shadow_value = "shadow_value"; static const char *m_cmd_region = "region"; static const char *m_cmd_pkcs12_file = "pkcs12_file"; static const char *m_cmd_pkcs12_password = "pkcs12_password"; @@ -961,6 +963,9 @@ namespace Utils returnData.input_shadowProperty = cmdUtils.GetCommandOrDefault(m_cmd_shadow_property, "color"); returnData.input_clientId = cmdUtils.GetCommandOrDefault(m_cmd_client_id, Aws::Crt::String("test-") + Aws::Crt::UUID().ToString()); + + returnData.input_shadowName = cmdUtils.GetCommandOrDefault(m_cmd_shadow_name , ""); + returnData.input_shadowValue = cmdUtils.GetCommandOrDefault(m_cmd_shadow_value , ""); return returnData; } diff --git a/samples/utils/CommandLineUtils.h b/samples/utils/CommandLineUtils.h index 413550605..4615f06be 100644 --- a/samples/utils/CommandLineUtils.h +++ b/samples/utils/CommandLineUtils.h @@ -274,6 +274,8 @@ namespace Utils Aws::Crt::String input_proxyPassword; // Shadow Aws::Crt::String input_shadowProperty; + Aws::Crt::String input_shadowName; + Aws::Crt::String input_shadowValue; // PKCS12 Aws::Crt::String input_pkcs12File; Aws::Crt::String input_pkcs12Password; diff --git a/servicetests/test_cases/mqtt3_named_shadow_cfg.json b/servicetests/test_cases/mqtt3_named_shadow_cfg.json new file mode 100644 index 000000000..5b4760efd --- /dev/null +++ b/servicetests/test_cases/mqtt3_named_shadow_cfg.json @@ -0,0 +1,40 @@ +{ + "language": "Javascript", + "runnable_file": "../build/servicetests/tests/ShadowUpdate/shadow-update", + "runnable_region": "us-east-1", + "runnable_main_class": "", + "arguments": [ + { + "name": "--mqtt_version", + "data": "3" + }, + { + "name": "--endpoint", + "secret": "ci/endpoint" + }, + { + "name": "--cert", + "data": "tests/ShadowUpdate/certificate.pem.crt" + }, + { + "name": "--key", + "data": "tests/ShadowUpdate/private.pem.key" + }, + { + "name": "--thing_name", + "data": "ServiceTest_Shadow_$INPUT_UUID" + }, + { + "name": "--shadow_property", + "data": "color" + }, + { + "name": "--shadow_value", + "data": "on" + }, + { + "name": "--shadow_name", + "data": "testShadow" + } + ] +} diff --git a/servicetests/test_cases/mqtt3_shadow_cfg.json b/servicetests/test_cases/mqtt3_shadow_cfg.json new file mode 100644 index 000000000..98c2dfd0a --- /dev/null +++ b/servicetests/test_cases/mqtt3_shadow_cfg.json @@ -0,0 +1,36 @@ +{ + "language": "Javascript", + "runnable_file": "../build/servicetests/tests/ShadowUpdate/shadow-update", + "runnable_region": "us-east-1", + "runnable_main_class": "", + "arguments": [ + { + "name": "--mqtt_version", + "data": "3" + }, + { + "name": "--endpoint", + "secret": "ci/endpoint" + }, + { + "name": "--cert", + "data": "tests/ShadowUpdate/certificate.pem.crt" + }, + { + "name": "--key", + "data": "tests/ShadowUpdate/private.pem.key" + }, + { + "name": "--thing_name", + "data": "ServiceTest_Shadow_$INPUT_UUID" + }, + { + "name": "--shadow_property", + "data": "color" + }, + { + "name": "--shadow_value", + "data": "on" + } + ] +} diff --git a/servicetests/test_cases/mqtt5_named_shadow_cfg.json b/servicetests/test_cases/mqtt5_named_shadow_cfg.json new file mode 100644 index 000000000..f64b71641 --- /dev/null +++ b/servicetests/test_cases/mqtt5_named_shadow_cfg.json @@ -0,0 +1,40 @@ +{ + "language": "Javascript", + "runnable_file": "../build/servicetests/tests/ShadowUpdate/shadow-update", + "runnable_region": "us-east-1", + "runnable_main_class": "", + "arguments": [ + { + "name": "--mqtt_version", + "data": "5" + }, + { + "name": "--endpoint", + "secret": "ci/endpoint" + }, + { + "name": "--cert", + "data": "tests/ShadowUpdate/certificate.pem.crt" + }, + { + "name": "--key", + "data": "tests/ShadowUpdate/private.pem.key" + }, + { + "name": "--thing_name", + "data": "ServiceTest_Shadow_$INPUT_UUID" + }, + { + "name": "--shadow_property", + "data": "color" + }, + { + "name": "--shadow_value", + "data": "on" + }, + { + "name": "--shadow_name", + "data": "testShadow" + } + ] +} diff --git a/servicetests/test_cases/mqtt5_shadow_cfg.json b/servicetests/test_cases/mqtt5_shadow_cfg.json new file mode 100644 index 000000000..84abce198 --- /dev/null +++ b/servicetests/test_cases/mqtt5_shadow_cfg.json @@ -0,0 +1,36 @@ +{ + "language": "CPP", + "runnable_file": "../build/servicetests/tests/ShadowUpdate/shadow-update", + "runnable_region": "us-east-1", + "runnable_main_class": "", + "arguments": [ + { + "name": "--mqtt_version", + "data": "5" + }, + { + "name": "--endpoint", + "secret": "ci/endpoint" + }, + { + "name": "--cert", + "data": "tests/ShadowUpdate/certificate.pem.crt" + }, + { + "name": "--key", + "data": "tests/ShadowUpdate/private.pem.key" + }, + { + "name": "--thing_name", + "data": "ServiceTest_Shadow_$INPUT_UUID" + }, + { + "name": "--shadow_property", + "data": "color" + }, + { + "name": "--shadow_value", + "data": "on" + } + ] +} diff --git a/servicetests/test_cases/test_shadow_update.py b/servicetests/test_cases/test_shadow_update.py new file mode 100644 index 000000000..6c5a82915 --- /dev/null +++ b/servicetests/test_cases/test_shadow_update.py @@ -0,0 +1,127 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-3.0. + +import argparse +import json +import os +import sys +import uuid + +import boto3 + +import run_in_ci +import ci_iot_thing + + +def get_shadow_attrs(config_file): + with open(config_file) as f: + json_data = json.load(f) + shadow_name = next((json_arg["data"] for json_arg in json_data["arguments"] if json_arg.get("name", "") == "--shadow_name"), "") + shadow_property = next((json_arg["data"] for json_arg in json_data["arguments"] if json_arg.get("name", "") == "--shadow_property"), "") + shadow_desired_value = next((json_arg["data"] for json_arg in json_data["arguments"] if json_arg.get("name", "") == "--shadow_value"), "") + return [shadow_name, shadow_property, shadow_desired_value] + + +def main(): + argument_parser = argparse.ArgumentParser( + description="Run Shadow test in CI") + argument_parser.add_argument( + "--config-file", required=True, + help="JSON file providing command-line arguments for a test") + argument_parser.add_argument( + "--input-uuid", required=False, help="UUID for thing name. UUID will be generated if this option is omit") + argument_parser.add_argument( + "--region", required=False, default="us-east-1", help="The name of the region to use") + parsed_commands = argument_parser.parse_args() + + [shadow_name, shadow_property, shadow_desired_value] = get_shadow_attrs(parsed_commands.config_file) + print(f"Shadow name: '{shadow_name}'") + print(f"Shadow property: '{shadow_property}'") + print(f"Shadow desired value: '{shadow_desired_value}'") + + try: + iot_data_client = boto3.client('iot-data', region_name=parsed_commands.region) + secrets_client = boto3.client("secretsmanager", region_name=parsed_commands.region) + except Exception as e: + print(f"ERROR: Could not make Boto3 iot-data client. Credentials likely could not be sourced. Exception: {e}", + file=sys.stderr) + return -1 + + input_uuid = parsed_commands.input_uuid if parsed_commands.input_uuid else str(uuid.uuid4()) + + thing_name = "ServiceTest_Shadow_" + input_uuid + policy_name = secrets_client.get_secret_value( + SecretId="ci/ShadowServiceClientTest/policy_name")["SecretString"] + + # Temporary certificate/key file path. + certificate_path = os.path.join(os.getcwd(), "tests/ShadowUpdate/certificate.pem.crt") + key_path = os.path.join(os.getcwd(), "tests/ShadowUpdate/private.pem.key") + + try: + ci_iot_thing.create_iot_thing( + thing_name=thing_name, + region=parsed_commands.region, + policy_name=policy_name, + certificate_path=certificate_path, + key_path=key_path) + except Exception as e: + print(f"ERROR: Failed to create IoT thing: {e}") + sys.exit(-1) + + # Perform Shadow test. If it's successful, a shadow should appear for a specified thing. + try: + test_result = run_in_ci.setup_and_launch(parsed_commands.config_file, input_uuid) + except Exception as e: + print(f"ERROR: Failed to execute Jobs test: {e}") + test_result = -1 + + # Test reported success, verify that shadow was indeed updated. + if test_result == 0: + print("Verifying that shadow was updated") + shadow_value = None + try: + if shadow_name: + thing_shadow = iot_data_client.get_thing_shadow(thingName=thing_name, shadowName=shadow_name) + else: + thing_shadow = iot_data_client.get_thing_shadow(thingName=thing_name) + + payload = thing_shadow['payload'].read() + data = json.loads(payload) + shadow_value = data.get('state', {}).get('reported', {}).get(shadow_property, None) + if shadow_value != shadow_desired_value: + print(f"ERROR: Could not verify thing shadow: {shadow_property} is not set to desired value " + f"'{shadow_desired_value}'; shadow actual state: {data}") + test_result = -1 + except KeyError as e: + print(f"ERROR: Could not verify thing shadow: key {e} does not exist in shadow response: {thing_shadow}") + test_result = -1 + except Exception as e: + print(f"ERROR: Could not verify thing shadow: {e}") + test_result = -1 + + if test_result == 0: + print("Test succeeded") + + # Delete a thing created for this test run. + # NOTE We want to try to delete thing even if test was unsuccessful. + try: + ci_iot_thing.delete_iot_thing(thing_name, parsed_commands.region) + except Exception as e: + print(f"ERROR: Failed to delete thing: {e}") + # Fail the test if unable to delete thing, so this won't remain unnoticed. + test_result = -1 + + try: + if os.path.isfile(certificate_path): + os.remove(certificate_path) + if os.path.isfile(key_path): + os.remove(key_path) + except Exception as e: + print(f"WARNING: Failed to delete local files: {e}") + + if test_result != 0: + sys.exit(-1) + + +if __name__ == "__main__": + main()