From 6cb0428d20871d7a8931664fb496932629f332d1 Mon Sep 17 00:00:00 2001 From: Bryan Alexander Date: Wed, 15 Jan 2020 10:41:54 -0600 Subject: [PATCH 01/15] adds tagging support for cloudwatch events service --- moto/events/models.py | 27 ++++++++++ moto/events/responses.py | 23 ++++++++ moto/utilities/tagging_service.py | 56 ++++++++++++++++++++ tests/test_events/test_events.py | 50 ++++++++++++++--- tests/test_utilities/test_tagging_service.py | 53 ++++++++++++++++++ 5 files changed, 201 insertions(+), 8 deletions(-) create mode 100644 moto/utilities/tagging_service.py create mode 100644 tests/test_utilities/test_tagging_service.py diff --git a/moto/events/models.py b/moto/events/models.py index 548d41393..84a663b6d 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -6,6 +6,7 @@ from boto3 import Session from moto.core.exceptions import JsonRESTError from moto.core import BaseBackend, BaseModel from moto.sts.models import ACCOUNT_ID +from moto.utilities.tagging_service import TaggingService class Rule(BaseModel): @@ -104,6 +105,7 @@ class EventsBackend(BaseBackend): self.region_name = region_name self.event_buses = {} self.event_sources = {} + self.tagger = TaggingService() self._add_default_event_bus() @@ -360,7 +362,32 @@ class EventsBackend(BaseBackend): ) self.event_buses.pop(name, None) + + def list_tags_for_resource(self, arn): + name = arn.split('/')[-1] + if name in self.rules: + return self.tagger.list_tags_for_resource(self.rules[name].arn) + raise JsonRESTError( + "ResourceNotFoundException", "An entity that you specified does not exist." + ) + def tag_resource(self, arn, tags): + name = arn.split('/')[-1] + if name in self.rules: + self.tagger.tag_resource(self.rules[name].arn, tags) + return {} + raise JsonRESTError( + "ResourceNotFoundException", "An entity that you specified does not exist." + ) + + def untag_resource(self, arn, tag_names): + name = arn.split('/')[-1] + if name in self.rules: + self.tagger.untag_resource_using_names(self.rules[name].arn, tag_names) + return {} + raise JsonRESTError( + "ResourceNotFoundException", "An entity that you specified does not exist." + ) events_backends = {} for region in Session().get_available_regions("events"): diff --git a/moto/events/responses.py b/moto/events/responses.py index b415564f8..68c2114a6 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -297,3 +297,26 @@ class EventsHandler(BaseResponse): self.events_backend.delete_event_bus(name) return "", self.response_headers + + def list_tags_for_resource(self): + arn = self._get_param("ResourceARN") + + result = self.events_backend.list_tags_for_resource(arn) + + return json.dumps(result), self.response_headers + + def tag_resource(self): + arn = self._get_param("ResourceARN") + tags = self._get_param("Tags") + + result = self.events_backend.tag_resource(arn, tags) + + return json.dumps(result), self.response_headers + + def untag_resource(self): + arn = self._get_param("ResourceARN") + tags = self._get_param("TagKeys") + + result = self.events_backend.untag_resource(arn, tags) + + return json.dumps(result), self.response_headers diff --git a/moto/utilities/tagging_service.py b/moto/utilities/tagging_service.py new file mode 100644 index 000000000..5eae095ec --- /dev/null +++ b/moto/utilities/tagging_service.py @@ -0,0 +1,56 @@ +class TaggingService: + def __init__(self, tagName='Tags', keyName='Key', valueName='Value'): + self.tagName = tagName + self.keyName = keyName + self.valueName = valueName + self.tags = {} + + def list_tags_for_resource(self, arn): + result = [] + if arn in self.tags: + for k, v in self.tags[arn].items(): + result.append({self.keyName: k, self.valueName: v}) + return {self.tagName: result} + + def tag_resource(self, arn, tags): + if arn not in self.tags: + self.tags[arn] = {} + for t in tags: + if self.valueName in t: + self.tags[arn][t[self.keyName]] = t[self.valueName] + else: + self.tags[arn][t[self.keyName]] = None + + def untag_resource_using_names(self, arn, tag_names): + for name in tag_names: + if name in self.tags.get(arn, {}): + del self.tags[arn][name] + + def untag_resource_using_tags(self, arn, tags): + m = self.tags.get(arn, {}) + for t in tags: + if self.keyName in t: + if t[self.keyName] in m: + if self.valueName in t: + if m[t[self.keyName]] != t[self.valueName]: + continue + # If both key and value are provided, match both before deletion + del m[t[self.keyName]] + + def extract_tag_names(self, tags): + results = [] + if len(tags) == 0: + return results + for tag in tags: + if self.keyName in tag: + results.append(tag[self.keyName]) + return results + + def flatten_tag_list(self, tags): + result = {} + for t in tags: + if self.valueName in t: + result[t[self.keyName]] = t[self.valueName] + else: + result[t[self.keyName]] = None + return result diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index 14d872806..6e9ca3a03 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -5,8 +5,10 @@ import sure # noqa from moto.events import mock_events from botocore.exceptions import ClientError +from moto.core.exceptions import JsonRESTError from nose.tools import assert_raises from moto.core import ACCOUNT_ID +from moto.events.models import EventsBackend RULES = [ {"Name": "test1", "ScheduleExpression": "rate(5 minutes)"}, @@ -136,14 +138,6 @@ def test_list_rule_names_by_target(): assert rule in test_2_target["Rules"] -@mock_events -def test_list_rules(): - client = generate_environment() - - rules = client.list_rules() - assert len(rules["Rules"]) == len(RULES) - - @mock_events def test_delete_rule(): client = generate_environment() @@ -461,3 +455,43 @@ def test_delete_event_bus_errors(): client.delete_event_bus.when.called_with(Name="default").should.throw( ClientError, "Cannot delete event bus default." ) + +@mock_events +def test_rule_tagging_happy(): + client = generate_environment() + rule_name = get_random_rule()["Name"] + rule_arn = client.describe_rule(Name=rule_name).get("Arn") + + tags = [{"Key": "key1", "Value": "value1"}, {"Key": "key2", "Value": "value2"}] + client.tag_resource(ResourceARN=rule_arn, Tags=tags) + + actual = client.list_tags_for_resource(ResourceARN=rule_arn).get("Tags") + assert tags == actual + + client.untag_resource(ResourceARN=rule_arn, TagKeys=["key1"]) + + actual = client.list_tags_for_resource(ResourceARN=rule_arn).get("Tags") + expected = [{"Key": "key2", "Value": "value2"}] + assert expected == actual + +@mock_events +def test_rule_tagging_sad(): + b = EventsBackend("us-west-2") + + try: + b.tag_resource('unknown', []) + raise 'tag_resource should fail if ResourceARN is not known' + except JsonRESTError: + pass + + try: + b.untag_resource('unknown', []) + raise 'untag_resource should fail if ResourceARN is not known' + except JsonRESTError: + pass + + try: + b.list_tags_for_resource('unknown') + raise 'list_tags_for_resource should fail if ResourceARN is not known' + except JsonRESTError: + pass \ No newline at end of file diff --git a/tests/test_utilities/test_tagging_service.py b/tests/test_utilities/test_tagging_service.py new file mode 100644 index 000000000..94415cb2a --- /dev/null +++ b/tests/test_utilities/test_tagging_service.py @@ -0,0 +1,53 @@ +import unittest + +from moto.utilities.tagging_service import TaggingService + + +class TestTaggingService(unittest.TestCase): + def test_list_empty(self): + svc = TaggingService() + result = svc.list_tags_for_resource('test') + self.assertEqual(result, {'Tags': []}) + + def test_create_tag(self): + svc = TaggingService('TheTags', 'TagKey', 'TagValue') + tags = [{'TagKey': 'key_key', 'TagValue': 'value_value'}] + svc.tag_resource('arn', tags) + actual = svc.list_tags_for_resource('arn') + expected = {'TheTags': [{'TagKey': 'key_key', 'TagValue': 'value_value'}]} + self.assertDictEqual(expected, actual) + + def test_create_tag_without_value(self): + svc = TaggingService() + tags = [{'Key': 'key_key'}] + svc.tag_resource('arn', tags) + actual = svc.list_tags_for_resource('arn') + expected = {'Tags': [{'Key': 'key_key', 'Value': ''}]} + self.assertDictEqual(expected, actual) + + def test_delete_tag(self): + svc = TaggingService() + tags = [{'Key': 'key_key', 'Value': 'value_value'}] + svc.tag_resource('arn', tags) + svc.untag_resource('arn', ['key_key']) + result = svc.list_tags_for_resource('arn') + self.assertEqual( + result, {'Tags': []}) + + def test_list_empty_delete(self): + svc = TaggingService() + svc.untag_resource('arn', ['key_key']) + result = svc.list_tags_for_resource('arn') + self.assertEqual( + result, {'Tags': []}) + + def test_extract_tag_names(self): + svc = TaggingService() + tags = [{'Key': 'key1', 'Value': 'value1'}, {'Key': 'key2', 'Value': 'value2'}] + actual = svc.extract_tag_names(tags) + expected = ['key1', 'key2'] + self.assertEqual(expected, actual) + + +if __name__ == '__main__': + unittest.main() From 85207b885b69ec0fb217cc074bd471a8bde98e05 Mon Sep 17 00:00:00 2001 From: Bryan Alexander Date: Thu, 16 Jan 2020 12:10:38 -0600 Subject: [PATCH 02/15] updates KMS service to use TaggingService --- moto/kms/models.py | 45 ++++++++++++++++++-------- moto/kms/responses.py | 18 ++++++++--- tests/test_kms/test_kms.py | 63 +++++++++++++++++++++--------------- tests/test_kms/test_utils.py | 8 ++--- 4 files changed, 87 insertions(+), 47 deletions(-) diff --git a/moto/kms/models.py b/moto/kms/models.py index 22f0039b2..32fcd23ae 100644 --- a/moto/kms/models.py +++ b/moto/kms/models.py @@ -7,13 +7,14 @@ from datetime import datetime, timedelta from boto3 import Session from moto.core import BaseBackend, BaseModel +from moto.core.exceptions import JsonRESTError from moto.core.utils import iso_8601_datetime_without_milliseconds - +from moto.utilities.tagging_service import TaggingService from .utils import decrypt, encrypt, generate_key_id, generate_master_key class Key(BaseModel): - def __init__(self, policy, key_usage, description, tags, region): + def __init__(self, policy, key_usage, description, region): self.id = generate_key_id() self.policy = policy self.key_usage = key_usage @@ -24,7 +25,6 @@ class Key(BaseModel): self.account_id = "012345678912" self.key_rotation_status = False self.deletion_date = None - self.tags = tags or {} self.key_material = generate_master_key() @property @@ -70,11 +70,12 @@ class Key(BaseModel): policy=properties["KeyPolicy"], key_usage="ENCRYPT_DECRYPT", description=properties["Description"], - tags=properties.get("Tags"), region=region_name, ) key.key_rotation_status = properties["EnableKeyRotation"] key.enabled = properties["Enabled"] + kms_backend.tag_resource(key.id, properties.get("Tags")) + return key def get_cfn_attribute(self, attribute_name): @@ -89,24 +90,19 @@ class KmsBackend(BaseBackend): def __init__(self): self.keys = {} self.key_to_aliases = defaultdict(set) + self.tagger = TaggingService(keyName='TagKey', valueName='TagValue') def create_key(self, policy, key_usage, description, tags, region): - key = Key(policy, key_usage, description, tags, region) + key = Key(policy, key_usage, description, region) self.keys[key.id] = key + if tags != None and len(tags) > 0: + self.tag_resource(key.id, tags) return key def update_key_description(self, key_id, description): key = self.keys[self.get_key_id(key_id)] key.description = description - def tag_resource(self, key_id, tags): - key = self.keys[self.get_key_id(key_id)] - key.tags = tags - - def list_resource_tags(self, key_id): - key = self.keys[self.get_key_id(key_id)] - return key.tags - def delete_key(self, key_id): if key_id in self.keys: if key_id in self.key_to_aliases: @@ -282,6 +278,29 @@ class KmsBackend(BaseBackend): return plaintext, ciphertext_blob, arn + def list_resource_tags(self, key_id): + if key_id in self.keys: + return self.tagger.list_tags_for_resource(key_id) + raise JsonRESTError( + "NotFoundException", "The request was rejected because the specified entity or resource could not be found." + ) + + def tag_resource(self, key_id, tags): + if key_id in self.keys: + self.tagger.tag_resource(key_id, tags) + return {} + raise JsonRESTError( + "NotFoundException", "The request was rejected because the specified entity or resource could not be found." + ) + + def untag_resource(self, key_id, tag_names): + if key_id in self.keys: + self.tagger.untag_resource_using_names(key_id, tag_names) + return {} + raise JsonRESTError( + "NotFoundException", "The request was rejected because the specified entity or resource could not be found." + ) + kms_backends = {} for region in Session().get_available_regions("kms"): diff --git a/moto/kms/responses.py b/moto/kms/responses.py index d3a9726e1..3658f0d37 100644 --- a/moto/kms/responses.py +++ b/moto/kms/responses.py @@ -143,17 +143,27 @@ class KmsResponse(BaseResponse): self._validate_cmk_id(key_id) - self.kms_backend.tag_resource(key_id, tags) - return json.dumps({}) + result = self.kms_backend.tag_resource(key_id, tags) + return json.dumps(result) + + def untag_resource(self): + """https://docs.aws.amazon.com/kms/latest/APIReference/API_UntagResource.html""" + key_id = self.parameters.get("KeyId") + tag_names = self.parameters.get("TagKeys") + + self._validate_cmk_id(key_id) + + result = self.kms_backend.untag_resource(key_id, tag_names) + return json.dumps(result) def list_resource_tags(self): """https://docs.aws.amazon.com/kms/latest/APIReference/API_ListResourceTags.html""" key_id = self.parameters.get("KeyId") - self._validate_cmk_id(key_id) tags = self.kms_backend.list_resource_tags(key_id) - return json.dumps({"Tags": tags, "NextMarker": None, "Truncated": False}) + tags.update({"NextMarker": None, "Truncated": False}) + return json.dumps(tags) def describe_key(self): """https://docs.aws.amazon.com/kms/latest/APIReference/API_DescribeKey.html""" diff --git a/tests/test_kms/test_kms.py b/tests/test_kms/test_kms.py index 70fa68787..6a35ee2c8 100644 --- a/tests/test_kms/test_kms.py +++ b/tests/test_kms/test_kms.py @@ -17,7 +17,8 @@ from boto.kms.exceptions import AlreadyExistsException, NotFoundException from freezegun import freeze_time from nose.tools import assert_raises from parameterized import parameterized - +from moto.core.exceptions import JsonRESTError +from moto.kms.models import KmsBackend from moto.kms.exceptions import NotFoundException as MotoNotFoundException from moto import mock_kms, mock_kms_deprecated @@ -910,36 +911,46 @@ def test_update_key_description(): result = client.update_key_description(KeyId=key_id, Description="new_description") assert "ResponseMetadata" in result +@mock_kms +def test_key_tagging_happy(): + client = boto3.client("kms", region_name="us-east-1") + key = client.create_key(Description="test-key-tagging") + key_id = key["KeyMetadata"]["KeyId"] + + tags = [{"TagKey": "key1", "TagValue": "value1"}, {"TagKey": "key2", "TagValue": "value2"}] + client.tag_resource(KeyId=key_id, Tags=tags) + + result = client.list_resource_tags(KeyId=key_id) + actual = result.get("Tags", []) + assert tags == actual + + client.untag_resource(KeyId=key_id, TagKeys=["key1"]) + + actual = client.list_resource_tags(KeyId=key_id).get("Tags", []) + expected = [{"TagKey": "key2", "TagValue": "value2"}] + assert expected == actual @mock_kms -def test_tag_resource(): - client = boto3.client("kms", region_name="us-east-1") - key = client.create_key(Description="cancel-key-deletion") - response = client.schedule_key_deletion(KeyId=key["KeyMetadata"]["KeyId"]) +def test_key_tagging_sad(): + b = KmsBackend() - keyid = response["KeyId"] - response = client.tag_resource( - KeyId=keyid, Tags=[{"TagKey": "string", "TagValue": "string"}] - ) + try: + b.tag_resource('unknown', []) + raise 'tag_resource should fail if KeyId is not known' + except JsonRESTError: + pass - # Shouldn't have any data, just header - assert len(response.keys()) == 1 + try: + b.untag_resource('unknown', []) + raise 'untag_resource should fail if KeyId is not known' + except JsonRESTError: + pass - -@mock_kms -def test_list_resource_tags(): - client = boto3.client("kms", region_name="us-east-1") - key = client.create_key(Description="cancel-key-deletion") - response = client.schedule_key_deletion(KeyId=key["KeyMetadata"]["KeyId"]) - - keyid = response["KeyId"] - response = client.tag_resource( - KeyId=keyid, Tags=[{"TagKey": "string", "TagValue": "string"}] - ) - - response = client.list_resource_tags(KeyId=keyid) - assert response["Tags"][0]["TagKey"] == "string" - assert response["Tags"][0]["TagValue"] == "string" + try: + b.list_resource_tags('unknown') + raise 'list_resource_tags should fail if KeyId is not known' + except JsonRESTError: + pass @parameterized( diff --git a/tests/test_kms/test_utils.py b/tests/test_kms/test_utils.py index f5478e0ef..29ea969b5 100644 --- a/tests/test_kms/test_utils.py +++ b/tests/test_kms/test_utils.py @@ -102,7 +102,7 @@ def test_deserialize_ciphertext_blob(raw, serialized): @parameterized(((ec[0],) for ec in ENCRYPTION_CONTEXT_VECTORS)) def test_encrypt_decrypt_cycle(encryption_context): plaintext = b"some secret plaintext" - master_key = Key("nop", "nop", "nop", [], "nop") + master_key = Key("nop", "nop", "nop", "nop") master_key_map = {master_key.id: master_key} ciphertext_blob = encrypt( @@ -133,7 +133,7 @@ def test_encrypt_unknown_key_id(): def test_decrypt_invalid_ciphertext_format(): - master_key = Key("nop", "nop", "nop", [], "nop") + master_key = Key("nop", "nop", "nop", "nop") master_key_map = {master_key.id: master_key} with assert_raises(InvalidCiphertextException): @@ -153,7 +153,7 @@ def test_decrypt_unknwown_key_id(): def test_decrypt_invalid_ciphertext(): - master_key = Key("nop", "nop", "nop", [], "nop") + master_key = Key("nop", "nop", "nop", "nop") master_key_map = {master_key.id: master_key} ciphertext_blob = ( master_key.id.encode("utf-8") + b"123456789012" @@ -171,7 +171,7 @@ def test_decrypt_invalid_ciphertext(): def test_decrypt_invalid_encryption_context(): plaintext = b"some secret plaintext" - master_key = Key("nop", "nop", "nop", [], "nop") + master_key = Key("nop", "nop", "nop", "nop") master_key_map = {master_key.id: master_key} ciphertext_blob = encrypt( From 9971bcdfcd982a8765c852fb03347682f8da96f4 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Tue, 18 Feb 2020 11:49:55 +0000 Subject: [PATCH 03/15] DynamoDB - Send item to DDB Stream on update, not just on create --- moto/dynamodb2/models.py | 3 ++ tests/test_awslambda/test_lambda.py | 66 ++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 82c3559ea..88f750775 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -1406,6 +1406,7 @@ class DynamoDBBackend(BaseBackend): range_value = None item = table.get_item(hash_value, range_value) + orig_item = copy.deepcopy(item) if not expected: expected = {} @@ -1439,6 +1440,8 @@ class DynamoDBBackend(BaseBackend): ) else: item.update_with_attribute_updates(attribute_updates) + if table.stream_shard is not None: + table.stream_shard.add(orig_item, item) return item def delete_item( diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index d26d78fd4..397da2813 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -1161,7 +1161,7 @@ def test_invoke_function_from_sqs(): @mock_logs @mock_lambda @mock_dynamodb2 -def test_invoke_function_from_dynamodb(): +def test_invoke_function_from_dynamodb_put(): logs_conn = boto3.client("logs", region_name="us-east-1") dynamodb = boto3.client("dynamodb", region_name="us-east-1") table_name = "table_with_stream" @@ -1218,6 +1218,70 @@ def test_invoke_function_from_dynamodb(): assert False, "Test Failed" +@mock_logs +@mock_lambda +@mock_dynamodb2 +def test_invoke_function_from_dynamodb_update(): + logs_conn = boto3.client("logs", region_name="us-east-1") + dynamodb = boto3.client("dynamodb", region_name="us-east-1") + table_name = "table_with_stream" + table = dynamodb.create_table( + TableName=table_name, + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + StreamSpecification={ + "StreamEnabled": True, + "StreamViewType": "NEW_AND_OLD_IMAGES", + }, + ) + dynamodb.put_item(TableName=table_name, Item={"id": {"S": "item 1"}}) + + conn = boto3.client("lambda", region_name="us-east-1") + func = conn.create_function( + FunctionName="testFunction", + Runtime="python2.7", + Role=get_role_name(), + Handler="lambda_function.lambda_handler", + Code={"ZipFile": get_test_zip_file3()}, + Description="test lambda function executed after a DynamoDB table is updated", + Timeout=3, + MemorySize=128, + Publish=True, + ) + + response = conn.create_event_source_mapping( + EventSourceArn=table["TableDescription"]["LatestStreamArn"], + FunctionName=func["FunctionArn"], + ) + + assert response["EventSourceArn"] == table["TableDescription"]["LatestStreamArn"] + assert response["State"] == "Enabled" + dynamodb.update_item(TableName=table_name, + Key={'id': {'S': 'item 1'}}, + UpdateExpression="set #attr = :val", + ExpressionAttributeNames={'#attr': 'new_attr'}, + ExpressionAttributeValues={':val': {'S': 'new_val'}}) + start = time.time() + while (time.time() - start) < 30: + result = logs_conn.describe_log_streams(logGroupName="/aws/lambda/testFunction") + log_streams = result.get("logStreams") + if not log_streams: + time.sleep(1) + continue + + assert len(log_streams) == 1 + result = logs_conn.get_log_events( + logGroupName="/aws/lambda/testFunction", + logStreamName=log_streams[0]["logStreamName"], + ) + for event in result.get("events"): + if event["message"] == "get_test_zip_file3 success": + return + time.sleep(1) + + assert False, "Test Failed" + + @mock_logs @mock_lambda @mock_sqs From 979d20753c4cc861a732cb8ec455e3dd4d35a0f3 Mon Sep 17 00:00:00 2001 From: Laurie O Date: Tue, 18 Feb 2020 21:59:06 +1000 Subject: [PATCH 04/15] Support more defaults in SWF workflow registration SWF workflow type now keeps track of the default task-priority and default AWS Lambda role, set at workflow registration. --- moto/swf/models/workflow_type.py | 2 + moto/swf/responses.py | 12 +++++- .../test_swf/responses/test_workflow_types.py | 37 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/moto/swf/models/workflow_type.py b/moto/swf/models/workflow_type.py index ddb2475b2..137f0e221 100644 --- a/moto/swf/models/workflow_type.py +++ b/moto/swf/models/workflow_type.py @@ -8,6 +8,8 @@ class WorkflowType(GenericType): "defaultChildPolicy", "defaultExecutionStartToCloseTimeout", "defaultTaskStartToCloseTimeout", + "defaultTaskPriority", + "defaultLambdaRole", ] @property diff --git a/moto/swf/responses.py b/moto/swf/responses.py index 98b736cda..c57d966eb 100644 --- a/moto/swf/responses.py +++ b/moto/swf/responses.py @@ -300,6 +300,12 @@ class SWFResponse(BaseResponse): default_execution_start_to_close_timeout = self._params.get( "defaultExecutionStartToCloseTimeout" ) + default_task_priority = self._params.get( + "defaultTaskPriority" + ) + default_lambda_role = self._params.get( + "defaultLambdaRole" + ) description = self._params.get("description") self._check_string(domain) @@ -309,10 +315,10 @@ class SWFResponse(BaseResponse): self._check_none_or_string(default_child_policy) self._check_none_or_string(default_task_start_to_close_timeout) self._check_none_or_string(default_execution_start_to_close_timeout) + self._check_none_or_string(default_task_priority) + self._check_none_or_string(default_lambda_role) self._check_none_or_string(description) - # TODO: add defaultTaskPriority when boto gets to support it - # TODO: add defaultLambdaRole when boto gets to support it self.swf_backend.register_type( "workflow", domain, @@ -322,6 +328,8 @@ class SWFResponse(BaseResponse): default_child_policy=default_child_policy, default_task_start_to_close_timeout=default_task_start_to_close_timeout, default_execution_start_to_close_timeout=default_execution_start_to_close_timeout, + default_task_priority=default_task_priority, + default_lambda_role=default_lambda_role, description=description, ) return "" diff --git a/tests/test_swf/responses/test_workflow_types.py b/tests/test_swf/responses/test_workflow_types.py index 4c92d7762..72aa814d2 100644 --- a/tests/test_swf/responses/test_workflow_types.py +++ b/tests/test_swf/responses/test_workflow_types.py @@ -1,7 +1,9 @@ import sure import boto +import boto3 from moto import mock_swf_deprecated +from moto import mock_swf from boto.swf.exceptions import SWFResponseError @@ -133,6 +135,41 @@ def test_describe_workflow_type(): infos["status"].should.equal("REGISTERED") +@mock_swf +def test_describe_workflow_type_full_boto3(): + # boto3 required as boto doesn't support all of the arguments + client = boto3.client("swf", region_name="us-east-1") + client.register_domain( + name="test-domain", workflowExecutionRetentionPeriodInDays="2" + ) + client.register_workflow_type( + domain="test-domain", + name="test-workflow", + version="v1.0", + description="Test workflow.", + defaultTaskStartToCloseTimeout="20", + defaultExecutionStartToCloseTimeout="60", + defaultTaskList={"name": "foo"}, + defaultTaskPriority="-2", + defaultChildPolicy="ABANDON", + defaultLambdaRole="arn:bar", + ) + + resp = client.describe_workflow_type( + domain="test-domain", workflowType={"name": "test-workflow", "version": "v1.0"} + ) + resp["typeInfo"]["workflowType"]["name"].should.equal("test-workflow") + resp["typeInfo"]["workflowType"]["version"].should.equal("v1.0") + resp["typeInfo"]["status"].should.equal("REGISTERED") + resp["typeInfo"]["description"].should.equal("Test workflow.") + resp["configuration"]["defaultTaskStartToCloseTimeout"].should.equal("20") + resp["configuration"]["defaultExecutionStartToCloseTimeout"].should.equal("60") + resp["configuration"]["defaultTaskList"]["name"].should.equal("foo") + resp["configuration"]["defaultTaskPriority"].should.equal("-2") + resp["configuration"]["defaultChildPolicy"].should.equal("ABANDON") + resp["configuration"]["defaultLambdaRole"].should.equal("arn:bar") + + @mock_swf_deprecated def test_describe_non_existent_workflow_type(): conn = boto.connect_swf("the_key", "the_secret") From 5863d9fab9bc1b78f4ff8739202fea59965b3635 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Tue, 18 Feb 2020 12:34:24 +0000 Subject: [PATCH 05/15] Linting --- tests/test_awslambda/test_lambda.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_awslambda/test_lambda.py b/tests/test_awslambda/test_lambda.py index 397da2813..3c3185c8a 100644 --- a/tests/test_awslambda/test_lambda.py +++ b/tests/test_awslambda/test_lambda.py @@ -1256,11 +1256,13 @@ def test_invoke_function_from_dynamodb_update(): assert response["EventSourceArn"] == table["TableDescription"]["LatestStreamArn"] assert response["State"] == "Enabled" - dynamodb.update_item(TableName=table_name, - Key={'id': {'S': 'item 1'}}, - UpdateExpression="set #attr = :val", - ExpressionAttributeNames={'#attr': 'new_attr'}, - ExpressionAttributeValues={':val': {'S': 'new_val'}}) + dynamodb.update_item( + TableName=table_name, + Key={"id": {"S": "item 1"}}, + UpdateExpression="set #attr = :val", + ExpressionAttributeNames={"#attr": "new_attr"}, + ExpressionAttributeValues={":val": {"S": "new_val"}}, + ) start = time.time() while (time.time() - start) < 30: result = logs_conn.describe_log_streams(logGroupName="/aws/lambda/testFunction") From 3500e7d5d39d4a583cbd02c9ef0254394e5e9254 Mon Sep 17 00:00:00 2001 From: Laurie O Date: Tue, 18 Feb 2020 23:00:37 +1000 Subject: [PATCH 06/15] Styling --- moto/swf/responses.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/moto/swf/responses.py b/moto/swf/responses.py index c57d966eb..2b7794ffd 100644 --- a/moto/swf/responses.py +++ b/moto/swf/responses.py @@ -300,12 +300,8 @@ class SWFResponse(BaseResponse): default_execution_start_to_close_timeout = self._params.get( "defaultExecutionStartToCloseTimeout" ) - default_task_priority = self._params.get( - "defaultTaskPriority" - ) - default_lambda_role = self._params.get( - "defaultLambdaRole" - ) + default_task_priority = self._params.get("defaultTaskPriority") + default_lambda_role = self._params.get("defaultLambdaRole") description = self._params.get("description") self._check_string(domain) From b64a571a37453c36e565f09637a4ae2638a4f910 Mon Sep 17 00:00:00 2001 From: Bryan Alexander Date: Tue, 18 Feb 2020 10:33:27 -0600 Subject: [PATCH 07/15] adds utilities init --- moto/utilities/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 moto/utilities/__init__.py diff --git a/moto/utilities/__init__.py b/moto/utilities/__init__.py new file mode 100644 index 000000000..e69de29bb From 1d140852947d1dc318b05b765e2bc0326aac07c3 Mon Sep 17 00:00:00 2001 From: Jon Beilke Date: Tue, 18 Feb 2020 10:49:35 -0600 Subject: [PATCH 08/15] add API Gateway authorizers --- moto/apigateway/exceptions.py | 9 + moto/apigateway/models.py | 121 +++++++++++ moto/apigateway/responses.py | 84 ++++++++ moto/apigateway/urls.py | 2 + tests/test_apigateway/test_apigateway.py | 250 ++++++++++++++++++++++- 5 files changed, 465 insertions(+), 1 deletion(-) diff --git a/moto/apigateway/exceptions.py b/moto/apigateway/exceptions.py index 2a306ab99..ccb870f52 100644 --- a/moto/apigateway/exceptions.py +++ b/moto/apigateway/exceptions.py @@ -85,6 +85,15 @@ class NoMethodDefined(BadRequestException): ) +class AuthorizerNotFoundException(RESTError): + code = 404 + + def __init__(self): + super(AuthorizerNotFoundException, self).__init__( + "NotFoundException", "Invalid Authorizer identifier specified" + ) + + class StageNotFoundException(RESTError): code = 404 diff --git a/moto/apigateway/models.py b/moto/apigateway/models.py index ae7bdfac3..c0e570630 100644 --- a/moto/apigateway/models.py +++ b/moto/apigateway/models.py @@ -28,6 +28,7 @@ from .exceptions import ( InvalidHttpEndpoint, InvalidResourcePathException, InvalidRequestInput, + AuthorizerNotFoundException, StageNotFoundException, RoleNotSpecified, NoIntegrationDefined, @@ -182,6 +183,54 @@ class Resource(BaseModel): return self.resource_methods[method_type].pop("methodIntegration") +class Authorizer(BaseModel, dict): + def __init__(self, id, name, authorizer_type, **kwargs): + super(Authorizer, self).__init__() + self["id"] = id + self["name"] = name + self["type"] = authorizer_type + if kwargs.get("provider_arns"): + self["providerARNs"] = kwargs.get("provider_arns") + if kwargs.get("auth_type"): + self["authType"] = kwargs.get("auth_type") + if kwargs.get("authorizer_uri"): + self["authorizerUri"] = kwargs.get("authorizer_uri") + if kwargs.get("authorizer_credentials"): + self["authorizerCredentials"] = kwargs.get("authorizer_credentials") + if kwargs.get("identity_source"): + self["identitySource"] = kwargs.get("identity_source") + if kwargs.get("identity_validation_expression"): + self["identityValidationExpression"] = kwargs.get( + "identity_validation_expression" + ) + self["authorizerResultTtlInSeconds"] = kwargs.get("authorizer_result_ttl") + + def apply_operations(self, patch_operations): + for op in patch_operations: + if "/authorizerUri" in op["path"]: + self["authorizerUri"] = op["value"] + elif "/authorizerCredentials" in op["path"]: + self["authorizerCredentials"] = op["value"] + elif "/authorizerResultTtlInSeconds" in op["path"]: + self["authorizerResultTtlInSeconds"] = int(op["value"]) + elif "/authType" in op["path"]: + self["authType"] = op["value"] + elif "/identitySource" in op["path"]: + self["identitySource"] = op["value"] + elif "/identityValidationExpression" in op["path"]: + self["identityValidationExpression"] = op["value"] + elif "/name" in op["path"]: + self["name"] = op["value"] + elif "/providerARNs" in op["path"]: + # TODO: add and remove + raise Exception('Patch operation for "%s" not implemented' % op["path"]) + elif "/type" in op["path"]: + self["type"] = op["value"] + else: + raise Exception('Patch operation "%s" not implemented' % op["op"]) + return self + + class Stage(BaseModel, dict): def __init__( self, @@ -407,6 +456,7 @@ class RestAPI(BaseModel): self.tags = kwargs.get("tags") or {} self.deployments = {} + self.authorizers = {} self.stages = {} self.resources = {} @@ -474,6 +524,34 @@ class RestAPI(BaseModel): ), ) + def create_authorizer( + self, + id, + name, + authorizer_type, + provider_arns=None, + auth_type=None, + authorizer_uri=None, + authorizer_credentials=None, + identity_source=None, + identiy_validation_expression=None, + authorizer_result_ttl=None, + ): + authorizer = Authorizer( + id=id, + name=name, + authorizer_type=authorizer_type, + provider_arns=provider_arns, + auth_type=auth_type, + authorizer_uri=authorizer_uri, + authorizer_credentials=authorizer_credentials, + identity_source=identity_source, + identiy_validation_expression=identiy_validation_expression, + authorizer_result_ttl=authorizer_result_ttl, + ) + self.authorizers[id] = authorizer + return authorizer + def create_stage( self, name, @@ -513,6 +591,9 @@ class RestAPI(BaseModel): def get_deployment(self, deployment_id): return self.deployments[deployment_id] + def get_authorizers(self): + return list(self.authorizers.values()) + def get_stages(self): return list(self.stages.values()) @@ -599,6 +680,46 @@ class APIGatewayBackend(BaseBackend): method = resource.add_method(method_type, authorization_type) return method + def get_authorizer(self, restapi_id, authorizer_id): + api = self.get_rest_api(restapi_id) + authorizer = api.authorizers.get(authorizer_id) + if authorizer is None: + raise AuthorizerNotFoundException() + else: + return authorizer + + def get_authorizers(self, restapi_id): + api = self.get_rest_api(restapi_id) + return api.get_authorizers() + + def create_authorizer(self, restapi_id, name, authorizer_type, **kwargs): + api = self.get_rest_api(restapi_id) + authorizer_id = create_id() + authorizer = api.create_authorizer( + authorizer_id, + name, + authorizer_type, + provider_arns=kwargs.get("provider_arns"), + auth_type=kwargs.get("auth_type"), + authorizer_uri=kwargs.get("authorizer_uri"), + authorizer_credentials=kwargs.get("authorizer_credentials"), + identity_source=kwargs.get("identity_source"), + identiy_validation_expression=kwargs.get("identiy_validation_expression"), + authorizer_result_ttl=kwargs.get("authorizer_result_ttl"), + ) + return api.authorizers.get(authorizer["id"]) + + def update_authorizer(self, restapi_id, authorizer_id, patch_operations): + authorizer = self.get_authorizer(restapi_id, authorizer_id) + if not authorizer: + api = self.get_rest_api(restapi_id) + authorizer = api.authorizers[authorizer_id] = Authorizer() + return authorizer.apply_operations(patch_operations) + + def delete_authorizer(self, restapi_id, authorizer_id): + api = self.get_rest_api(restapi_id) + del api.authorizers[authorizer_id] + def get_stage(self, function_id, stage_name): api = self.get_rest_api(function_id) stage = api.stages.get(stage_name) diff --git a/moto/apigateway/responses.py b/moto/apigateway/responses.py index e10d670c5..14a20832a 100644 --- a/moto/apigateway/responses.py +++ b/moto/apigateway/responses.py @@ -8,11 +8,13 @@ from .exceptions import ( ApiKeyNotFoundException, BadRequestException, CrossAccountNotAllowed, + AuthorizerNotFoundException, StageNotFoundException, ApiKeyAlreadyExists, ) API_KEY_SOURCES = ["AUTHORIZER", "HEADER"] +AUTHORIZER_TYPES = ["TOKEN", "REQUEST", "COGNITO_USER_POOLS"] ENDPOINT_CONFIGURATION_TYPES = ["PRIVATE", "EDGE", "REGIONAL"] @@ -172,6 +174,88 @@ class APIGatewayResponse(BaseResponse): ) return 200, {}, json.dumps(method_response) + def restapis_authorizers(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + url_path_parts = self.path.split("/") + restapi_id = url_path_parts[2] + + if self.method == "POST": + name = self._get_param("name") + authorizer_type = self._get_param("type") + + provider_arns = self._get_param_with_default_value("providerARNs", None) + auth_type = self._get_param_with_default_value("authType", None) + authorizer_uri = self._get_param_with_default_value("authorizerUri", None) + authorizer_credentials = self._get_param_with_default_value( + "authorizerCredentials", None + ) + identity_source = self._get_param_with_default_value("identitySource", None) + identiy_validation_expression = self._get_param_with_default_value( + "identityValidationExpression", None + ) + authorizer_result_ttl = self._get_param_with_default_value( + "authorizerResultTtlInSeconds", 300 + ) + + # Param validation + if authorizer_type and authorizer_type not in AUTHORIZER_TYPES: + return self.error( + "ValidationException", + ( + "1 validation error detected: " + "Value '{authorizer_type}' at 'createAuthorizerInput.type' failed " + "to satisfy constraint: Member must satisfy enum value set: " + "[TOKEN, REQUEST, COGNITO_USER_POOLS]" + ).format(authorizer_type=authorizer_type), + ) + + authorizer_response = self.backend.create_authorizer( + restapi_id, + name, + authorizer_type, + provider_arns=provider_arns, + auth_type=auth_type, + authorizer_uri=authorizer_uri, + authorizer_credentials=authorizer_credentials, + identity_source=identity_source, + identiy_validation_expression=identiy_validation_expression, + authorizer_result_ttl=authorizer_result_ttl, + ) + elif self.method == "GET": + authorizers = self.backend.get_authorizers(restapi_id) + return 200, {}, json.dumps({"item": authorizers}) + + return 200, {}, json.dumps(authorizer_response) + + def authorizers(self, request, full_url, headers): + self.setup_class(request, full_url, headers) + url_path_parts = self.path.split("/") + restapi_id = url_path_parts[2] + authorizer_id = url_path_parts[4] + + if self.method == "GET": + try: + authorizer_response = self.backend.get_authorizer( + restapi_id, authorizer_id + ) + except AuthorizerNotFoundException as error: + return ( + error.code, + {}, + '{{"message":"{0}","code":"{1}"}}'.format( + error.message, error.error_type + ), + ) + elif self.method == "PATCH": + patch_operations = self._get_param("patchOperations") + authorizer_response = self.backend.update_authorizer( + restapi_id, authorizer_id, patch_operations + ) + elif self.method == "DELETE": + self.backend.delete_authorizer(restapi_id, authorizer_id) + return 202, {}, "{}" + return 200, {}, json.dumps(authorizer_response) + def restapis_stages(self, request, full_url, headers): self.setup_class(request, full_url, headers) url_path_parts = self.path.split("/") diff --git a/moto/apigateway/urls.py b/moto/apigateway/urls.py index bb2b2d216..4ef6ae72b 100644 --- a/moto/apigateway/urls.py +++ b/moto/apigateway/urls.py @@ -7,6 +7,8 @@ url_paths = { "{0}/restapis$": APIGatewayResponse().restapis, "{0}/restapis/(?P[^/]+)/?$": APIGatewayResponse().restapis_individual, "{0}/restapis/(?P[^/]+)/resources$": APIGatewayResponse().resources, + "{0}/restapis/(?P[^/]+)/authorizers$": APIGatewayResponse().restapis_authorizers, + "{0}/restapis/(?P[^/]+)/authorizers/(?P[^/]+)/?$": APIGatewayResponse().authorizers, "{0}/restapis/(?P[^/]+)/stages$": APIGatewayResponse().restapis_stages, "{0}/restapis/(?P[^/]+)/stages/(?P[^/]+)/?$": APIGatewayResponse().stages, "{0}/restapis/(?P[^/]+)/deployments$": APIGatewayResponse().deployments, diff --git a/tests/test_apigateway/test_apigateway.py b/tests/test_apigateway/test_apigateway.py index 496098e8c..0b2b75b0b 100644 --- a/tests/test_apigateway/test_apigateway.py +++ b/tests/test_apigateway/test_apigateway.py @@ -8,7 +8,7 @@ import sure # noqa from botocore.exceptions import ClientError import responses -from moto import mock_apigateway, settings +from moto import mock_apigateway, mock_cognitoidp, settings from moto.core import ACCOUNT_ID from nose.tools import assert_raises @@ -547,6 +547,254 @@ def test_integration_response(): response["methodIntegration"]["integrationResponses"].should.equal({}) +@mock_apigateway +@mock_cognitoidp +def test_update_authorizer_configuration(): + client = boto3.client("apigateway", region_name="us-west-2") + authorizer_name = "my_authorizer" + response = client.create_rest_api(name="my_api", description="this is my api") + api_id = response["id"] + + cognito_client = boto3.client("cognito-idp", region_name="us-west-2") + user_pool_arn = cognito_client.create_user_pool(PoolName="my_cognito_pool")[ + "UserPool" + ]["Arn"] + + response = client.create_authorizer( + restApiId=api_id, + name=authorizer_name, + type="COGNITO_USER_POOLS", + providerARNs=[user_pool_arn], + identitySource="method.request.header.Authorization", + ) + authorizer_id = response["id"] + + response = client.get_authorizer(restApiId=api_id, authorizerId=authorizer_id) + # createdDate is hard to match against, remove it + response.pop("createdDate", None) + # this is hard to match against, so remove it + response["ResponseMetadata"].pop("HTTPHeaders", None) + response["ResponseMetadata"].pop("RetryAttempts", None) + response.should.equal( + { + "id": authorizer_id, + "name": authorizer_name, + "type": "COGNITO_USER_POOLS", + "providerARNs": [user_pool_arn], + "identitySource": "method.request.header.Authorization", + "authorizerResultTtlInSeconds": 300, + "ResponseMetadata": {"HTTPStatusCode": 200}, + } + ) + + client.update_authorizer( + restApiId=api_id, + authorizerId=authorizer_id, + patchOperations=[{"op": "replace", "path": "/type", "value": "TOKEN"}], + ) + + authorizer = client.get_authorizer(restApiId=api_id, authorizerId=authorizer_id) + + authorizer.should.have.key("type").which.should.equal("TOKEN") + + client.update_authorizer( + restApiId=api_id, + authorizerId=authorizer_id, + patchOperations=[{"op": "replace", "path": "/type", "value": "REQUEST"}], + ) + + authorizer = client.get_authorizer(restApiId=api_id, authorizerId=authorizer_id) + + authorizer.should.have.key("type").which.should.equal("REQUEST") + + # TODO: implement mult-update tests + + try: + client.update_authorizer( + restApiId=api_id, + authorizerId=authorizer_id, + patchOperations=[ + {"op": "add", "path": "/notasetting", "value": "eu-west-1"} + ], + ) + assert False.should.be.ok # Fail, should not be here + except Exception: + assert True.should.be.ok + + +@mock_apigateway +def test_non_existent_authorizer(): + client = boto3.client("apigateway", region_name="us-west-2") + response = client.create_rest_api(name="my_api", description="this is my api") + api_id = response["id"] + + client.get_authorizer.when.called_with( + restApiId=api_id, authorizerId="xxx" + ).should.throw(ClientError) + + +@mock_apigateway +@mock_cognitoidp +def test_create_authorizer(): + client = boto3.client("apigateway", region_name="us-west-2") + authorizer_name = "my_authorizer" + response = client.create_rest_api(name="my_api", description="this is my api") + api_id = response["id"] + + cognito_client = boto3.client("cognito-idp", region_name="us-west-2") + user_pool_arn = cognito_client.create_user_pool(PoolName="my_cognito_pool")[ + "UserPool" + ]["Arn"] + + response = client.create_authorizer( + restApiId=api_id, + name=authorizer_name, + type="COGNITO_USER_POOLS", + providerARNs=[user_pool_arn], + identitySource="method.request.header.Authorization", + ) + authorizer_id = response["id"] + + response = client.get_authorizer(restApiId=api_id, authorizerId=authorizer_id) + # createdDate is hard to match against, remove it + response.pop("createdDate", None) + # this is hard to match against, so remove it + response["ResponseMetadata"].pop("HTTPHeaders", None) + response["ResponseMetadata"].pop("RetryAttempts", None) + response.should.equal( + { + "id": authorizer_id, + "name": authorizer_name, + "type": "COGNITO_USER_POOLS", + "providerARNs": [user_pool_arn], + "identitySource": "method.request.header.Authorization", + "authorizerResultTtlInSeconds": 300, + "ResponseMetadata": {"HTTPStatusCode": 200}, + } + ) + + authorizer_name2 = "my_authorizer2" + response = client.create_authorizer( + restApiId=api_id, + name=authorizer_name2, + type="COGNITO_USER_POOLS", + providerARNs=[user_pool_arn], + identitySource="method.request.header.Authorization", + ) + authorizer_id2 = response["id"] + + response = client.get_authorizers(restApiId=api_id) + + # this is hard to match against, so remove it + response["ResponseMetadata"].pop("HTTPHeaders", None) + response["ResponseMetadata"].pop("RetryAttempts", None) + + response["items"][0]["id"].should.match( + r"{0}|{1}".format(authorizer_id2, authorizer_id) + ) + response["items"][1]["id"].should.match( + r"{0}|{1}".format(authorizer_id2, authorizer_id) + ) + + new_authorizer_name_with_vars = "authorizer_with_vars" + response = client.create_authorizer( + restApiId=api_id, + name=new_authorizer_name_with_vars, + type="COGNITO_USER_POOLS", + providerARNs=[user_pool_arn], + identitySource="method.request.header.Authorization", + ) + authorizer_id3 = response["id"] + + # this is hard to match against, so remove it + response["ResponseMetadata"].pop("HTTPHeaders", None) + response["ResponseMetadata"].pop("RetryAttempts", None) + + response.should.equal( + { + "name": new_authorizer_name_with_vars, + "id": authorizer_id3, + "type": "COGNITO_USER_POOLS", + "providerARNs": [user_pool_arn], + "identitySource": "method.request.header.Authorization", + "authorizerResultTtlInSeconds": 300, + "ResponseMetadata": {"HTTPStatusCode": 200}, + } + ) + + stage = client.get_authorizer(restApiId=api_id, authorizerId=authorizer_id3) + stage["name"].should.equal(new_authorizer_name_with_vars) + stage["id"].should.equal(authorizer_id3) + stage["type"].should.equal("COGNITO_USER_POOLS") + stage["providerARNs"].should.equal([user_pool_arn]) + stage["identitySource"].should.equal("method.request.header.Authorization") + stage["authorizerResultTtlInSeconds"].should.equal(300) + + +@mock_apigateway +@mock_cognitoidp +def test_delete_authorizer(): + client = boto3.client("apigateway", region_name="us-west-2") + authorizer_name = "my_authorizer" + response = client.create_rest_api(name="my_api", description="this is my api") + api_id = response["id"] + + cognito_client = boto3.client("cognito-idp", region_name="us-west-2") + user_pool_arn = cognito_client.create_user_pool(PoolName="my_cognito_pool")[ + "UserPool" + ]["Arn"] + + response = client.create_authorizer( + restApiId=api_id, + name=authorizer_name, + type="COGNITO_USER_POOLS", + providerARNs=[user_pool_arn], + identitySource="method.request.header.Authorization", + ) + authorizer_id = response["id"] + + response = client.get_authorizer(restApiId=api_id, authorizerId=authorizer_id) + # createdDate is hard to match against, remove it + response.pop("createdDate", None) + # this is hard to match against, so remove it + response["ResponseMetadata"].pop("HTTPHeaders", None) + response["ResponseMetadata"].pop("RetryAttempts", None) + response.should.equal( + { + "id": authorizer_id, + "name": authorizer_name, + "type": "COGNITO_USER_POOLS", + "providerARNs": [user_pool_arn], + "identitySource": "method.request.header.Authorization", + "authorizerResultTtlInSeconds": 300, + "ResponseMetadata": {"HTTPStatusCode": 200}, + } + ) + + authorizer_name2 = "my_authorizer2" + response = client.create_authorizer( + restApiId=api_id, + name=authorizer_name2, + type="COGNITO_USER_POOLS", + providerARNs=[user_pool_arn], + identitySource="method.request.header.Authorization", + ) + authorizer_id2 = response["id"] + + authorizers = client.get_authorizers(restApiId=api_id)["items"] + sorted([authorizer["name"] for authorizer in authorizers]).should.equal( + sorted([authorizer_name2, authorizer_name]) + ) + # delete stage + response = client.delete_authorizer(restApiId=api_id, authorizerId=authorizer_id2) + response["ResponseMetadata"]["HTTPStatusCode"].should.equal(202) + # verify other stage still exists + authorizers = client.get_authorizers(restApiId=api_id)["items"] + sorted([authorizer["name"] for authorizer in authorizers]).should.equal( + sorted([authorizer_name]) + ) + + @mock_apigateway def test_update_stage_configuration(): client = boto3.client("apigateway", region_name="us-west-2") From d1efedec2952a8597624d821ecaaa718597612a9 Mon Sep 17 00:00:00 2001 From: Bryan Alexander Date: Tue, 18 Feb 2020 13:40:34 -0600 Subject: [PATCH 09/15] updates kms to use tagging service and support untag_resource --- moto/kms/models.py | 25 +++++---------------- tests/test_kms/test_kms.py | 43 ++++++++++++++++++++++++++++++++++++ tests/test_kms/test_utils.py | 8 +++---- 3 files changed, 53 insertions(+), 23 deletions(-) diff --git a/moto/kms/models.py b/moto/kms/models.py index 9f61b275f..3d0da036e 100644 --- a/moto/kms/models.py +++ b/moto/kms/models.py @@ -7,27 +7,18 @@ from datetime import datetime, timedelta from boto3 import Session from moto.core import BaseBackend, BaseModel -<<<<<<< HEAD -from moto.core.exceptions import JsonRESTError -from moto.core.utils import iso_8601_datetime_without_milliseconds -from moto.utilities.tagging_service import TaggingService -======= from moto.core.utils import unix_time - +from moto.utilities.tagging_service import TaggingService +from moto.core.exceptions import JsonRESTError from moto.iam.models import ACCOUNT_ID ->>>>>>> 100dbd529f174f18d579a1dcc066d55409f2e38f from .utils import decrypt, encrypt, generate_key_id, generate_master_key class Key(BaseModel): -<<<<<<< HEAD - def __init__(self, policy, key_usage, description, region): -======= def __init__( - self, policy, key_usage, customer_master_key_spec, description, tags, region + self, policy, key_usage, customer_master_key_spec, description, region ): ->>>>>>> 100dbd529f174f18d579a1dcc066d55409f2e38f self.id = generate_key_id() self.creation_date = unix_time() self.policy = policy @@ -142,19 +133,14 @@ class KmsBackend(BaseBackend): self.key_to_aliases = defaultdict(set) self.tagger = TaggingService(keyName='TagKey', valueName='TagValue') -<<<<<<< HEAD - def create_key(self, policy, key_usage, description, tags, region): - key = Key(policy, key_usage, description, region) -======= def create_key( self, policy, key_usage, customer_master_key_spec, description, tags, region ): key = Key( - policy, key_usage, customer_master_key_spec, description, tags, region + policy, key_usage, customer_master_key_spec, description, region ) ->>>>>>> 100dbd529f174f18d579a1dcc066d55409f2e38f self.keys[key.id] = key - if tags != None and len(tags) > 0: + if tags is not None and len(tags) > 0: self.tag_resource(key.id, tags) return key @@ -166,6 +152,7 @@ class KmsBackend(BaseBackend): if key_id in self.keys: if key_id in self.key_to_aliases: self.key_to_aliases.pop(key_id) + self.tagger.delete_all_tags_for_resource(key_id) return self.keys.pop(key_id) diff --git a/tests/test_kms/test_kms.py b/tests/test_kms/test_kms.py index aaf09a6be..d2dca6786 100644 --- a/tests/test_kms/test_kms.py +++ b/tests/test_kms/test_kms.py @@ -680,3 +680,46 @@ def test__assert_default_policy(): _assert_default_policy.when.called_with("default").should_not.throw( MotoNotFoundException ) + + +@mock_kms +def test_key_tagging_happy(): + client = boto3.client("kms", region_name="us-east-1") + key = client.create_key(Description="test-key-tagging") + key_id = key["KeyMetadata"]["KeyId"] + + tags = [{"TagKey": "key1", "TagValue": "value1"}, {"TagKey": "key2", "TagValue": "value2"}] + client.tag_resource(KeyId=key_id, Tags=tags) + + result = client.list_resource_tags(KeyId=key_id) + actual = result.get("Tags", []) + assert tags == actual + + client.untag_resource(KeyId=key_id, TagKeys=["key1"]) + + actual = client.list_resource_tags(KeyId=key_id).get("Tags", []) + expected = [{"TagKey": "key2", "TagValue": "value2"}] + assert expected == actual + + +@mock_kms +def test_key_tagging_sad(): + b = KmsBackend() + + try: + b.tag_resource('unknown', []) + raise 'tag_resource should fail if KeyId is not known' + except JsonRESTError: + pass + + try: + b.untag_resource('unknown', []) + raise 'untag_resource should fail if KeyId is not known' + except JsonRESTError: + pass + + try: + b.list_resource_tags('unknown') + raise 'list_resource_tags should fail if KeyId is not known' + except JsonRESTError: + pass diff --git a/tests/test_kms/test_utils.py b/tests/test_kms/test_utils.py index 4c84ed127..4446635f3 100644 --- a/tests/test_kms/test_utils.py +++ b/tests/test_kms/test_utils.py @@ -102,7 +102,7 @@ def test_deserialize_ciphertext_blob(raw, serialized): @parameterized(((ec[0],) for ec in ENCRYPTION_CONTEXT_VECTORS)) def test_encrypt_decrypt_cycle(encryption_context): plaintext = b"some secret plaintext" - master_key = Key("nop", "nop", "nop", "nop", [], "nop") + master_key = Key("nop", "nop", "nop", "nop", "nop") master_key_map = {master_key.id: master_key} ciphertext_blob = encrypt( @@ -133,7 +133,7 @@ def test_encrypt_unknown_key_id(): def test_decrypt_invalid_ciphertext_format(): - master_key = Key("nop", "nop", "nop", "nop", [], "nop") + master_key = Key("nop", "nop", "nop", "nop", "nop") master_key_map = {master_key.id: master_key} with assert_raises(InvalidCiphertextException): @@ -153,7 +153,7 @@ def test_decrypt_unknwown_key_id(): def test_decrypt_invalid_ciphertext(): - master_key = Key("nop", "nop", "nop", "nop", [], "nop") + master_key = Key("nop", "nop", "nop", "nop", "nop") master_key_map = {master_key.id: master_key} ciphertext_blob = ( master_key.id.encode("utf-8") + b"123456789012" @@ -171,7 +171,7 @@ def test_decrypt_invalid_ciphertext(): def test_decrypt_invalid_encryption_context(): plaintext = b"some secret plaintext" - master_key = Key("nop", "nop", "nop", "nop", [], "nop") + master_key = Key("nop", "nop", "nop", "nop", "nop") master_key_map = {master_key.id: master_key} ciphertext_blob = encrypt( From 4e2fe76820025a75ce6282047e3ae0663ea45ccc Mon Sep 17 00:00:00 2001 From: Bryan Alexander Date: Tue, 18 Feb 2020 13:51:35 -0600 Subject: [PATCH 10/15] removes duplicate declaration of list_tags_for_resource --- moto/events/models.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/moto/events/models.py b/moto/events/models.py index c400677df..6787f51ab 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -374,14 +374,6 @@ class EventsBackend(BaseBackend): "ResourceNotFoundException", "An entity that you specified does not exist." ) - def list_tags_for_resource(self, arn): - name = arn.split("/")[-1] - if name in self.rules: - return self.tagger.list_tags_for_resource(self.rules[name].arn) - raise JsonRESTError( - "ResourceNotFoundException", "An entity that you specified does not exist." - ) - def tag_resource(self, arn, tags): name = arn.split("/")[-1] if name in self.rules: From 1432e82606946bd9d7b002bbbb5e87423011308f Mon Sep 17 00:00:00 2001 From: Bryan Alexander Date: Tue, 18 Feb 2020 14:01:15 -0600 Subject: [PATCH 11/15] fixes kms/models create_key parameters --- moto/kms/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/kms/models.py b/moto/kms/models.py index 3d0da036e..89cc5758a 100644 --- a/moto/kms/models.py +++ b/moto/kms/models.py @@ -111,11 +111,11 @@ class Key(BaseModel): key_usage="ENCRYPT_DECRYPT", customer_master_key_spec="SYMMETRIC_DEFAULT", description=properties["Description"], + tags=properties["Tags"], region=region_name, ) key.key_rotation_status = properties["EnableKeyRotation"] key.enabled = properties["Enabled"] - kms_backend.tag_resource(key.id, properties.get("Tags")) return key From 38413577fc04164886728ac46f1b7563054f56b3 Mon Sep 17 00:00:00 2001 From: Bryan Alexander Date: Wed, 19 Feb 2020 09:18:01 -0600 Subject: [PATCH 12/15] fixes bug in resourcetaggingapi/get_kms_tags --- moto/resourcegroupstaggingapi/models.py | 2 +- tests/test_events/test_events.py | 7 ------ tests/test_kms/test_kms.py | 4 ++-- .../test_resourcegroupstaggingapi.py | 23 +++++++++++++++++++ 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py index 850ab5c04..8c17864f3 100644 --- a/moto/resourcegroupstaggingapi/models.py +++ b/moto/resourcegroupstaggingapi/models.py @@ -318,7 +318,7 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): # KMS def get_kms_tags(kms_key_id): result = [] - for tag in self.kms_backend.list_resource_tags(kms_key_id): + for tag in self.kms_backend.list_resource_tags(kms_key_id).get("Tags",[]): result.append({"Key": tag["TagKey"], "Value": tag["TagValue"]}) return result diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index cf3743d34..80fadb449 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -10,9 +10,6 @@ from moto.core.exceptions import JsonRESTError from nose.tools import assert_raises from moto.core import ACCOUNT_ID -<< << << < HEAD -== == == = ->>>>>> > 100dbd529f174f18d579a1dcc066d55409f2e38f RULES = [ {"Name": "test1", "ScheduleExpression": "rate(5 minutes)"}, @@ -461,10 +458,6 @@ def test_delete_event_bus_errors(): ) -<< << << < HEAD -== == == = - ->>>>>> > 100dbd529f174f18d579a1dcc066d55409f2e38f @mock_events def test_rule_tagging_happy(): client = generate_environment() diff --git a/tests/test_kms/test_kms.py b/tests/test_kms/test_kms.py index d2dca6786..d00c885f2 100644 --- a/tests/test_kms/test_kms.py +++ b/tests/test_kms/test_kms.py @@ -682,7 +682,7 @@ def test__assert_default_policy(): ) -@mock_kms +@mock_kms_deprecated def test_key_tagging_happy(): client = boto3.client("kms", region_name="us-east-1") key = client.create_key(Description="test-key-tagging") @@ -702,7 +702,7 @@ def test_key_tagging_happy(): assert expected == actual -@mock_kms +@mock_kms_deprecated def test_key_tagging_sad(): b = KmsBackend() diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py index 3ee517ce8..dc75bb722 100644 --- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py +++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py @@ -248,3 +248,26 @@ def test_get_many_resources(): ) # TODO test pagenation + + +@mock_kms +def test_get_kms_tags(): + kms = boto3.client("kms", region_name="us-east-1") + key = kms.create_key( + KeyUsage="ENCRYPT_DECRYPT", + Tags=[ + {"TagKey": "key_name", "TagValue": "a_value"}, + {"TagKey": "key_2", "TagValue": "val2"}, + ], + ) + key_id = key["KeyMetadata"]["KeyId"] + + rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-east-1") + resp = rtapi.get_resources( + ResourceTypeFilters=["kms"], + TagFilters=[{"Key": "key_name"}], + ) + resp["ResourceTagMappingList"].should.have.length_of(1) + resp["ResourceTagMappingList"][0]["Tags"].should.contain( + {"Key": "key_name", "Value": "a_value"} + ) From 7205ab77854e7e086a204bc7df9d28c8a747ffcb Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Thu, 20 Feb 2020 08:59:21 +0000 Subject: [PATCH 13/15] #1427 - EMR - Return start time of first step --- moto/emr/models.py | 5 +++++ moto/emr/responses.py | 2 +- tests/test_emr/test_emr_boto3.py | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/moto/emr/models.py b/moto/emr/models.py index 713b15b9f..d9ec2fd69 100644 --- a/moto/emr/models.py +++ b/moto/emr/models.py @@ -86,6 +86,9 @@ class FakeStep(BaseModel): self.start_datetime = None self.state = state + def start(self): + self.start_datetime = datetime.now(pytz.utc) + class FakeCluster(BaseModel): def __init__( @@ -204,6 +207,8 @@ class FakeCluster(BaseModel): self.start_cluster() self.run_bootstrap_actions() + if self.steps: + self.steps[0].start() @property def instance_groups(self): diff --git a/moto/emr/responses.py b/moto/emr/responses.py index 94847ec8b..38b9774e1 100644 --- a/moto/emr/responses.py +++ b/moto/emr/responses.py @@ -835,7 +835,7 @@ LIST_STEPS_TEMPLATE = """ Date: Wed, 19 Feb 2020 09:12:13 -0500 Subject: [PATCH 14/15] fix test cases, bug when no tags are present and conflict --- moto/events/models.py | 2 +- moto/kms/models.py | 17 ++--- moto/resourcegroupstaggingapi/models.py | 2 +- setup.cfg | 2 +- tests/test_kms/test_kms.py | 65 ++++++++++++++----- .../test_resourcegroupstaggingapi.py | 23 ------- 6 files changed, 61 insertions(+), 50 deletions(-) diff --git a/moto/events/models.py b/moto/events/models.py index 6787f51ab..a80b86daa 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -367,7 +367,7 @@ class EventsBackend(BaseBackend): self.event_buses.pop(name, None) def list_tags_for_resource(self, arn): - name = arn.split('/')[-1] + name = arn.split("/")[-1] if name in self.rules: return self.tagger.list_tags_for_resource(self.rules[name].arn) raise JsonRESTError( diff --git a/moto/kms/models.py b/moto/kms/models.py index 89cc5758a..36f72e6de 100644 --- a/moto/kms/models.py +++ b/moto/kms/models.py @@ -111,7 +111,7 @@ class Key(BaseModel): key_usage="ENCRYPT_DECRYPT", customer_master_key_spec="SYMMETRIC_DEFAULT", description=properties["Description"], - tags=properties["Tags"], + tags=properties.get("Tags", []), region=region_name, ) key.key_rotation_status = properties["EnableKeyRotation"] @@ -131,14 +131,12 @@ class KmsBackend(BaseBackend): def __init__(self): self.keys = {} self.key_to_aliases = defaultdict(set) - self.tagger = TaggingService(keyName='TagKey', valueName='TagValue') + self.tagger = TaggingService(keyName="TagKey", valueName="TagValue") def create_key( self, policy, key_usage, customer_master_key_spec, description, tags, region ): - key = Key( - policy, key_usage, customer_master_key_spec, description, region - ) + key = Key(policy, key_usage, customer_master_key_spec, description, region) self.keys[key.id] = key if tags is not None and len(tags) > 0: self.tag_resource(key.id, tags) @@ -326,7 +324,8 @@ class KmsBackend(BaseBackend): if key_id in self.keys: return self.tagger.list_tags_for_resource(key_id) raise JsonRESTError( - "NotFoundException", "The request was rejected because the specified entity or resource could not be found." + "NotFoundException", + "The request was rejected because the specified entity or resource could not be found.", ) def tag_resource(self, key_id, tags): @@ -334,7 +333,8 @@ class KmsBackend(BaseBackend): self.tagger.tag_resource(key_id, tags) return {} raise JsonRESTError( - "NotFoundException", "The request was rejected because the specified entity or resource could not be found." + "NotFoundException", + "The request was rejected because the specified entity or resource could not be found.", ) def untag_resource(self, key_id, tag_names): @@ -342,7 +342,8 @@ class KmsBackend(BaseBackend): self.tagger.untag_resource_using_names(key_id, tag_names) return {} raise JsonRESTError( - "NotFoundException", "The request was rejected because the specified entity or resource could not be found." + "NotFoundException", + "The request was rejected because the specified entity or resource could not be found.", ) diff --git a/moto/resourcegroupstaggingapi/models.py b/moto/resourcegroupstaggingapi/models.py index 8c17864f3..d05a53f81 100644 --- a/moto/resourcegroupstaggingapi/models.py +++ b/moto/resourcegroupstaggingapi/models.py @@ -318,7 +318,7 @@ class ResourceGroupsTaggingAPIBackend(BaseBackend): # KMS def get_kms_tags(kms_key_id): result = [] - for tag in self.kms_backend.list_resource_tags(kms_key_id).get("Tags",[]): + for tag in self.kms_backend.list_resource_tags(kms_key_id).get("Tags", []): result.append({"Key": tag["TagKey"], "Value": tag["TagValue"]}) return result diff --git a/setup.cfg b/setup.cfg index fb04c16a8..9dbd988db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [nosetests] verbosity=1 detailed-errors=1 -with-coverage=1 +#with-coverage=1 cover-package=moto [bdist_wheel] diff --git a/tests/test_kms/test_kms.py b/tests/test_kms/test_kms.py index d00c885f2..3384d940e 100644 --- a/tests/test_kms/test_kms.py +++ b/tests/test_kms/test_kms.py @@ -2,8 +2,10 @@ from __future__ import unicode_literals import base64 import re +from collections import OrderedDict import boto.kms +import boto3 import six import sure # noqa from boto.exception import JSONResponseError @@ -13,7 +15,7 @@ from parameterized import parameterized from moto.core.exceptions import JsonRESTError from moto.kms.models import KmsBackend from moto.kms.exceptions import NotFoundException as MotoNotFoundException -from moto import mock_kms_deprecated +from moto import mock_kms_deprecated, mock_kms PLAINTEXT_VECTORS = ( (b"some encodeable plaintext",), @@ -682,24 +684,55 @@ def test__assert_default_policy(): ) -@mock_kms_deprecated -def test_key_tagging_happy(): - client = boto3.client("kms", region_name="us-east-1") - key = client.create_key(Description="test-key-tagging") - key_id = key["KeyMetadata"]["KeyId"] +if six.PY2: + sort = sorted +else: + sort = lambda l: sorted(l, key=lambda d: d.keys()) - tags = [{"TagKey": "key1", "TagValue": "value1"}, {"TagKey": "key2", "TagValue": "value2"}] - client.tag_resource(KeyId=key_id, Tags=tags) + +@mock_kms +def test_key_tag_on_create_key_happy(): + client = boto3.client("kms", region_name="us-east-1") + + tags = [ + {"TagKey": "key1", "TagValue": "value1"}, + {"TagKey": "key2", "TagValue": "value2"}, + ] + key = client.create_key(Description="test-key-tagging", Tags=tags) + key_id = key["KeyMetadata"]["KeyId"] result = client.list_resource_tags(KeyId=key_id) actual = result.get("Tags", []) - assert tags == actual + assert sort(tags) == sort(actual) client.untag_resource(KeyId=key_id, TagKeys=["key1"]) actual = client.list_resource_tags(KeyId=key_id).get("Tags", []) expected = [{"TagKey": "key2", "TagValue": "value2"}] - assert expected == actual + assert sort(expected) == sort(actual) + + +@mock_kms +def test_key_tag_added_happy(): + client = boto3.client("kms", region_name="us-east-1") + + key = client.create_key(Description="test-key-tagging") + key_id = key["KeyMetadata"]["KeyId"] + tags = [ + {"TagKey": "key1", "TagValue": "value1"}, + {"TagKey": "key2", "TagValue": "value2"}, + ] + client.tag_resource(KeyId=key_id, Tags=tags) + + result = client.list_resource_tags(KeyId=key_id) + actual = result.get("Tags", []) + assert sort(tags) == sort(actual) + + client.untag_resource(KeyId=key_id, TagKeys=["key1"]) + + actual = client.list_resource_tags(KeyId=key_id).get("Tags", []) + expected = [{"TagKey": "key2", "TagValue": "value2"}] + assert sort(expected) == sort(actual) @mock_kms_deprecated @@ -707,19 +740,19 @@ def test_key_tagging_sad(): b = KmsBackend() try: - b.tag_resource('unknown', []) - raise 'tag_resource should fail if KeyId is not known' + b.tag_resource("unknown", []) + raise "tag_resource should fail if KeyId is not known" except JsonRESTError: pass try: - b.untag_resource('unknown', []) - raise 'untag_resource should fail if KeyId is not known' + b.untag_resource("unknown", []) + raise "untag_resource should fail if KeyId is not known" except JsonRESTError: pass try: - b.list_resource_tags('unknown') - raise 'list_resource_tags should fail if KeyId is not known' + b.list_resource_tags("unknown") + raise "list_resource_tags should fail if KeyId is not known" except JsonRESTError: pass diff --git a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py index dc75bb722..3ee517ce8 100644 --- a/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py +++ b/tests/test_resourcegroupstaggingapi/test_resourcegroupstaggingapi.py @@ -248,26 +248,3 @@ def test_get_many_resources(): ) # TODO test pagenation - - -@mock_kms -def test_get_kms_tags(): - kms = boto3.client("kms", region_name="us-east-1") - key = kms.create_key( - KeyUsage="ENCRYPT_DECRYPT", - Tags=[ - {"TagKey": "key_name", "TagValue": "a_value"}, - {"TagKey": "key_2", "TagValue": "val2"}, - ], - ) - key_id = key["KeyMetadata"]["KeyId"] - - rtapi = boto3.client("resourcegroupstaggingapi", region_name="us-east-1") - resp = rtapi.get_resources( - ResourceTypeFilters=["kms"], - TagFilters=[{"Key": "key_name"}], - ) - resp["ResourceTagMappingList"].should.have.length_of(1) - resp["ResourceTagMappingList"][0]["Tags"].should.contain( - {"Key": "key_name", "Value": "a_value"} - ) From c162f02091e1c19428b30f6d03de61687b625568 Mon Sep 17 00:00:00 2001 From: Brady Date: Fri, 21 Feb 2020 15:39:23 -0500 Subject: [PATCH 15/15] re-add coverage and remove unused import --- setup.cfg | 2 +- tests/test_kms/test_kms.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 9dbd988db..fb04c16a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [nosetests] verbosity=1 detailed-errors=1 -#with-coverage=1 +with-coverage=1 cover-package=moto [bdist_wheel] diff --git a/tests/test_kms/test_kms.py b/tests/test_kms/test_kms.py index 3384d940e..a04a24a82 100644 --- a/tests/test_kms/test_kms.py +++ b/tests/test_kms/test_kms.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals import base64 import re -from collections import OrderedDict import boto.kms import boto3