From d1da5aa5850ff901d7299ebe7ed59f33a75601d6 Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Fri, 4 Dec 2020 16:37:59 -0800 Subject: [PATCH 1/2] Support explicit etag across IoT Hub resources. --- HISTORY.rst | 22 + azext_iot/_params.py | 15 +- .../common/digitaltwin_sas_token_auth.py | 65 --- azext_iot/common/utility.py | 16 - azext_iot/constants.py | 3 +- azext_iot/operations/hub.py | 167 ++---- .../configurations/test_iot_config_unit.py | 51 +- azext_iot/tests/test_iot_ext_unit.py | 515 ++++++++++-------- 8 files changed, 383 insertions(+), 471 deletions(-) delete mode 100644 azext_iot/common/digitaltwin_sas_token_auth.py diff --git a/HISTORY.rst b/HISTORY.rst index 0d24273c0..b15e2656a 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,28 @@ Release History =============== +0.10.8 ++++++++++++++++ + +**IoT Hub updates** + +The following commands support an explicit etag parameter. If no etag arg is passed the value "*" is used. + +* az iot hub device-identity update +* az iot hub device-identity delete +* az iot hub device-identity renew-key +* az iot hub device-twin update +* az iot hub device-twin delete +* az iot hub module-identity update +* az iot hub module-identity delete +* az iot hub module-twin update +* az iot hub module-twin delete +* az iot hub configuration update +* az iot hub configuration delete +* az iot edge deployment update +* az iot edge deployment update + + 0.10.7 +++++++++++++++ diff --git a/azext_iot/_params.py b/azext_iot/_params.py index 58839506e..8d0520fb4 100644 --- a/azext_iot/_params.py +++ b/azext_iot/_params.py @@ -114,7 +114,10 @@ def load_arguments(self, _): help="Valid token duration in seconds.", ) context.argument( - "etag", options_list=["--etag", "-e"], help="Entity tag value." + "etag", + options_list=["--etag", "-e"], + help="Etag or entity tag corresponding to the last state of the resource. " + "If no etag is provided the value '*' is used." ) context.argument( "top", @@ -180,16 +183,6 @@ def load_arguments(self, _): arg_type=get_three_state_flag(), help="Reinstall uamqp dependency compatible with extension version. Default: false", ) - context.argument( - "repo_endpoint", - options_list=["--endpoint", "-e"], - help="IoT Plug and Play endpoint.", - ) - context.argument( - "repo_id", - options_list=["--repo-id", "-r"], - help="IoT Plug and Play repository Id.", - ) context.argument( "consumer_group", options_list=["--consumer-group", "--cg", "-c"], diff --git a/azext_iot/common/digitaltwin_sas_token_auth.py b/azext_iot/common/digitaltwin_sas_token_auth.py deleted file mode 100644 index c2abbe108..000000000 --- a/azext_iot/common/digitaltwin_sas_token_auth.py +++ /dev/null @@ -1,65 +0,0 @@ -# coding=utf-8 -# -------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See License.txt in the project root for license information. -# -------------------------------------------------------------------------------------------- -""" -digitaltwin_sas_token_auth: Module containing DigitalTwin Model Shared Access Signature token class. - -""" - -from base64 import b64encode, b64decode -from hashlib import sha256 -from hmac import HMAC -from time import time -try: - from urllib import (urlencode, quote_plus) -except ImportError: - from urllib.parse import (urlencode, quote_plus) -from msrest.authentication import Authentication - - -class DigitalTwinSasTokenAuthentication(Authentication): - """ - Shared Access Signature authorization for DigitalTwin Repository. - - Args: - uri (str): Uri of target resource. - shared_access_policy_name (str): Name of shared access policy. - shared_access_key (str): Shared access key. - expiry (int): Expiry of the token to be generated. Input should - be seconds since the epoch, in UTC. Default is an hour later from now. - """ - def __init__(self, repositoryId, endpoint, shared_access_key_name, shared_access_key, expiry=None): - self.repositoryId = repositoryId - self.policy = shared_access_key_name - self.key = shared_access_key - self.endpoint = endpoint - if expiry is None: - self.expiry = time() + 3600 # Default expiry is an hour later - else: - self.expiry = expiry - - def generate_sas_token(self): - """ - Create a shared access signiture token as a string literal. - - Returns: - result (str): SAS token as string literal. - """ - encoded_uri = quote_plus(self.endpoint) - encoded_repo_id = quote_plus(self.repositoryId) - ttl = int(self.expiry) - sign_key = '%s\n%s\n%d' % (encoded_repo_id, encoded_uri, ttl) - signature = b64encode(HMAC(b64decode(self.key), sign_key.encode('utf-8'), sha256).digest()) - result = { - 'sr': self.endpoint, - 'sig': signature, - 'se': str(ttl) - } - - if self.policy: - result['skn'] = self.policy - result['rid'] = self.repositoryId - - return 'SharedAccessSignature ' + urlencode(result) diff --git a/azext_iot/common/utility.py b/azext_iot/common/utility.py index 1e21672fe..ff2f2a128 100644 --- a/azext_iot/common/utility.py +++ b/azext_iot/common/utility.py @@ -380,22 +380,6 @@ def init_monitoring(cmd, timeout, properties, enqueued_time, repair, yes): return (enqueued_time, properties, timeout, output) -def get_sas_token(target): - from azext_iot.common.digitaltwin_sas_token_auth import ( - DigitalTwinSasTokenAuthentication, - ) - - token = "" - if target.get("repository_id"): - token = DigitalTwinSasTokenAuthentication( - target["repository_id"], - target["entity"], - target["policy"], - target["primarykey"], - ).generate_sas_token() - return {"Authorization": "{}".format(token)} - - def dict_clean(d): """ Remove None from dictionary """ if not isinstance(d, dict): diff --git a/azext_iot/constants.py b/azext_iot/constants.py index 239b7b3e0..5d80ead0e 100644 --- a/azext_iot/constants.py +++ b/azext_iot/constants.py @@ -7,14 +7,13 @@ import os -VERSION = "0.10.7" +VERSION = "0.10.8" EXTENSION_NAME = "azure-iot" EXTENSION_ROOT = os.path.dirname(os.path.abspath(__file__)) EXTENSION_CONFIG_ROOT_KEY = "iotext" EDGE_DEPLOYMENT_SCHEMA_2_PATH = os.path.join( EXTENSION_ROOT, "assets", "edge-deploy-2.0.schema.json" ) -BASE_API_VERSION = "2018-08-30-preview" BASE_MQTT_API_VERSION = "2018-06-30" MESSAGING_HTTP_C2D_SYSTEM_PROPERTIES = [ "iothub-messageid", diff --git a/azext_iot/operations/hub.py b/azext_iot/operations/hub.py index 8e70e5bc9..bceb2f9f3 100644 --- a/azext_iot/operations/hub.py +++ b/azext_iot/operations/hub.py @@ -327,7 +327,7 @@ def update_iot_device_custom( def iot_device_update( - cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None + cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None, etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -346,7 +346,7 @@ def iot_device_update( parameters.get('statusReason'), parameters.get('deviceScope') ) - updated_device.etag = parameters.get("etag", "*") + updated_device.etag = etag if etag else "*" return _iot_device_update(target, device_id, updated_device) @@ -367,7 +367,7 @@ def _iot_device_update(target, device_id, device): def iot_device_delete( - cmd, device_id, hub_name=None, resource_group_name=None, login=None + cmd, device_id, hub_name=None, resource_group_name=None, login=None, etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -377,47 +377,35 @@ def iot_device_delete( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - device = _iot_device_show(target=target, device_id=device_id) - etag = device.get("etag") - - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - service_sdk.devices.delete_identity( - id=device_id, custom_headers=headers - ) - return - raise LookupError("device etag not found") + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + service_sdk.devices.delete_identity( + id=device_id, custom_headers=headers + ) + return except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) -def _update_device_key(target, device, auth_method, pk, sk): +def _update_device_key(target, device, auth_method, pk, sk, etag=None): resolver = SdkResolver(target=target) service_sdk = resolver.get_sdk(SdkType.service_sdk) try: auth = _assemble_auth(auth_method, pk, sk) device["authentication"] = auth - etag = device.get("etag", None) - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.devices.create_or_update_identity( - id=device["deviceId"], - device=device, - custom_headers=headers, - ) - raise LookupError("device etag not found.") + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + return service_sdk.devices.create_or_update_identity( + id=device["deviceId"], + device=device, + custom_headers=headers, + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) -def iot_device_key_regenerate(cmd, hub_name, device_id, renew_key_type, resource_group_name=None, login=None): +def iot_device_key_regenerate(cmd, hub_name, device_id, renew_key_type, resource_group_name=None, login=None, etag=None): discovery = IotHubDiscovery(cmd) target = discovery.get_target( hub_name=hub_name, resource_group_name=resource_group_name, login=login @@ -438,7 +426,7 @@ def iot_device_key_regenerate(cmd, hub_name, device_id, renew_key_type, resource pk = sk sk = temp - return _update_device_key(target, device, device["authentication"]["type"], pk, sk) + return _update_device_key(target, device, device["authentication"]["type"], pk, sk, etag) def iot_device_get_parent( @@ -731,6 +719,7 @@ def iot_device_module_update( hub_name=None, resource_group_name=None, login=None, + etag=None, ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -741,21 +730,16 @@ def iot_device_module_update( try: updated_module = _handle_module_update_params(parameters) - etag = parameters.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.modules.create_or_update_identity( - id=device_id, - mid=module_id, - module=updated_module, - custom_headers=headers, - ) - raise LookupError("module etag not found.") + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + return service_sdk.modules.create_or_update_identity( + id=device_id, + mid=module_id, + module=updated_module, + custom_headers=headers, + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def _handle_module_update_params(parameters): @@ -829,7 +813,7 @@ def _iot_device_module_show(target, device_id, module_id): def iot_device_module_delete( - cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None + cmd, device_id, module_id, hub_name=None, resource_group_name=None, login=None, etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -839,22 +823,14 @@ def iot_device_module_delete( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - module = _iot_device_module_show( - target=target, device_id=device_id, module_id=module_id + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + service_sdk.modules.delete_identity( + id=device_id, mid=module_id, custom_headers=headers ) - etag = module.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - service_sdk.modules.delete_identity( - id=device_id, mid=module_id, custom_headers=headers - ) - return - raise LookupError("module etag not found") + return except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def iot_device_module_twin_show( @@ -889,6 +865,7 @@ def iot_device_module_twin_update( hub_name=None, resource_group_name=None, login=None, + etag=None ): from azext_iot.common.utility import verify_transform @@ -901,7 +878,7 @@ def iot_device_module_twin_update( try: headers = {} - headers["If-Match"] = '"*"' + headers["If-Match"] = '"{}"'.format(etag if etag else "*") verify = {} if parameters.get("properties"): if parameters["properties"].get("desired"): @@ -929,6 +906,7 @@ def iot_device_module_twin_replace( hub_name=None, resource_group_name=None, login=None, + etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -939,24 +917,16 @@ def iot_device_module_twin_replace( try: target_json = process_json_arg(target_json, argument_name="json") - module = _iot_device_module_twin_show( - target=target, device_id=device_id, module_id=module_id + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + return service_sdk.modules.replace_twin( + id=device_id, + mid=module_id, + device_twin_info=target_json, + custom_headers=headers, ) - etag = module.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.modules.replace_twin( - id=device_id, - mid=module_id, - device_twin_info=target_json, - custom_headers=headers, - ) - raise LookupError("module twin etag not found") except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def iot_edge_set_modules( @@ -1204,7 +1174,7 @@ def _validate_payload_schema(content): def iot_hub_configuration_update( - cmd, config_id, parameters, hub_name=None, resource_group_name=None, login=None + cmd, config_id, parameters, hub_name=None, resource_group_name=None, login=None, etag=None ): from azext_iot.sdk.iothub.service.models import Configuration from azext_iot.common.utility import verify_transform @@ -1217,11 +1187,8 @@ def iot_hub_configuration_update( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - etag = parameters.get("etag") - if not etag: - raise LookupError("invalid request, configuration etag not found") headers = {} - headers["If-Match"] = '"{}"'.format(etag) + headers["If-Match"] = '"{}"'.format(etag if etag else "*") verify = {"metrics": dict, "metrics.queries": dict, "content": dict} if parameters.get("labels"): verify["labels"] = dict @@ -1233,15 +1200,14 @@ def iot_hub_configuration_update( content=parameters["content"], metrics=parameters.get("metrics", None), target_condition=parameters["targetCondition"], - priority=parameters["priority"], - content_type="assignment", + priority=parameters["priority"] ) return service_sdk.configuration.create_or_update( id=config_id, configuration=config, custom_headers=headers ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except (AttributeError, LookupError, TypeError) as err: + except (AttributeError, TypeError) as err: raise CLIError(err) @@ -1319,7 +1285,7 @@ def _iot_hub_configuration_list( def iot_hub_configuration_delete( - cmd, config_id, hub_name=None, resource_group_name=None, login=None + cmd, config_id, hub_name=None, resource_group_name=None, login=None, etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -1329,18 +1295,11 @@ def iot_hub_configuration_delete( service_sdk = resolver.get_sdk(SdkType.service_sdk) try: - config = _iot_hub_configuration_show(target=target, config_id=config_id) - etag = config.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - service_sdk.configuration.delete(id=config_id, custom_headers=headers) - return - raise LookupError("configuration etag not found") + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + service_sdk.configuration.delete(id=config_id, custom_headers=headers) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def iot_edge_deployment_metric_show( @@ -1450,7 +1409,7 @@ def iot_twin_update_custom(instance, desired=None, tags=None): def iot_device_twin_update( - cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None + cmd, device_id, parameters, hub_name=None, resource_group_name=None, login=None, etag=None ): from azext_iot.common.utility import verify_transform @@ -1463,7 +1422,7 @@ def iot_device_twin_update( try: headers = {} - headers["If-Match"] = '"*"' + headers["If-Match"] = '"{}"'.format(etag if etag else "*") verify = {} if parameters.get("properties"): if parameters["properties"].get("desired"): @@ -1481,7 +1440,7 @@ def iot_device_twin_update( def iot_device_twin_replace( - cmd, device_id, target_json, hub_name=None, resource_group_name=None, login=None + cmd, device_id, target_json, hub_name=None, resource_group_name=None, login=None, etag=None ): discovery = IotHubDiscovery(cmd) target = discovery.get_target( @@ -1492,19 +1451,13 @@ def iot_device_twin_replace( try: target_json = process_json_arg(target_json, argument_name="json") - device = _iot_device_twin_show(target=target, device_id=device_id) - etag = device.get("etag") - if etag: - headers = {} - headers["If-Match"] = '"{}"'.format(etag) - return service_sdk.devices.replace_twin( - id=device_id, device_twin_info=target_json, custom_headers=headers - ) - raise LookupError("device twin etag not found") + headers = {} + headers["If-Match"] = '"{}"'.format(etag if etag else "*") + return service_sdk.devices.replace_twin( + id=device_id, device_twin_info=target_json, custom_headers=headers + ) except CloudError as e: raise CLIError(unpack_msrest_error(e)) - except LookupError as err: - raise CLIError(err) def iot_device_method( diff --git a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py index 5b185427f..1edcac871 100644 --- a/azext_iot/tests/iothub/configurations/test_iot_config_unit.py +++ b/azext_iot/tests/iothub/configurations/test_iot_config_unit.py @@ -5,6 +5,7 @@ # -------------------------------------------------------------------------------------------- +from azext_iot.tests.generators import generate_generic_id import pytest import responses import json @@ -563,21 +564,21 @@ def test_config_create_error( class TestConfigDelete: - @pytest.fixture(params=[(200, 204)]) + @pytest.fixture(params=[204]) def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - etag = str(uuid4()) - service_client.expected_etag = etag side_effect = [ - build_mock_response(mocker, request.param[0], {"etag": etag}), - build_mock_response(mocker, request.param[1]), + build_mock_response(mocker, request.param), ] service_client.side_effect = side_effect return service_client - def test_config_delete(self, serviceclient, fixture_cmd): + @pytest.mark.parametrize( + "etag", [generate_generic_id(), None], + ) + def test_config_delete(self, serviceclient, fixture_cmd, etag): subject.iot_hub_configuration_delete( - fixture_cmd, config_id=config_id, hub_name=mock_target["entity"] + fixture_cmd, config_id=config_id, hub_name=mock_target["entity"], etag=etag ) args = serviceclient.call_args url = args[0][0].url @@ -586,19 +587,7 @@ def test_config_delete(self, serviceclient, fixture_cmd): assert method == "DELETE" assert "{}/configurations/{}?".format(mock_target["entity"], config_id) in url - assert headers["If-Match"] == '"{}"'.format(serviceclient.expected_etag) - - @pytest.mark.parametrize("expected_error", [CLIError]) - def test_config_delete_invalid_args( - self, - fixture_cmd, - serviceclient_generic_invalid_or_missing_etag, - expected_error, - ): - with pytest.raises(expected_error): - subject.iot_hub_configuration_delete( - fixture_cmd, config_id=config_id, hub_name=mock_target["entity"] - ) + assert headers["If-Match"] == '"{}"'.format(etag if etag else "*") def test_config_delete_error(self, fixture_cmd, serviceclient_generic_error): with pytest.raises(CLIError): @@ -614,12 +603,16 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client.return_value = build_mock_response(mocker, request.param, {}) return service_client - def test_config_update(self, fixture_cmd, serviceclient, sample_config_show): + @pytest.mark.parametrize( + "etag", [generate_generic_id(), None], + ) + def test_config_update(self, fixture_cmd, serviceclient, sample_config_show, etag): subject.iot_hub_configuration_update( cmd=fixture_cmd, config_id=config_id, hub_name=mock_target["entity"], parameters=sample_config_show, + etag=etag ) args = serviceclient.call_args url = args[0][0].url @@ -630,30 +623,20 @@ def test_config_update(self, fixture_cmd, serviceclient, sample_config_show): assert "{}/configurations/{}?".format(mock_target["entity"], config_id) in url assert method == "PUT" - assert headers["If-Match"] == '"{}"'.format(sample_config_show["etag"]) - assert body["id"] == sample_config_show["id"] assert body.get("metrics") == sample_config_show.get("metrics") assert body.get("targetCondition") == sample_config_show.get("targetCondition") assert body.get("priority") == sample_config_show.get("priority") assert body.get("labels") == sample_config_show.get("labels") + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(etag if etag else "*") + def test_config_update_invalid_args( self, fixture_cmd, serviceclient, sample_config_show ): from copy import deepcopy - request = deepcopy(sample_config_show) - request["etag"] = None - - with pytest.raises(CLIError): - subject.iot_hub_configuration_update( - cmd=fixture_cmd, - config_id=config_id, - hub_name=mock_target["entity"], - parameters=request, - ) - request = deepcopy(sample_config_show) request["labels"] = "not a dictionary" diff --git a/azext_iot/tests/test_iot_ext_unit.py b/azext_iot/tests/test_iot_ext_unit.py index 02555b21d..4e79a2234 100644 --- a/azext_iot/tests/test_iot_ext_unit.py +++ b/azext_iot/tests/test_iot_ext_unit.py @@ -4,19 +4,18 @@ # Licensed under the MIT License. See License.txt in the project root for license information. # -------------------------------------------------------------------------------------------- -''' +""" NOTICE: These tests are to be phased out and introduced in more modern form. Try not to add any new content, only fixes if necessary. Look at IoT Hub jobs or configuration tests for a better example. Also use responses fixtures like mocked_response for http request mocking. -''' +""" import pytest import json import os import responses import re -from uuid import uuid4 from azext_iot.operations import hub as subject from azext_iot.common.utility import ( validate_min_python_version, @@ -43,11 +42,7 @@ module_id = "mymod" config_id = "myconfig" message_etag = "3k28zb44-0d00-4ddd-ade3-6110eb94c476" -c2d_purge_response = { - "deviceId": device_id, - "moduleId": None, - "totalMessagesPurged": 3 -} +c2d_purge_response = {"deviceId": device_id, "moduleId": None, "totalMessagesPurged": 3} generic_cs_template = "HostName={};SharedAccessKeyName={};SharedAccessKey={}" @@ -277,10 +272,9 @@ def test_device_create_addchildren(self, sc_device_create_addchildren, req): assert "{}/devices/{}?".format(mock_target["entity"], child_device_id) in url assert args[0][0].method == "PUT" assert body["deviceId"] == child_device_id - assert ( - body["deviceScope"] == generate_parent_device().get("deviceScope") or - body["parentScopes"] == [generate_parent_device().get("deviceScope")] - ) + assert body["deviceScope"] == generate_parent_device().get( + "deviceScope" + ) or body["parentScopes"] == [generate_parent_device().get("deviceScope")] @pytest.fixture(params=[(200, 0)]) def sc_invalid_args_device_create_addchildren( @@ -322,7 +316,10 @@ def test_device_create_addchildren_invalid_args( @pytest.mark.parametrize( "req, exp", [ - (generate_device_create_req(ee=True, auth="x509_thumbprint", ptp=None), ValueError), + ( + generate_device_create_req(ee=True, auth="x509_thumbprint", ptp=None), + ValueError, + ), (generate_device_create_req(auth="doesnotexist"), ValueError), ( generate_device_create_req(auth="x509_thumbprint", ptp=None, stp=""), @@ -363,9 +360,8 @@ def generate_device_show(**kvp): "authentication": { "symmetricKey": {"primaryKey": None, "secondaryKey": None}, "x509Thumbprint": {"primaryThumbprint": None, "secondaryThumbprint": None}, - "type": "sas" + "type": "sas", }, - "etag": "abcd", "capabilities": {"iotEdge": True}, "deviceId": device_id, "status": "disabled", @@ -385,7 +381,7 @@ def device_update_con_arg( primary_thumbprint=None, secondary_thumbprint=None, primary_key=None, - secondary_key=None + secondary_key=None, ): return { "edge_enabled": edge_enabled, @@ -395,7 +391,7 @@ def device_update_con_arg( "primary_thumbprint": primary_thumbprint, "secondary_thumbprint": secondary_thumbprint, "primary_key": primary_key, - "secondary_key": secondary_key + "secondary_key": secondary_key, } @@ -431,12 +427,21 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): } ) ), - (generate_device_show(authentication={"type": "certificateAuthority"})), + ( + generate_device_show( + authentication={"type": "certificateAuthority"}, + etag=generate_generic_id(), + ) + ), ], ) def test_device_update(self, fixture_cmd, serviceclient, req): subject.iot_device_update( - fixture_cmd, req["deviceId"], hub_name=mock_target["entity"], parameters=req + fixture_cmd, + req["deviceId"], + hub_name=mock_target["entity"], + parameters=req, + etag=req.get("etag"), ) args = serviceclient.call_args assert ( @@ -457,76 +462,65 @@ def test_device_update(self, fixture_cmd, serviceclient, req): assert body["authentication"]["x509Thumbprint"]["secondaryThumbprint"] headers = args[0][0].headers - assert headers["If-Match"] == '"{}"'.format(req["etag"]) + target_etag = req.get("etag") + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") @pytest.mark.parametrize( "req, arg", [ ( - generate_device_show( - capabilities={"iotEdge": False} - ), - device_update_con_arg( - edge_enabled=True - ) + generate_device_show(capabilities={"iotEdge": False}), + device_update_con_arg(edge_enabled=True), ), ( - generate_device_show( - status="disabled" - ), - device_update_con_arg( - status="enabled" - ) - ), - ( - generate_device_show(), - device_update_con_arg( - status_reason="test" - ) + generate_device_show(status="disabled"), + device_update_con_arg(status="enabled"), ), + (generate_device_show(), device_update_con_arg(status_reason="test")), ( generate_device_show(), device_update_con_arg( auth_method="shared_private_key", primary_key="primarykeyUpdated", - secondary_key="secondarykeyUpdated" - ) + secondary_key="secondarykeyUpdated", + ), ), ( generate_device_show( authentication={ "type": "selfSigned", "symmetricKey": {"primaryKey": None, "secondaryKey": None}, - "x509Thumbprint": {"primaryThumbprint": "123", "secondaryThumbprint": "321"}, + "x509Thumbprint": { + "primaryThumbprint": "123", + "secondaryThumbprint": "321", + }, } ), device_update_con_arg( auth_method="shared_private_key", primary_key="primary_key", - secondary_key="secondary_key" - ) + secondary_key="secondary_key", + ), ), ( generate_device_show( authentication={ "type": "certificateAuthority", "symmetricKey": {"primaryKey": None, "secondaryKey": None}, - "x509Thumbprint": {"primaryThumbprint": None, "secondaryThumbprint": None}, + "x509Thumbprint": { + "primaryThumbprint": None, + "secondaryThumbprint": None, + }, } ), device_update_con_arg( auth_method="x509_thumbprint", primary_thumbprint="primary_thumbprint", - secondary_thumbprint="secondary_thumbprint" - ) - ), - ( - generate_device_show(), - device_update_con_arg( - auth_method="x509_ca", - ) + secondary_thumbprint="secondary_thumbprint", + ), ), - ] + (generate_device_show(), device_update_con_arg(auth_method="x509_ca",)), + ], ) def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): instance = subject.update_iot_device_custom( @@ -534,11 +528,11 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): arg["edge_enabled"], arg["status"], arg["status_reason"], - arg['auth_method'], - arg['primary_thumbprint'], - arg['secondary_thumbprint'], - arg['primary_key'], - arg["secondary_key"] + arg["auth_method"], + arg["primary_thumbprint"], + arg["secondary_thumbprint"], + arg["primary_key"], + arg["secondary_key"], ) if arg["edge_enabled"]: @@ -546,20 +540,28 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): if arg["status"]: assert instance["status"] == arg["status"] if arg["status_reason"]: - assert instance['statusReason'] == arg["status_reason"] + assert instance["statusReason"] == arg["status_reason"] if arg["auth_method"]: if arg["auth_method"] == "shared_private_key": - assert instance['authentication']['type'] == "sas" - instance['authentication']['symmetricKey']['primaryKey'] == arg['primary_key'] - instance['authentication']['symmetricKey']['secondaryKey'] == arg['secondary_key'] + assert instance["authentication"]["type"] == "sas" + instance["authentication"]["symmetricKey"]["primaryKey"] == arg[ + "primary_key" + ] + instance["authentication"]["symmetricKey"]["secondaryKey"] == arg[ + "secondary_key" + ] if arg["auth_method"] == "x509_thumbprint": - assert instance['authentication']['type'] == "selfSigned" - if arg['primary_thumbprint']: - instance['authentication']['x509Thumbprint']['primaryThumbprint'] = arg['primary_thumbprint'] - if arg['secondary_thumbprint']: - instance['authentication']['x509Thumbprint']['secondaryThumbprint'] = arg['secondary_thumbprint'] - if arg['auth_method'] == "x509_ca": - assert instance['authentication']['type'] == "certificateAuthority" + assert instance["authentication"]["type"] == "selfSigned" + if arg["primary_thumbprint"]: + instance["authentication"]["x509Thumbprint"][ + "primaryThumbprint" + ] = arg["primary_thumbprint"] + if arg["secondary_thumbprint"]: + instance["authentication"]["x509Thumbprint"][ + "secondaryThumbprint" + ] = arg["secondary_thumbprint"] + if arg["auth_method"] == "x509_ca": + assert instance["authentication"]["type"] == "certificateAuthority" @pytest.mark.parametrize( "req, arg, exp", @@ -567,26 +569,21 @@ def test_iot_device_custom(self, fixture_cmd, serviceclient, req, arg): ( generate_device_show(), device_update_con_arg( - auth_method="shared_private_key", - primary_key="primarykeyUpdated", + auth_method="shared_private_key", primary_key="primarykeyUpdated", ), - CLIError + CLIError, ), ( generate_device_show(), - device_update_con_arg( - auth_method="x509_thumbprint", - ), - CLIError + device_update_con_arg(auth_method="x509_thumbprint",), + CLIError, ), ( generate_device_show(), - device_update_con_arg( - auth_method="Unknown", - ), - ValueError + device_update_con_arg(auth_method="Unknown",), + ValueError, ), - ] + ], ) def test_iot_device_custom_invalid_args(self, serviceclient, req, arg, exp): with pytest.raises(exp): @@ -595,11 +592,11 @@ def test_iot_device_custom_invalid_args(self, serviceclient, req, arg, exp): arg["edge_enabled"], arg["status"], arg["status_reason"], - arg['auth_method'], - arg['primary_thumbprint'], - arg['secondary_thumbprint'], - arg['primary_key'], - arg["secondary_key"] + arg["auth_method"], + arg["primary_thumbprint"], + arg["secondary_thumbprint"], + arg["primary_key"], + arg["secondary_key"], ) @pytest.mark.parametrize( @@ -617,7 +614,7 @@ def test_iot_device_custom_invalid_args(self, serviceclient, req, arg, exp): ), CLIError, ), - (generate_device_show(authentication={"type": "doesnotexist"}), CLIError) + (generate_device_show(authentication={"type": "doesnotexist"}), CLIError), ], ) def test_device_update_invalid_args(self, serviceclient, req, exp): @@ -648,24 +645,24 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): kvp.setdefault( "authentication", { - "symmetricKey": { - "primaryKey": "123", - "secondaryKey": "321" - }, - "type": "sas" - } + "symmetricKey": {"primaryKey": "123", "secondaryKey": "321"}, + "type": "sas", + }, ) test_side_effect = [ build_mock_response(mocker, 200, generate_device_show(**kvp)), - build_mock_response(mocker, 200, {}) + build_mock_response(mocker, 200, {}), ] service_client.side_effect = test_side_effect return service_client - @pytest.mark.parametrize("req", ["primary", "secondary", "swap"]) - def test_device_key_regenerate(self, fixture_cmd, serviceclient, req): + @pytest.mark.parametrize( + "req, etag", + [("primary", generate_generic_id()), ("secondary", None), ("swap", None)], + ) + def test_device_key_regenerate(self, fixture_cmd, serviceclient, req, etag): subject.iot_device_key_regenerate( - fixture_cmd, mock_target["entity"], device_id, req + fixture_cmd, mock_target["entity"], device_id, req, etag=etag ) args = serviceclient.call_args assert ( @@ -674,14 +671,17 @@ def test_device_key_regenerate(self, fixture_cmd, serviceclient, req): assert args[0][0].method == "PUT" body = json.loads(args[0][0].body) - if(req == "primary"): + if req == "primary": assert body["authentication"]["symmetricKey"]["primaryKey"] != "123" - if(req == "secondary"): + if req == "secondary": assert body["authentication"]["symmetricKey"]["secondaryKey"] != "321" - if(req == "swap"): + if req == "swap": assert body["authentication"]["symmetricKey"]["primaryKey"] == "321" assert body["authentication"]["symmetricKey"]["secondaryKey"] == "123" + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(etag if etag else "*") + @pytest.fixture(params=[200]) def serviceclient_invalid_args(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) @@ -694,14 +694,11 @@ def serviceclient_invalid_args(self, mocker, fixture_ghcs, fixture_sas, request) return service_client @pytest.mark.parametrize( - "req, exp", - [ - ("primary", CLIError), - ("secondary", CLIError), - ("swap", CLIError) - ] + "req, exp", [("primary", CLIError), ("secondary", CLIError), ("swap", CLIError)] ) - def test_device_key_regenerate_invalid_args(self, fixture_cmd, serviceclient_invalid_args, req, exp): + def test_device_key_regenerate_invalid_args( + self, fixture_cmd, serviceclient_invalid_args, req, exp + ): with pytest.raises(exp): subject.iot_device_key_regenerate( fixture_cmd, mock_target["entity"], device_id, req @@ -716,33 +713,29 @@ def test_device_key_regenerate_error(self, serviceclient_generic_error, req): class TestDeviceDelete: - @pytest.fixture(params=[(200, 204)]) + @pytest.fixture(params=[204]) def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - etag = str(uuid4()) - service_client.expected_etag = etag test_side_effect = [ - build_mock_response(mocker, request.param[0], {"etag": etag}), - build_mock_response(mocker, request.param[1]), + build_mock_response(mocker, request.param), ] service_client.side_effect = test_side_effect return service_client - def test_device_delete(self, serviceclient): - subject.iot_device_delete(fixture_cmd, device_id, mock_target["entity"]) + @pytest.mark.parametrize("target_etag", [generate_generic_id(), None]) + def test_device_delete(self, serviceclient, target_etag): + subject.iot_device_delete( + cmd=fixture_cmd, + device_id=device_id, + hub_name=mock_target["entity"], + etag=target_etag, + ) args = serviceclient.call_args url = args[0][0].url assert "{}/devices/{}?".format(mock_target["entity"], device_id) in url assert args[0][0].method == "DELETE" headers = args[0][0].headers - assert headers["If-Match"] == '"{}"'.format(serviceclient.expected_etag) - - @pytest.mark.parametrize("exception", [CLIError]) - def test_device_delete_invalid_args( - self, serviceclient_generic_invalid_or_missing_etag, exception - ): - with pytest.raises(exception): - subject.iot_device_delete(fixture_cmd, device_id, mock_target["entity"]) + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") def test_device_delete_error(self, serviceclient_generic_error): with pytest.raises(CLIError): @@ -755,11 +748,13 @@ def test_device_show(self, fixture_cmd, mocked_response, fixture_ghcs): device_id = generate_generic_id() mocked_response.add( method=responses.GET, - url=re.compile("https://{}/devices/{}".format(mock_target["entity"], device_id)), + url=re.compile( + "https://{}/devices/{}".format(mock_target["entity"], device_id) + ), body=json.dumps(generate_device_show(deviceId=device_id)), status=200, - content_type='application/json', - match_querystring=False + content_type="application/json", + match_querystring=False, ) result = subject.iot_device_show(fixture_cmd, device_id, mock_target["entity"]) @@ -785,7 +780,7 @@ def service_client(self, mocked_response, fixture_ghcs, request): headers={"x-ms-continuation": ""}, status=200, content_type="application/json", - match_querystring=False + match_querystring=False, ) mocked_response.expected_size = size @@ -924,7 +919,8 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): authentication={ "symmetricKey": {"primaryKey": "", "secondaryKey": ""}, "type": "sas", - } + }, + etag=generate_generic_id(), ) ), ( @@ -975,8 +971,10 @@ def test_device_module_update(self, serviceclient, req): elif req["authentication"]["type"] == "selfSigned": assert body["authentication"]["x509Thumbprint"]["primaryThumbprint"] assert body["authentication"]["x509Thumbprint"]["secondaryThumbprint"] + + target_etag = req.get("etag") headers = args[0][0].headers - assert headers["If-Match"] == '"{}"'.format(req["etag"]) + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") @pytest.mark.parametrize( "req, exp", @@ -997,7 +995,6 @@ def test_device_module_update(self, serviceclient, req): generate_device_module_show(authentication={"type": "doesnotexist"}), CLIError, ), - (generate_device_module_show(etag=None), CLIError), ], ) def test_device_module_update_invalid_args(self, serviceclient, req, exp): @@ -1023,21 +1020,23 @@ def test_device_module_update_error(self, serviceclient_generic_error, req): class TestDeviceModuleDelete: - @pytest.fixture(params=[(200, 204)]) + @pytest.fixture(params=[204]) def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): service_client = mocker.patch(path_service_client) - etag = str(uuid4()) - service_client.expected_etag = etag test_side_effect = [ - build_mock_response(mocker, request.param[0], {"etag": etag}), - build_mock_response(mocker, request.param[1]), + build_mock_response(mocker, request.param), ] service_client.side_effect = test_side_effect return service_client - def test_device_module_delete(self, serviceclient): + @pytest.mark.parametrize("target_etag", [generate_generic_id(), None]) + def test_device_module_delete(self, serviceclient, target_etag): subject.iot_device_module_delete( - fixture_cmd, device_id, module_id=module_id, hub_name=mock_target["entity"] + fixture_cmd, + device_id, + module_id=module_id, + hub_name=mock_target["entity"], + etag=target_etag, ) args = serviceclient.call_args url = args[0][0].url @@ -1046,19 +1045,7 @@ def test_device_module_delete(self, serviceclient): assert "devices/{}/modules/{}?".format(device_id, module_id) in url assert method == "DELETE" - assert headers["If-Match"] == '"{}"'.format(serviceclient.expected_etag) - - @pytest.mark.parametrize("exception", [CLIError]) - def test_device_module_invalid_args( - self, serviceclient_generic_invalid_or_missing_etag, exception - ): - with pytest.raises(exception): - subject.iot_device_module_delete( - fixture_cmd, - device_id, - module_id=module_id, - hub_name=mock_target["entity"], - ) + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") def test_device_module_delete_error(self, serviceclient_generic_error): with pytest.raises(CLIError): @@ -1144,7 +1131,7 @@ def generate_device_twin_show(file_handle=False, **kvp): path = os.path.realpath("test_generic_twin.json") return path - payload = {"deviceId": device_id, "etag": "abcd"} + payload = {"deviceId": device_id} for k in kvp: payload[k] = kvp[k] return payload @@ -1187,20 +1174,32 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): # Update does a GET/SHOW first @pytest.mark.parametrize( - "req", [(generate_device_twin_show(properties={"desired": {"key": "value"}}))] + "req", + [ + generate_device_twin_show( + properties={"desired": {"key": "value"}}, etag="abcd" + ), + generate_device_twin_show(properties={"desired": {"key": "value"}}), + ], ) def test_device_twin_update(self, serviceclient, req): subject.iot_device_twin_update( - fixture_cmd, req["deviceId"], hub_name=mock_target["entity"], parameters=req + fixture_cmd, + req["deviceId"], + hub_name=mock_target["entity"], + parameters=req, + etag=req.get("etag"), ) args = serviceclient.call_args body = json.loads(args[0][0].body) assert body == req assert "twins/{}".format(device_id) in args[0][0].url - @pytest.mark.parametrize( - "req", [generate_device_twin_show()] - ) + target_etag = req.get("etag") + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") + + @pytest.mark.parametrize("req", [generate_device_twin_show()]) def test_device_twin_update_error(self, serviceclient_generic_error, req): with pytest.raises(CLIError): subject.iot_device_twin_update( @@ -1222,23 +1221,32 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): # Replace does a GET/SHOW first @pytest.mark.parametrize( - "req, isfile", + "req, isfile, etag", [ - (generate_device_twin_show(moduleId=module_id), False), + ( + generate_device_twin_show(moduleId=module_id), + False, + generate_generic_id(), + ), ( generate_device_twin_show( moduleId=module_id, properties={"desired": {"key": "value"}} ), False, + None, ), - (generate_device_twin_show(file_handle=True), True), + (generate_device_twin_show(file_handle=True), True, None), ], ) - def test_device_twin_replace(self, serviceclient, req, isfile): + def test_device_twin_replace(self, serviceclient, req, isfile, etag): if not isfile: req = json.dumps(req) subject.iot_device_twin_replace( - fixture_cmd, device_id, hub_name=mock_target["entity"], target_json=req + cmd=fixture_cmd, + device_id=device_id, + hub_name=mock_target["entity"], + target_json=req, + etag=etag, ) args = serviceclient.call_args body = json.loads(args[0][0].body) @@ -1250,22 +1258,8 @@ def test_device_twin_replace(self, serviceclient, req, isfile): assert "{}/twins/{}?".format(mock_target["entity"], device_id) in args[0][0].url assert args[0][0].method == "PUT" - @pytest.mark.parametrize( - "req, exp", - [ - (generate_device_twin_show(etag=None), CLIError), - ({"invalid": "args"}, CLIError) - ], - ) - def test_device_twin_replace_invalid_args(self, serviceclient, req, exp): - with pytest.raises(exp): - serviceclient.return_value = build_mock_response(status_code=200, payload=req) - subject.iot_device_twin_replace( - fixture_cmd, - device_id, - hub_name=mock_target["entity"], - target_json=json.dumps(req), - ) + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(etag if etag else "*") @pytest.mark.parametrize("req", [(generate_device_twin_show(moduleId=module_id))]) def test_device_twin_replace_error(self, serviceclient_generic_error, req): @@ -1319,11 +1313,18 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): @pytest.mark.parametrize( "req", [ + ( + generate_device_twin_show( + moduleId=module_id, + properties={"desired": {"key": "value"}}, + etag=generate_generic_id(), + ) + ), ( generate_device_twin_show( moduleId=module_id, properties={"desired": {"key": "value"}} ) - ) + ), ], ) def test_device_module_twin_update(self, serviceclient, req): @@ -1333,6 +1334,7 @@ def test_device_module_twin_update(self, serviceclient, req): hub_name=mock_target["entity"], module_id=module_id, parameters=req, + etag=req.get("etag"), ) args = serviceclient.call_args body = json.loads(args[0][0].body) @@ -1341,6 +1343,10 @@ def test_device_module_twin_update(self, serviceclient, req): "twins/{}/modules/{}?".format(req["deviceId"], module_id) in args[0][0].url ) + target_etag = req.get("etag") + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(target_etag if target_etag else "*") + @pytest.mark.parametrize("req", [(generate_device_twin_show(moduleId=module_id))]) def test_device_module_twin_update_error(self, serviceclient_generic_error, req): with pytest.raises(CLIError): @@ -1364,19 +1370,24 @@ def serviceclient(self, mocker, fixture_ghcs, fixture_sas, request): # Replace does a GET/SHOW first @pytest.mark.parametrize( - "req, isfile", + "req, isfile, etag", [ - (generate_device_twin_show(moduleId=module_id), False), + ( + generate_device_twin_show(moduleId=module_id), + False, + generate_generic_id(), + ), ( generate_device_twin_show( moduleId=module_id, properties={"desired": {"key": "value"}} ), False, + None, ), - (generate_device_twin_show(file_handle=True), True), + (generate_device_twin_show(file_handle=True), True, None), ], ) - def test_device_module_twin_replace(self, serviceclient, req, isfile): + def test_device_module_twin_replace(self, serviceclient, req, isfile, etag): if not isfile: req = json.dumps(req) subject.iot_device_module_twin_replace( @@ -1385,6 +1396,7 @@ def test_device_module_twin_replace(self, serviceclient, req, isfile): hub_name=mock_target["entity"], module_id=module_id, target_json=req, + etag=etag, ) args = serviceclient.call_args body = json.loads(args[0][0].body) @@ -1396,26 +1408,8 @@ def test_device_module_twin_replace(self, serviceclient, req, isfile): assert "twins/{}/modules/{}?".format(device_id, module_id) in args[0][0].url assert args[0][0].method == "PUT" - @pytest.mark.parametrize( - "req, exp", - [ - (generate_device_twin_show(moduleId=module_id, etag=None), CLIError), - ({"invalid": "payload"}, CLIError), - ], - ) - def test_device_module_twin_replace_invalid_args(self, mocker, serviceclient, req, exp): - with pytest.raises(exp): - serviceclient.return_value = build_mock_response( - mocker, 200, payload=req - ) - - subject.iot_device_module_twin_replace( - fixture_cmd, - device_id, - hub_name=mock_target["entity"], - module_id=module_id, - target_json=json.dumps(req), - ) + headers = args[0][0].headers + assert headers["If-Match"] == '"{}"'.format(etag if etag else "*") @pytest.mark.parametrize("req", [(generate_device_twin_show(moduleId=module_id))]) def test_device_module_twin_replace_error(self, serviceclient_generic_error, req): @@ -1469,9 +1463,7 @@ def test_query_basic(self, serviceclient, query, servresult, servtotal, top): continuation[-1] = None serviceclient.return_value = build_mock_response( - status_code=200, - payload=servresult, - headers_get_side_effect=continuation + status_code=200, payload=servresult, headers_get_side_effect=continuation ) result = subject.iot_query( @@ -1707,7 +1699,7 @@ def c2d_receive_scenario(self, fixture_ghcs, mocked_response, request): body=payload["body"], headers=payload["headers"], status=200, - match_querystring=False + match_querystring=False, ) yield (mocked_response, payload) @@ -1715,6 +1707,7 @@ def c2d_receive_scenario(self, fixture_ghcs, mocked_response, request): @pytest.fixture() def c2d_receive_ack_scenario(self, fixture_ghcs, mocked_response): from .generators import create_c2d_receive_response + payload = create_c2d_receive_response() mocked_response.add( method=responses.GET, @@ -1798,13 +1791,14 @@ def c2d_ack_abandon_scenario(self, fixture_ghcs, mocked_response): @pytest.fixture() def c2d_purge_scenario(self, fixture_ghcs, mocked_response): import json + mocked_response.add( method=responses.DELETE, url="https://{}/devices/{}/commands".format( mock_target["entity"], device_id ), body=json.dumps(c2d_purge_response), - content_type='application/json', + content_type="application/json", status=200, ) yield mocked_response @@ -1829,15 +1823,42 @@ def test_c2d_receive(self, c2d_receive_scenario): ) assert headers["IotHub-MessageLockTimeout"] == str(timeout) - assert result["properties"]["system"]["iothub-ack"] == sample_c2d_receive["headers"]["iothub-ack"] - assert result["properties"]["system"]["iothub-correlationid"] == sample_c2d_receive["headers"]["iothub-correlationid"] - assert result["properties"]["system"]["iothub-deliverycount"] == sample_c2d_receive["headers"]["iothub-deliverycount"] - assert result["properties"]["system"]["iothub-expiry"] == sample_c2d_receive["headers"]["iothub-expiry"] - assert result["properties"]["system"]["iothub-enqueuedtime"] == sample_c2d_receive["headers"]["iothub-enqueuedtime"] - assert result["properties"]["system"]["iothub-messageid"] == sample_c2d_receive["headers"]["iothub-messageid"] - assert result["properties"]["system"]["iothub-sequencenumber"] == sample_c2d_receive["headers"]["iothub-sequencenumber"] - assert result["properties"]["system"]["iothub-userid"] == sample_c2d_receive["headers"]["iothub-userid"] - assert result["properties"]["system"]["iothub-to"] == sample_c2d_receive["headers"]["iothub-to"] + assert ( + result["properties"]["system"]["iothub-ack"] + == sample_c2d_receive["headers"]["iothub-ack"] + ) + assert ( + result["properties"]["system"]["iothub-correlationid"] + == sample_c2d_receive["headers"]["iothub-correlationid"] + ) + assert ( + result["properties"]["system"]["iothub-deliverycount"] + == sample_c2d_receive["headers"]["iothub-deliverycount"] + ) + assert ( + result["properties"]["system"]["iothub-expiry"] + == sample_c2d_receive["headers"]["iothub-expiry"] + ) + assert ( + result["properties"]["system"]["iothub-enqueuedtime"] + == sample_c2d_receive["headers"]["iothub-enqueuedtime"] + ) + assert ( + result["properties"]["system"]["iothub-messageid"] + == sample_c2d_receive["headers"]["iothub-messageid"] + ) + assert ( + result["properties"]["system"]["iothub-sequencenumber"] + == sample_c2d_receive["headers"]["iothub-sequencenumber"] + ) + assert ( + result["properties"]["system"]["iothub-userid"] + == sample_c2d_receive["headers"]["iothub-userid"] + ) + assert ( + result["properties"]["system"]["iothub-to"] + == sample_c2d_receive["headers"]["iothub-to"] + ) assert result["etag"] == sample_c2d_receive["headers"]["etag"].strip('"') @@ -1855,9 +1876,9 @@ def test_c2d_receive_ack(self, c2d_receive_ack_scenario): device_id, mock_target["entity"], timeout, - complete=(ack == 'complete'), - reject=(ack == 'reject'), - abandon=(ack == 'abandon') + complete=(ack == "complete"), + reject=(ack == "reject"), + abandon=(ack == "abandon"), ) retrieve, action = service_client.calls[0], service_client.calls[1] @@ -1873,18 +1894,42 @@ def test_c2d_receive_ack(self, c2d_receive_ack_scenario): ) assert headers["IotHub-MessageLockTimeout"] == str(timeout) - assert result["properties"]["system"]["iothub-ack"] == sample_c2d_receive["headers"]["iothub-ack"] - assert result["properties"]["system"]["iothub-correlationid"] == sample_c2d_receive["headers"]["iothub-correlationid"] - assert result["properties"]["system"]["iothub-deliverycount"] == sample_c2d_receive["headers"]["iothub-deliverycount"] - assert result["properties"]["system"]["iothub-expiry"] == sample_c2d_receive["headers"]["iothub-expiry"] - assert result["properties"]["system"]["iothub-enqueuedtime"] == sample_c2d_receive["headers"]["iothub-enqueuedtime"] - assert result["properties"]["system"]["iothub-messageid"] == sample_c2d_receive["headers"]["iothub-messageid"] + assert ( + result["properties"]["system"]["iothub-ack"] + == sample_c2d_receive["headers"]["iothub-ack"] + ) + assert ( + result["properties"]["system"]["iothub-correlationid"] + == sample_c2d_receive["headers"]["iothub-correlationid"] + ) + assert ( + result["properties"]["system"]["iothub-deliverycount"] + == sample_c2d_receive["headers"]["iothub-deliverycount"] + ) + assert ( + result["properties"]["system"]["iothub-expiry"] + == sample_c2d_receive["headers"]["iothub-expiry"] + ) + assert ( + result["properties"]["system"]["iothub-enqueuedtime"] + == sample_c2d_receive["headers"]["iothub-enqueuedtime"] + ) + assert ( + result["properties"]["system"]["iothub-messageid"] + == sample_c2d_receive["headers"]["iothub-messageid"] + ) assert ( result["properties"]["system"]["iothub-sequencenumber"] == sample_c2d_receive["headers"]["iothub-sequencenumber"] ) - assert result["properties"]["system"]["iothub-userid"] == sample_c2d_receive["headers"]["iothub-userid"] - assert result["properties"]["system"]["iothub-to"] == sample_c2d_receive["headers"]["iothub-to"] + assert ( + result["properties"]["system"]["iothub-userid"] + == sample_c2d_receive["headers"]["iothub-userid"] + ) + assert ( + result["properties"]["system"]["iothub-to"] + == sample_c2d_receive["headers"]["iothub-to"] + ) assert result["etag"] == sample_c2d_receive["headers"]["etag"].strip('"') @@ -2024,9 +2069,10 @@ def test_c2d_message_purge(self, c2d_purge_scenario): method = request.method assert method == "DELETE" - assert "https://{}/devices/{}/commands".format( - mock_target["entity"], device_id - ) in url + assert ( + "https://{}/devices/{}/commands".format(mock_target["entity"], device_id) + in url + ) assert result assert result.total_messages_purged == 3 assert result.device_id == device_id @@ -2506,10 +2552,9 @@ def test_device_children_add(self, sc_addchildren): assert "{}/devices/{}?".format(mock_target["entity"], child_device_id) in url assert args[0][0].method == "PUT" assert body["deviceId"] == child_device_id - assert ( - body["deviceScope"] == generate_parent_device().get("deviceScope") or - body["parentScopes"] == [generate_parent_device().get("deviceScope")] - ) + assert body["deviceScope"] == generate_parent_device().get( + "deviceScope" + ) or body["parentScopes"] == [generate_parent_device().get("deviceScope")] @pytest.fixture(params=[(200, 0), (200, 1)]) def sc_invalid_args_addchildren(self, mocker, fixture_ghcs, fixture_sas, request): @@ -2588,9 +2633,7 @@ def sc_listchildren(self, mocker, fixture_ghcs, fixture_sas, request): result.append(generate_child_device(**child_kvp)) test_side_effect = [ build_mock_response(mocker, 200, generate_parent_device()), - build_mock_response( - mocker, 200, result, {"x-ms-continuation": None} - ), + build_mock_response(mocker, 200, result, {"x-ms-continuation": None}), ] service_client.side_effect = test_side_effect return service_client From b100f19e26800634967901b845384baab420e41d Mon Sep 17 00:00:00 2001 From: Paymaun Heidari Date: Fri, 4 Dec 2020 17:37:14 -0800 Subject: [PATCH 2/2] Temp comment out IT caused by inconsistent hub policy. --- azext_iot/tests/test_iot_ext_int.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/azext_iot/tests/test_iot_ext_int.py b/azext_iot/tests/test_iot_ext_int.py index 63bd73fd7..925ab99d0 100644 --- a/azext_iot/tests/test_iot_ext_int.py +++ b/azext_iot/tests/test_iot_ext_int.py @@ -76,9 +76,10 @@ def test_hub(self): LIVE_HUB) conn_str_eventhub_pattern = r'^Endpoint=sb://' - hubs_in_sub = self.cmd('iot hub connection-string show').get_output_in_json() - hubs_in_rg = self.cmd('iot hub connection-string show -g {}'.format(LIVE_RG)).get_output_in_json() - assert len(hubs_in_sub) >= len(hubs_in_rg) + # TODO: Temporarily disable to support warning on missing policy. + # hubs_in_sub = self.cmd('iot hub connection-string show').get_output_in_json() + # hubs_in_rg = self.cmd('iot hub connection-string show -g {}'.format(LIVE_RG)).get_output_in_json() + # assert len(hubs_in_sub) >= len(hubs_in_rg) self.cmd('iot hub connection-string show -n {0}'.format(LIVE_HUB), checks=[ self.check_pattern('connectionString', conn_str_pattern)