diff --git a/.travis.yml b/.travis.yml index 8d22aa98f..f1854db54 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,9 +35,10 @@ install: if [ "$TEST_SERVER_MODE" = "true" ]; then python wait_for.py fi +before_script: +- if [[ $TRAVIS_PYTHON_VERSION == "3.7" ]]; then make lint; fi script: - make test-only -- if [[ $TRAVIS_PYTHON_VERSION == "3.7" ]]; then make lint; fi after_success: - coveralls before_deploy: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8413c796d..732dad23a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,6 @@ Moto Changelog * delete_configuration_aggregator() * describe_aggregation_authorizations() * describe_configuration_aggregators() - * describe_identity_pool() * get_resource_config_history() * list_aggregate_discovered_resources() (For S3) * list_discovered_resources() (For S3) @@ -39,6 +38,7 @@ Moto Changelog * put_configuration_aggregator() * Cognito * assume_role_with_web_identity() + * describe_identity_pool() * get_open_id_token() * update_user_pool_domain() * DataSync: diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index 2e5f055b9..c026ef601 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -2718,12 +2718,12 @@ - [ ] upgrade_elasticsearch_domain ## events -48% implemented +58% implemented - [ ] activate_event_source -- [ ] create_event_bus +- [X] create_event_bus - [ ] create_partner_event_source - [ ] deactivate_event_source -- [ ] delete_event_bus +- [X] delete_event_bus - [ ] delete_partner_event_source - [X] delete_rule - [X] describe_event_bus @@ -2732,7 +2732,7 @@ - [X] describe_rule - [X] disable_rule - [X] enable_rule -- [ ] list_event_buses +- [X] list_event_buses - [ ] list_event_sources - [ ] list_partner_event_source_accounts - [ ] list_partner_event_sources @@ -3977,46 +3977,46 @@ - [ ] update_resource ## lambda -0% implemented +41% implemented - [ ] add_layer_version_permission - [ ] add_permission - [ ] create_alias -- [ ] create_event_source_mapping -- [ ] create_function +- [X] create_event_source_mapping +- [X] create_function - [ ] delete_alias -- [ ] delete_event_source_mapping -- [ ] delete_function +- [X] delete_event_source_mapping +- [X] delete_function - [ ] delete_function_concurrency - [ ] delete_layer_version - [ ] get_account_settings - [ ] get_alias -- [ ] get_event_source_mapping -- [ ] get_function +- [X] get_event_source_mapping +- [X] get_function - [ ] get_function_configuration - [ ] get_layer_version - [ ] get_layer_version_by_arn - [ ] get_layer_version_policy - [ ] get_policy -- [ ] invoke +- [X] invoke - [ ] invoke_async - [ ] list_aliases -- [ ] list_event_source_mappings -- [ ] list_functions +- [X] list_event_source_mappings +- [X] list_functions - [ ] list_layer_versions - [ ] list_layers -- [ ] list_tags -- [ ] list_versions_by_function +- [X] list_tags +- [X] list_versions_by_function - [ ] publish_layer_version - [ ] publish_version - [ ] put_function_concurrency - [ ] remove_layer_version_permission - [ ] remove_permission -- [ ] tag_resource -- [ ] untag_resource +- [X] tag_resource +- [X] untag_resource - [ ] update_alias -- [ ] update_event_source_mapping -- [ ] update_function_code -- [ ] update_function_configuration +- [X] update_event_source_mapping +- [X] update_function_code +- [X] update_function_configuration ## lex-models 0% implemented @@ -4723,7 +4723,7 @@ - [ ] update_server_engine_attributes ## organizations -41% implemented +43% implemented - [ ] accept_handshake - [X] attach_policy - [ ] cancel_handshake @@ -4737,7 +4737,7 @@ - [ ] delete_organizational_unit - [ ] delete_policy - [X] describe_account -- [ ] describe_create_account_status +- [X] describe_create_account_status - [ ] describe_handshake - [X] describe_organization - [X] describe_organizational_unit diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py index a58582599..fe5c24c53 100644 --- a/moto/awslambda/models.py +++ b/moto/awslambda/models.py @@ -979,6 +979,32 @@ class LambdaBackend(BaseBackend): def add_policy(self, function_name, policy): self.get_function(function_name).policy = policy + def update_function_code(self, function_name, qualifier, body): + fn = self.get_function(function_name, qualifier) + + if fn: + if body.get("Publish", False): + fn = self.publish_function(function_name) + + config = fn.update_function_code(body) + return config + else: + return None + + def update_function_configuration(self, function_name, qualifier, body): + fn = self.get_function(function_name, qualifier) + + return fn.update_configuration(body) if fn else None + + def invoke(self, function_name, qualifier, body, headers, response_headers): + fn = self.get_function(function_name, qualifier) + if fn: + payload = fn.invoke(body, headers, response_headers) + response_headers["Content-Length"] = str(len(payload)) + return response_headers, payload + else: + return response_headers, None + def do_validate_s3(): return os.environ.get("VALIDATE_LAMBDA_S3", "") in ["", "1", "true"] diff --git a/moto/awslambda/responses.py b/moto/awslambda/responses.py index 62265b310..96a8afda6 100644 --- a/moto/awslambda/responses.py +++ b/moto/awslambda/responses.py @@ -168,10 +168,10 @@ class LambdaResponse(BaseResponse): function_name = self.path.rsplit("/", 2)[-2] qualifier = self._get_param("qualifier") - fn = self.lambda_backend.get_function(function_name, qualifier) - if fn: - payload = fn.invoke(self.body, self.headers, response_headers) - response_headers["Content-Length"] = str(len(payload)) + response_header, payload = self.lambda_backend.invoke( + function_name, qualifier, self.body, self.headers, response_headers + ) + if payload: return 202, response_headers, payload else: return 404, response_headers, "{}" @@ -337,26 +337,23 @@ class LambdaResponse(BaseResponse): def _put_configuration(self, request): function_name = self.path.rsplit("/", 2)[-2] qualifier = self._get_param("Qualifier", None) + resp = self.lambda_backend.update_function_configuration( + function_name, qualifier, body=self.json_body + ) - fn = self.lambda_backend.get_function(function_name, qualifier) - - if fn: - config = fn.update_configuration(self.json_body) - return 200, {}, json.dumps(config) + if resp: + return 200, {}, json.dumps(resp) else: return 404, {}, "{}" def _put_code(self): function_name = self.path.rsplit("/", 2)[-2] qualifier = self._get_param("Qualifier", None) + resp = self.lambda_backend.update_function_code( + function_name, qualifier, body=self.json_body + ) - fn = self.lambda_backend.get_function(function_name, qualifier) - - if fn: - if self.json_body.get("Publish", False): - fn = self.lambda_backend.publish_function(function_name) - - config = fn.update_function_code(self.json_body) - return 200, {}, json.dumps(config) + if resp: + return 200, {}, json.dumps(resp) else: return 404, {}, "{}" diff --git a/moto/datasync/models.py b/moto/datasync/models.py index 42626cceb..17a2659fb 100644 --- a/moto/datasync/models.py +++ b/moto/datasync/models.py @@ -27,12 +27,14 @@ class Task(BaseModel): name, region_name, arn_counter=0, + metadata=None, ): self.source_location_arn = source_location_arn self.destination_location_arn = destination_location_arn + self.name = name + self.metadata = metadata # For simplicity Tasks are either available or running self.status = "AVAILABLE" - self.name = name self.current_task_execution_arn = None # Generate ARN self.arn = "arn:aws:datasync:{0}:111222333444:task/task-{1}".format( @@ -129,7 +131,27 @@ class DataSyncBackend(BaseBackend): self.locations[location.arn] = location return location.arn - def create_task(self, source_location_arn, destination_location_arn, name): + def _get_location(self, location_arn, typ): + if location_arn not in self.locations: + raise InvalidRequestException( + "Location {0} is not found.".format(location_arn) + ) + location = self.locations[location_arn] + if location.typ != typ: + raise InvalidRequestException( + "Invalid Location type: {0}".format(location.typ) + ) + return location + + def delete_location(self, location_arn): + if location_arn in self.locations: + del self.locations[location_arn] + else: + raise InvalidRequestException + + def create_task( + self, source_location_arn, destination_location_arn, name, metadata=None + ): if source_location_arn not in self.locations: raise InvalidRequestException( "Location {0} not found.".format(source_location_arn) @@ -145,10 +167,33 @@ class DataSyncBackend(BaseBackend): name, region_name=self.region_name, arn_counter=self.arn_counter, + metadata=metadata, ) self.tasks[task.arn] = task return task.arn + def _get_task(self, task_arn): + if task_arn in self.tasks: + return self.tasks[task_arn] + else: + raise InvalidRequestException + + def update_task(self, task_arn, name, metadata): + if task_arn in self.tasks: + task = self.tasks[task_arn] + task.name = name + task.metadata = metadata + else: + raise InvalidRequestException( + "Sync task {0} is not found.".format(task_arn) + ) + + def delete_task(self, task_arn): + if task_arn in self.tasks: + del self.tasks[task_arn] + else: + raise InvalidRequestException + def start_task_execution(self, task_arn): self.arn_counter = self.arn_counter + 1 if task_arn in self.tasks: @@ -161,12 +206,19 @@ class DataSyncBackend(BaseBackend): return task_execution.arn raise InvalidRequestException("Invalid request.") + def _get_task_execution(self, task_execution_arn): + if task_execution_arn in self.task_executions: + return self.task_executions[task_execution_arn] + else: + raise InvalidRequestException + def cancel_task_execution(self, task_execution_arn): if task_execution_arn in self.task_executions: task_execution = self.task_executions[task_execution_arn] task_execution.cancel() task_arn = task_execution.task_arn self.tasks[task_arn].current_task_execution_arn = None + self.tasks[task_arn].status = "AVAILABLE" return raise InvalidRequestException( "Sync task {0} is not found.".format(task_execution_arn) diff --git a/moto/datasync/responses.py b/moto/datasync/responses.py index 30b906d44..23a480523 100644 --- a/moto/datasync/responses.py +++ b/moto/datasync/responses.py @@ -2,7 +2,6 @@ import json from moto.core.responses import BaseResponse -from .exceptions import InvalidRequestException from .models import datasync_backends @@ -18,17 +17,7 @@ class DataSyncResponse(BaseResponse): return json.dumps({"Locations": locations}) def _get_location(self, location_arn, typ): - location_arn = self._get_param("LocationArn") - if location_arn not in self.datasync_backend.locations: - raise InvalidRequestException( - "Location {0} is not found.".format(location_arn) - ) - location = self.datasync_backend.locations[location_arn] - if location.typ != typ: - raise InvalidRequestException( - "Invalid Location type: {0}".format(location.typ) - ) - return location + return self.datasync_backend._get_location(location_arn, typ) def create_location_s3(self): # s3://bucket_name/folder/ @@ -86,16 +75,40 @@ class DataSyncResponse(BaseResponse): } ) + def delete_location(self): + location_arn = self._get_param("LocationArn") + self.datasync_backend.delete_location(location_arn) + return json.dumps({}) + def create_task(self): destination_location_arn = self._get_param("DestinationLocationArn") source_location_arn = self._get_param("SourceLocationArn") name = self._get_param("Name") - + metadata = { + "CloudWatchLogGroupArn": self._get_param("CloudWatchLogGroupArn"), + "Options": self._get_param("Options"), + "Excludes": self._get_param("Excludes"), + "Tags": self._get_param("Tags"), + } arn = self.datasync_backend.create_task( - source_location_arn, destination_location_arn, name + source_location_arn, destination_location_arn, name, metadata=metadata ) return json.dumps({"TaskArn": arn}) + def update_task(self): + task_arn = self._get_param("TaskArn") + self.datasync_backend.update_task( + task_arn, + name=self._get_param("Name"), + metadata={ + "CloudWatchLogGroupArn": self._get_param("CloudWatchLogGroupArn"), + "Options": self._get_param("Options"), + "Excludes": self._get_param("Excludes"), + "Tags": self._get_param("Tags"), + }, + ) + return json.dumps({}) + def list_tasks(self): tasks = list() for arn, task in self.datasync_backend.tasks.items(): @@ -104,29 +117,32 @@ class DataSyncResponse(BaseResponse): ) return json.dumps({"Tasks": tasks}) + def delete_task(self): + task_arn = self._get_param("TaskArn") + self.datasync_backend.delete_task(task_arn) + return json.dumps({}) + def describe_task(self): task_arn = self._get_param("TaskArn") - if task_arn in self.datasync_backend.tasks: - task = self.datasync_backend.tasks[task_arn] - return json.dumps( - { - "TaskArn": task.arn, - "Name": task.name, - "CurrentTaskExecutionArn": task.current_task_execution_arn, - "Status": task.status, - "SourceLocationArn": task.source_location_arn, - "DestinationLocationArn": task.destination_location_arn, - } - ) - raise InvalidRequestException + task = self.datasync_backend._get_task(task_arn) + return json.dumps( + { + "TaskArn": task.arn, + "Status": task.status, + "Name": task.name, + "CurrentTaskExecutionArn": task.current_task_execution_arn, + "SourceLocationArn": task.source_location_arn, + "DestinationLocationArn": task.destination_location_arn, + "CloudWatchLogGroupArn": task.metadata["CloudWatchLogGroupArn"], + "Options": task.metadata["Options"], + "Excludes": task.metadata["Excludes"], + } + ) def start_task_execution(self): task_arn = self._get_param("TaskArn") - if task_arn in self.datasync_backend.tasks: - arn = self.datasync_backend.start_task_execution(task_arn) - if arn: - return json.dumps({"TaskExecutionArn": arn}) - raise InvalidRequestException("Invalid request.") + arn = self.datasync_backend.start_task_execution(task_arn) + return json.dumps({"TaskExecutionArn": arn}) def cancel_task_execution(self): task_execution_arn = self._get_param("TaskExecutionArn") @@ -135,21 +151,12 @@ class DataSyncResponse(BaseResponse): def describe_task_execution(self): task_execution_arn = self._get_param("TaskExecutionArn") - - if task_execution_arn in self.datasync_backend.task_executions: - task_execution = self.datasync_backend.task_executions[task_execution_arn] - if task_execution: - result = json.dumps( - { - "TaskExecutionArn": task_execution.arn, - "Status": task_execution.status, - } - ) - if task_execution.status == "SUCCESS": - self.datasync_backend.tasks[ - task_execution.task_arn - ].status = "AVAILABLE" - # Simulate task being executed - task_execution.iterate_status() - return result - raise InvalidRequestException + task_execution = self.datasync_backend._get_task_execution(task_execution_arn) + result = json.dumps( + {"TaskExecutionArn": task_execution.arn, "Status": task_execution.status,} + ) + if task_execution.status == "SUCCESS": + self.datasync_backend.tasks[task_execution.task_arn].status = "AVAILABLE" + # Simulate task being executed + task_execution.iterate_status() + return result diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py index 8a061041e..6361f8961 100644 --- a/moto/dynamodb2/models.py +++ b/moto/dynamodb2/models.py @@ -77,6 +77,7 @@ class DynamoType(object): attr, list_index = attribute_is_list(attr) if not key: # {'S': value} ==> {'S': new_value} + self.type = new_value.type self.value = new_value.value else: if attr not in self.value: # nonexistingattribute diff --git a/moto/ec2/models.py b/moto/ec2/models.py index efbbeb6fe..ccc7c7a37 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -214,6 +214,7 @@ class NetworkInterface(TaggedEC2Resource): ec2_backend, subnet, private_ip_address, + private_ip_addresses=None, device_index=0, public_ip_auto_assign=True, group_ids=None, @@ -223,6 +224,7 @@ class NetworkInterface(TaggedEC2Resource): self.id = random_eni_id() self.device_index = device_index self.private_ip_address = private_ip_address or random_private_ip() + self.private_ip_addresses = private_ip_addresses self.subnet = subnet self.instance = None self.attachment_id = None @@ -341,12 +343,19 @@ class NetworkInterfaceBackend(object): super(NetworkInterfaceBackend, self).__init__() def create_network_interface( - self, subnet, private_ip_address, group_ids=None, description=None, **kwargs + self, + subnet, + private_ip_address, + private_ip_addresses=None, + group_ids=None, + description=None, + **kwargs ): eni = NetworkInterface( self, subnet, private_ip_address, + private_ip_addresses, group_ids=group_ids, description=description, **kwargs @@ -2819,6 +2828,9 @@ class Subnet(TaggedEC2Resource): self.vpc_id = vpc_id self.cidr_block = cidr_block self.cidr = ipaddress.IPv4Network(six.text_type(self.cidr_block), strict=False) + self._available_ip_addresses = ( + ipaddress.IPv4Network(six.text_type(self.cidr_block)).num_addresses - 5 + ) self._availability_zone = availability_zone self.default_for_az = default_for_az self.map_public_ip_on_launch = map_public_ip_on_launch @@ -2854,6 +2866,21 @@ class Subnet(TaggedEC2Resource): return subnet + @property + def available_ip_addresses(self): + enis = [ + eni + for eni in self.ec2_backend.get_all_network_interfaces() + if eni.subnet.id == self.id + ] + addresses_taken = [ + eni.private_ip_address for eni in enis if eni.private_ip_address + ] + for eni in enis: + if eni.private_ip_addresses: + addresses_taken.extend(eni.private_ip_addresses) + return str(self._available_ip_addresses - len(addresses_taken)) + @property def availability_zone(self): return self._availability_zone.name diff --git a/moto/ec2/responses/elastic_network_interfaces.py b/moto/ec2/responses/elastic_network_interfaces.py index fa014b219..6761b294e 100644 --- a/moto/ec2/responses/elastic_network_interfaces.py +++ b/moto/ec2/responses/elastic_network_interfaces.py @@ -7,12 +7,13 @@ class ElasticNetworkInterfaces(BaseResponse): def create_network_interface(self): subnet_id = self._get_param("SubnetId") private_ip_address = self._get_param("PrivateIpAddress") + private_ip_addresses = self._get_multi_param("PrivateIpAddresses") groups = self._get_multi_param("SecurityGroupId") subnet = self.ec2_backend.get_subnet(subnet_id) description = self._get_param("Description") if self.is_not_dryrun("CreateNetworkInterface"): eni = self.ec2_backend.create_network_interface( - subnet, private_ip_address, groups, description + subnet, private_ip_address, private_ip_addresses, groups, description ) template = self.response_template(CREATE_NETWORK_INTERFACE_RESPONSE) return template.render(eni=eni) diff --git a/moto/ec2/responses/subnets.py b/moto/ec2/responses/subnets.py index c42583f23..e11984e52 100644 --- a/moto/ec2/responses/subnets.py +++ b/moto/ec2/responses/subnets.py @@ -53,7 +53,7 @@ CREATE_SUBNET_RESPONSE = """ pending {{ subnet.vpc_id }} {{ subnet.cidr_block }} - 251 + {{ subnet.available_ip_addresses }} {{ subnet._availability_zone.name }} {{ subnet._availability_zone.zone_id }} {{ subnet.default_for_az }} @@ -81,7 +81,7 @@ DESCRIBE_SUBNETS_RESPONSE = """ available {{ subnet.vpc_id }} {{ subnet.cidr_block }} - 251 + {{ subnet.available_ip_addresses }} {{ subnet._availability_zone.name }} {{ subnet._availability_zone.zone_id }} {{ subnet.default_for_az }} diff --git a/moto/events/models.py b/moto/events/models.py index e69062b2c..be4153b9f 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -5,6 +5,7 @@ import boto3 from moto.core.exceptions import JsonRESTError from moto.core import BaseBackend, BaseModel +from moto.sts.models import ACCOUNT_ID class Rule(BaseModel): @@ -54,6 +55,42 @@ class Rule(BaseModel): self.targets.pop(index) +class EventBus(BaseModel): + def __init__(self, region_name, name): + self.region = region_name + self.name = name + + self._permissions = {} + + @property + def arn(self): + return "arn:aws:events:{region}:{account_id}:event-bus/{name}".format( + region=self.region, account_id=ACCOUNT_ID, name=self.name + ) + + @property + def policy(self): + if not len(self._permissions): + return None + + policy = {"Version": "2012-10-17", "Statement": []} + + for sid, permission in self._permissions.items(): + policy["Statement"].append( + { + "Sid": sid, + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::{}:root".format(permission["Principal"]) + }, + "Action": permission["Action"], + "Resource": self.arn, + } + ) + + return json.dumps(policy) + + class EventsBackend(BaseBackend): ACCOUNT_ID = re.compile(r"^(\d{1,12}|\*)$") STATEMENT_ID = re.compile(r"^[a-zA-Z0-9-_]{1,64}$") @@ -65,13 +102,19 @@ class EventsBackend(BaseBackend): self.rules_order = [] self.next_tokens = {} self.region_name = region_name - self.permissions = {} + self.event_buses = {} + self.event_sources = {} + + self._add_default_event_bus() def reset(self): region_name = self.region_name self.__dict__ = {} self.__init__(region_name) + def _add_default_event_bus(self): + self.event_buses["default"] = EventBus(self.region_name, "default") + def _get_rule_by_index(self, i): return self.rules.get(self.rules_order[i]) @@ -221,9 +264,17 @@ class EventsBackend(BaseBackend): def test_event_pattern(self): raise NotImplementedError() - def put_permission(self, action, principal, statement_id): + def put_permission(self, event_bus_name, action, principal, statement_id): + if not event_bus_name: + event_bus_name = "default" + + event_bus = self.describe_event_bus(event_bus_name) + if action is None or action != "events:PutEvents": - raise JsonRESTError("InvalidParameterValue", "Action must be PutEvents") + raise JsonRESTError( + "ValidationException", + "Provided value in parameter 'action' is not supported.", + ) if principal is None or self.ACCOUNT_ID.match(principal) is None: raise JsonRESTError( @@ -235,34 +286,81 @@ class EventsBackend(BaseBackend): "InvalidParameterValue", "StatementId must match ^[a-zA-Z0-9-_]{1,64}$" ) - self.permissions[statement_id] = {"action": action, "principal": principal} + event_bus._permissions[statement_id] = { + "Action": action, + "Principal": principal, + } - def remove_permission(self, statement_id): - try: - del self.permissions[statement_id] - except KeyError: - raise JsonRESTError("ResourceNotFoundException", "StatementId not found") + def remove_permission(self, event_bus_name, statement_id): + if not event_bus_name: + event_bus_name = "default" - def describe_event_bus(self): - arn = "arn:aws:events:{0}:000000000000:event-bus/default".format( - self.region_name - ) - statements = [] - for statement_id, data in self.permissions.items(): - statements.append( - { - "Sid": statement_id, - "Effect": "Allow", - "Principal": { - "AWS": "arn:aws:iam::{0}:root".format(data["principal"]) - }, - "Action": data["action"], - "Resource": arn, - } + event_bus = self.describe_event_bus(event_bus_name) + + if not len(event_bus._permissions): + raise JsonRESTError( + "ResourceNotFoundException", "EventBus does not have a policy." ) - policy = {"Version": "2012-10-17", "Statement": statements} - policy_json = json.dumps(policy) - return {"Policy": policy_json, "Name": "default", "Arn": arn} + + if not event_bus._permissions.pop(statement_id, None): + raise JsonRESTError( + "ResourceNotFoundException", + "Statement with the provided id does not exist.", + ) + + def describe_event_bus(self, name): + if not name: + name = "default" + + event_bus = self.event_buses.get(name) + + if not event_bus: + raise JsonRESTError( + "ResourceNotFoundException", + "Event bus {} does not exist.".format(name), + ) + + return event_bus + + def create_event_bus(self, name, event_source_name): + if name in self.event_buses: + raise JsonRESTError( + "ResourceAlreadyExistsException", + "Event bus {} already exists.".format(name), + ) + + if not event_source_name and "/" in name: + raise JsonRESTError( + "ValidationException", "Event bus name must not contain '/'." + ) + + if event_source_name and event_source_name not in self.event_sources: + raise JsonRESTError( + "ResourceNotFoundException", + "Event source {} does not exist.".format(event_source_name), + ) + + self.event_buses[name] = EventBus(self.region_name, name) + + return self.event_buses[name] + + def list_event_buses(self, name_prefix): + if name_prefix: + return [ + event_bus + for event_bus in self.event_buses.values() + if event_bus.name.startswith(name_prefix) + ] + + return list(self.event_buses.values()) + + def delete_event_bus(self, name): + if name == "default": + raise JsonRESTError( + "ValidationException", "Cannot delete event bus default." + ) + + self.event_buses.pop(name, None) available_regions = boto3.session.Session().get_available_regions("events") diff --git a/moto/events/responses.py b/moto/events/responses.py index 39c5c75dc..98a33218a 100644 --- a/moto/events/responses.py +++ b/moto/events/responses.py @@ -238,20 +238,68 @@ class EventsHandler(BaseResponse): pass def put_permission(self): + event_bus_name = self._get_param("EventBusName") action = self._get_param("Action") principal = self._get_param("Principal") statement_id = self._get_param("StatementId") - self.events_backend.put_permission(action, principal, statement_id) + self.events_backend.put_permission( + event_bus_name, action, principal, statement_id + ) return "" def remove_permission(self): + event_bus_name = self._get_param("EventBusName") statement_id = self._get_param("StatementId") - self.events_backend.remove_permission(statement_id) + self.events_backend.remove_permission(event_bus_name, statement_id) return "" def describe_event_bus(self): - return json.dumps(self.events_backend.describe_event_bus()) + name = self._get_param("Name") + + event_bus = self.events_backend.describe_event_bus(name) + response = { + "Name": event_bus.name, + "Arn": event_bus.arn, + } + + if event_bus.policy: + response["Policy"] = event_bus.policy + + return json.dumps(response), self.response_headers + + def create_event_bus(self): + name = self._get_param("Name") + event_source_name = self._get_param("EventSourceName") + + event_bus = self.events_backend.create_event_bus(name, event_source_name) + + return json.dumps({"EventBusArn": event_bus.arn}), self.response_headers + + def list_event_buses(self): + name_prefix = self._get_param("NamePrefix") + # ToDo: add 'NextToken' & 'Limit' parameters + + response = [] + for event_bus in self.events_backend.list_event_buses(name_prefix): + event_bus_response = { + "Name": event_bus.name, + "Arn": event_bus.arn, + } + + if event_bus.policy: + event_bus_response["Policy"] = event_bus.policy + + response.append(event_bus_response) + + return json.dumps({"EventBuses": response}), self.response_headers + + def delete_event_bus(self): + name = self._get_param("Name") + + self.events_backend.delete_event_bus(name) + + return "", self.response_headers diff --git a/moto/iam/models.py b/moto/iam/models.py index 1110da682..b64c9402f 100644 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -818,6 +818,12 @@ class IAMBackend(BaseBackend): policy = ManagedPolicy( policy_name, description=description, document=policy_document, path=path ) + if policy.arn in self.managed_policies: + raise EntityAlreadyExists( + "A policy called {} already exists. Duplicate names are not allowed.".format( + policy_name + ) + ) self.managed_policies[policy.arn] = policy return policy @@ -1227,6 +1233,14 @@ class IAMBackend(BaseBackend): group = self.get_group(group_name) return group.get_policy(policy_name) + def delete_group(self, group_name): + try: + del self.groups[group_name] + except KeyError: + raise IAMNotFoundException( + "The group with name {0} cannot be found.".format(group_name) + ) + def create_user(self, user_name, path="/"): if user_name in self.users: raise IAMConflictException( diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 08fe13dc5..4bd1aa80c 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -428,6 +428,12 @@ class IamResponse(BaseResponse): template = self.response_template(GET_GROUP_POLICY_TEMPLATE) return template.render(name="GetGroupPolicyResponse", **policy_result) + def delete_group(self): + group_name = self._get_param("GroupName") + iam_backend.delete_group(group_name) + template = self.response_template(GENERIC_EMPTY_TEMPLATE) + return template.render(name="DeleteGroup") + def create_user(self): user_name = self._get_param("UserName") path = self._get_param("Path") diff --git a/moto/organizations/models.py b/moto/organizations/models.py index 37f8bdeb9..d558616d2 100644 --- a/moto/organizations/models.py +++ b/moto/organizations/models.py @@ -269,10 +269,32 @@ class OrganizationsBackend(BaseBackend): ) return account + def get_account_by_attr(self, attr, value): + account = next( + ( + account + for account in self.accounts + if hasattr(account, attr) and getattr(account, attr) == value + ), + None, + ) + if account is None: + raise RESTError( + "AccountNotFoundException", + "You specified an account that doesn't exist.", + ) + return account + def describe_account(self, **kwargs): account = self.get_account_by_id(kwargs["AccountId"]) return account.describe() + def describe_create_account_status(self, **kwargs): + account = self.get_account_by_attr( + "create_account_status_id", kwargs["CreateAccountRequestId"] + ) + return account.create_account_status + def list_accounts(self): return dict( Accounts=[account.describe()["Account"] for account in self.accounts] diff --git a/moto/organizations/responses.py b/moto/organizations/responses.py index 673bf5adb..f9e0b2e04 100644 --- a/moto/organizations/responses.py +++ b/moto/organizations/responses.py @@ -65,6 +65,13 @@ class OrganizationsResponse(BaseResponse): self.organizations_backend.describe_account(**self.request_params) ) + def describe_create_account_status(self): + return json.dumps( + self.organizations_backend.describe_create_account_status( + **self.request_params + ) + ) + def list_accounts(self): return json.dumps(self.organizations_backend.list_accounts()) diff --git a/moto/secretsmanager/exceptions.py b/moto/secretsmanager/exceptions.py index 13f1f2766..bf717e20c 100644 --- a/moto/secretsmanager/exceptions.py +++ b/moto/secretsmanager/exceptions.py @@ -14,23 +14,21 @@ class ResourceNotFoundException(SecretsManagerClientError): ) -# Using specialised exception due to the use of a non-ASCII character class SecretNotFoundException(SecretsManagerClientError): def __init__(self): self.code = 404 super(SecretNotFoundException, self).__init__( "ResourceNotFoundException", - message="Secrets Manager can\u2019t find the specified secret.", + message="Secrets Manager can't find the specified secret.", ) -# Using specialised exception due to the use of a non-ASCII character class SecretHasNoValueException(SecretsManagerClientError): def __init__(self, version_stage): self.code = 404 super(SecretHasNoValueException, self).__init__( "ResourceNotFoundException", - message="Secrets Manager can\u2019t find the specified secret " + message="Secrets Manager can't find the specified secret " "value for staging label: {}".format(version_stage), ) diff --git a/scripts/implementation_coverage.py b/scripts/implementation_coverage.py index b3855e0b2..4552ec18e 100755 --- a/scripts/implementation_coverage.py +++ b/scripts/implementation_coverage.py @@ -7,16 +7,18 @@ import boto3 script_dir = os.path.dirname(os.path.abspath(__file__)) +alternative_service_names = {'lambda': 'awslambda'} def get_moto_implementation(service_name): - service_name_standardized = service_name.replace("-", "") if "-" in service_name else service_name - if not hasattr(moto, service_name_standardized): + service_name = service_name.replace("-", "") if "-" in service_name else service_name + alt_service_name = alternative_service_names[service_name] if service_name in alternative_service_names else service_name + if not hasattr(moto, alt_service_name): return None - module = getattr(moto, service_name_standardized) + module = getattr(moto, alt_service_name) if module is None: return None - mock = getattr(module, "mock_{}".format(service_name_standardized)) + mock = getattr(module, "mock_{}".format(service_name)) if mock is None: return None backends = list(mock().backends.values()) diff --git a/tests/test_core/test_auth.py b/tests/test_core/test_auth.py index 7dc632188..c5e043ae5 100644 --- a/tests/test_core/test_auth.py +++ b/tests/test_core/test_auth.py @@ -402,10 +402,10 @@ def test_s3_access_denied_with_denying_attached_group_policy(): "Statement": [{"Effect": "Deny", "Action": "s3:List*", "Resource": "*"}], } access_key = create_user_with_access_key_and_attached_policy( - user_name, attached_policy_document + user_name, attached_policy_document, policy_name="policy1" ) create_group_with_attached_policy_and_add_user( - user_name, group_attached_policy_document + user_name, group_attached_policy_document, policy_name="policy2" ) client = boto3.client( "s3", @@ -476,10 +476,16 @@ def test_access_denied_with_many_irrelevant_policies(): "Statement": [{"Effect": "Deny", "Action": "lambda:*", "Resource": "*"}], } access_key = create_user_with_access_key_and_multiple_policies( - user_name, inline_policy_document, attached_policy_document + user_name, + inline_policy_document, + attached_policy_document, + attached_policy_name="policy1", ) create_group_with_multiple_policies_and_add_user( - user_name, group_inline_policy_document, group_attached_policy_document + user_name, + group_inline_policy_document, + group_attached_policy_document, + attached_policy_name="policy2", ) client = boto3.client( "ec2", diff --git a/tests/test_datasync/test_datasync.py b/tests/test_datasync/test_datasync.py index 825eb7fba..e3ea87675 100644 --- a/tests/test_datasync/test_datasync.py +++ b/tests/test_datasync/test_datasync.py @@ -127,6 +127,22 @@ def test_list_locations(): assert response["Locations"][2]["LocationUri"] == "s3://my_bucket/dir" +@mock_datasync +def test_delete_location(): + client = boto3.client("datasync", region_name="us-east-1") + locations = create_locations(client, create_smb=True) + response = client.list_locations() + assert len(response["Locations"]) == 1 + location_arn = locations["smb_arn"] + + response = client.delete_location(LocationArn=location_arn) + response = client.list_locations() + assert len(response["Locations"]) == 0 + + with assert_raises(ClientError) as e: + response = client.delete_location(LocationArn=location_arn) + + @mock_datasync def test_create_task(): client = boto3.client("datasync", region_name="us-east-1") @@ -208,6 +224,72 @@ def test_describe_task_not_exist(): client.describe_task(TaskArn="abc") +@mock_datasync +def test_update_task(): + client = boto3.client("datasync", region_name="us-east-1") + locations = create_locations(client, create_s3=True, create_smb=True) + + initial_name = "Initial_Name" + updated_name = "Updated_Name" + initial_options = { + "VerifyMode": "NONE", + "Atime": "BEST_EFFORT", + "Mtime": "PRESERVE", + } + updated_options = { + "VerifyMode": "POINT_IN_TIME_CONSISTENT", + "Atime": "BEST_EFFORT", + "Mtime": "PRESERVE", + } + response = client.create_task( + SourceLocationArn=locations["smb_arn"], + DestinationLocationArn=locations["s3_arn"], + Name=initial_name, + Options=initial_options, + ) + task_arn = response["TaskArn"] + response = client.describe_task(TaskArn=task_arn) + assert response["TaskArn"] == task_arn + assert response["Name"] == initial_name + assert response["Options"] == initial_options + + response = client.update_task( + TaskArn=task_arn, Name=updated_name, Options=updated_options + ) + + response = client.describe_task(TaskArn=task_arn) + assert response["TaskArn"] == task_arn + assert response["Name"] == updated_name + assert response["Options"] == updated_options + + with assert_raises(ClientError) as e: + client.update_task(TaskArn="doesnt_exist") + + +@mock_datasync +def test_delete_task(): + client = boto3.client("datasync", region_name="us-east-1") + locations = create_locations(client, create_s3=True, create_smb=True) + + response = client.create_task( + SourceLocationArn=locations["smb_arn"], + DestinationLocationArn=locations["s3_arn"], + Name="task_name", + ) + + response = client.list_tasks() + assert len(response["Tasks"]) == 1 + task_arn = response["Tasks"][0]["TaskArn"] + assert task_arn is not None + + response = client.delete_task(TaskArn=task_arn) + response = client.list_tasks() + assert len(response["Tasks"]) == 0 + + with assert_raises(ClientError) as e: + response = client.delete_task(TaskArn=task_arn) + + @mock_datasync def test_start_task_execution(): client = boto3.client("datasync", region_name="us-east-1") @@ -261,6 +343,8 @@ def test_describe_task_execution(): Name="task_name", ) task_arn = response["TaskArn"] + response = client.describe_task(TaskArn=task_arn) + assert response["Status"] == "AVAILABLE" response = client.start_task_execution(TaskArn=task_arn) task_execution_arn = response["TaskExecutionArn"] @@ -270,26 +354,38 @@ def test_describe_task_execution(): response = client.describe_task_execution(TaskExecutionArn=task_execution_arn) assert response["TaskExecutionArn"] == task_execution_arn assert response["Status"] == "INITIALIZING" + response = client.describe_task(TaskArn=task_arn) + assert response["Status"] == "RUNNING" response = client.describe_task_execution(TaskExecutionArn=task_execution_arn) assert response["TaskExecutionArn"] == task_execution_arn assert response["Status"] == "PREPARING" + response = client.describe_task(TaskArn=task_arn) + assert response["Status"] == "RUNNING" response = client.describe_task_execution(TaskExecutionArn=task_execution_arn) assert response["TaskExecutionArn"] == task_execution_arn assert response["Status"] == "TRANSFERRING" + response = client.describe_task(TaskArn=task_arn) + assert response["Status"] == "RUNNING" response = client.describe_task_execution(TaskExecutionArn=task_execution_arn) assert response["TaskExecutionArn"] == task_execution_arn assert response["Status"] == "VERIFYING" + response = client.describe_task(TaskArn=task_arn) + assert response["Status"] == "RUNNING" response = client.describe_task_execution(TaskExecutionArn=task_execution_arn) assert response["TaskExecutionArn"] == task_execution_arn assert response["Status"] == "SUCCESS" + response = client.describe_task(TaskArn=task_arn) + assert response["Status"] == "AVAILABLE" response = client.describe_task_execution(TaskExecutionArn=task_execution_arn) assert response["TaskExecutionArn"] == task_execution_arn assert response["Status"] == "SUCCESS" + response = client.describe_task(TaskArn=task_arn) + assert response["Status"] == "AVAILABLE" @mock_datasync @@ -317,11 +413,13 @@ def test_cancel_task_execution(): response = client.describe_task(TaskArn=task_arn) assert response["CurrentTaskExecutionArn"] == task_execution_arn + assert response["Status"] == "RUNNING" response = client.cancel_task_execution(TaskExecutionArn=task_execution_arn) response = client.describe_task(TaskArn=task_arn) assert "CurrentTaskExecutionArn" not in response + assert response["Status"] == "AVAILABLE" response = client.describe_task_execution(TaskExecutionArn=task_execution_arn) assert response["Status"] == "ERROR" diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index d492b0135..7f6963870 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -3319,3 +3319,66 @@ def _create_user_table(): TableName="users", Item={"username": {"S": "user3"}, "foo": {"S": "bar"}} ) return client + + +@mock_dynamodb2 +def test_update_item_if_original_value_is_none(): + dynamo = boto3.resource("dynamodb", region_name="eu-central-1") + dynamo.create_table( + AttributeDefinitions=[{"AttributeName": "job_id", "AttributeType": "S"}], + TableName="origin-rbu-dev", + KeySchema=[{"AttributeName": "job_id", "KeyType": "HASH"}], + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, + ) + table = dynamo.Table("origin-rbu-dev") + table.put_item(Item={"job_id": "a", "job_name": None}) + table.update_item( + Key={"job_id": "a"}, + UpdateExpression="SET job_name = :output", + ExpressionAttributeValues={":output": "updated",}, + ) + table.scan()["Items"][0]["job_name"].should.equal("updated") + + +@mock_dynamodb2 +def test_update_nested_item_if_original_value_is_none(): + dynamo = boto3.resource("dynamodb", region_name="eu-central-1") + dynamo.create_table( + AttributeDefinitions=[{"AttributeName": "job_id", "AttributeType": "S"}], + TableName="origin-rbu-dev", + KeySchema=[{"AttributeName": "job_id", "KeyType": "HASH"}], + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, + ) + table = dynamo.Table("origin-rbu-dev") + table.put_item(Item={"job_id": "a", "job_details": {"job_name": None}}) + table.update_item( + Key={"job_id": "a"}, + UpdateExpression="SET job_details.job_name = :output", + ExpressionAttributeValues={":output": "updated",}, + ) + table.scan()["Items"][0]["job_details"]["job_name"].should.equal("updated") + + +@mock_dynamodb2 +def test_allow_update_to_item_with_different_type(): + dynamo = boto3.resource("dynamodb", region_name="eu-central-1") + dynamo.create_table( + AttributeDefinitions=[{"AttributeName": "job_id", "AttributeType": "S"}], + TableName="origin-rbu-dev", + KeySchema=[{"AttributeName": "job_id", "KeyType": "HASH"}], + ProvisionedThroughput={"ReadCapacityUnits": 1, "WriteCapacityUnits": 1}, + ) + table = dynamo.Table("origin-rbu-dev") + table.put_item(Item={"job_id": "a", "job_details": {"job_name": {"nested": "yes"}}}) + table.put_item(Item={"job_id": "b", "job_details": {"job_name": {"nested": "yes"}}}) + table.update_item( + Key={"job_id": "a"}, + UpdateExpression="SET job_details.job_name = :output", + ExpressionAttributeValues={":output": "updated"}, + ) + table.get_item(Key={"job_id": "a"})["Item"]["job_details"][ + "job_name" + ].should.be.equal("updated") + table.get_item(Key={"job_id": "b"})["Item"]["job_details"][ + "job_name" + ].should.be.equal({"nested": "yes"}) diff --git a/tests/test_ec2/test_subnets.py b/tests/test_ec2/test_subnets.py index f5f1af433..7bb57aab4 100644 --- a/tests/test_ec2/test_subnets.py +++ b/tests/test_ec2/test_subnets.py @@ -11,6 +11,7 @@ from boto.exception import EC2ResponseError from botocore.exceptions import ParamValidationError, ClientError import json import sure # noqa +import random from moto import mock_cloudformation_deprecated, mock_ec2, mock_ec2_deprecated @@ -474,3 +475,127 @@ def test_create_subnets_with_overlapping_cidr_blocks(): subnet_cidr_block ) ) + + +@mock_ec2 +def test_available_ip_addresses_in_subnet(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + client = boto3.client("ec2", region_name="us-west-1") + + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + cidr_range_addresses = [ + ("10.0.0.0/16", 65531), + ("10.0.0.0/17", 32763), + ("10.0.0.0/18", 16379), + ("10.0.0.0/19", 8187), + ("10.0.0.0/20", 4091), + ("10.0.0.0/21", 2043), + ("10.0.0.0/22", 1019), + ("10.0.0.0/23", 507), + ("10.0.0.0/24", 251), + ("10.0.0.0/25", 123), + ("10.0.0.0/26", 59), + ("10.0.0.0/27", 27), + ("10.0.0.0/28", 11), + ] + for (cidr, expected_count) in cidr_range_addresses: + validate_subnet_details(client, vpc, cidr, expected_count) + + +@mock_ec2 +def test_available_ip_addresses_in_subnet_with_enis(): + ec2 = boto3.resource("ec2", region_name="us-west-1") + client = boto3.client("ec2", region_name="us-west-1") + + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + # Verify behaviour for various CIDR ranges (...) + # Don't try to assign ENIs to /27 and /28, as there are not a lot of IP addresses to go around + cidr_range_addresses = [ + ("10.0.0.0/16", 65531), + ("10.0.0.0/17", 32763), + ("10.0.0.0/18", 16379), + ("10.0.0.0/19", 8187), + ("10.0.0.0/20", 4091), + ("10.0.0.0/21", 2043), + ("10.0.0.0/22", 1019), + ("10.0.0.0/23", 507), + ("10.0.0.0/24", 251), + ("10.0.0.0/25", 123), + ("10.0.0.0/26", 59), + ] + for (cidr, expected_count) in cidr_range_addresses: + validate_subnet_details_after_creating_eni(client, vpc, cidr, expected_count) + + +def validate_subnet_details(client, vpc, cidr, expected_ip_address_count): + subnet = client.create_subnet( + VpcId=vpc.id, CidrBlock=cidr, AvailabilityZone="us-west-1b" + )["Subnet"] + subnet["AvailableIpAddressCount"].should.equal(expected_ip_address_count) + client.delete_subnet(SubnetId=subnet["SubnetId"]) + + +def validate_subnet_details_after_creating_eni( + client, vpc, cidr, expected_ip_address_count +): + subnet = client.create_subnet( + VpcId=vpc.id, CidrBlock=cidr, AvailabilityZone="us-west-1b" + )["Subnet"] + # Create a random number of Elastic Network Interfaces + nr_of_eni_to_create = random.randint(0, 5) + ip_addresses_assigned = 0 + enis_created = [] + for i in range(0, nr_of_eni_to_create): + # Create a random number of IP addresses per ENI + nr_of_ip_addresses = random.randint(1, 5) + if nr_of_ip_addresses == 1: + # Pick the first available IP address (First 4 are reserved by AWS) + private_address = "10.0.0." + str(ip_addresses_assigned + 4) + eni = client.create_network_interface( + SubnetId=subnet["SubnetId"], PrivateIpAddress=private_address + )["NetworkInterface"] + enis_created.append(eni) + ip_addresses_assigned = ip_addresses_assigned + 1 + else: + # Assign a list of IP addresses + private_addresses = [ + "10.0.0." + str(4 + ip_addresses_assigned + i) + for i in range(0, nr_of_ip_addresses) + ] + eni = client.create_network_interface( + SubnetId=subnet["SubnetId"], + PrivateIpAddresses=[ + {"PrivateIpAddress": address} for address in private_addresses + ], + )["NetworkInterface"] + enis_created.append(eni) + ip_addresses_assigned = ip_addresses_assigned + nr_of_ip_addresses + 1 # + # Verify that the nr of available IP addresses takes these ENIs into account + updated_subnet = client.describe_subnets(SubnetIds=[subnet["SubnetId"]])["Subnets"][ + 0 + ] + private_addresses = [ + eni["PrivateIpAddress"] for eni in enis_created if eni["PrivateIpAddress"] + ] + for eni in enis_created: + private_addresses.extend( + [address["PrivateIpAddress"] for address in eni["PrivateIpAddresses"]] + ) + error_msg = ( + "Nr of IP addresses for Subnet with CIDR {0} is incorrect. Expected: {1}, Actual: {2}. " + "Addresses: {3}" + ) + with sure.ensure( + error_msg, + cidr, + str(expected_ip_address_count), + updated_subnet["AvailableIpAddressCount"], + str(private_addresses), + ): + updated_subnet["AvailableIpAddressCount"].should.equal( + expected_ip_address_count - ip_addresses_assigned + ) + # Clean up, as we have to create a few more subnets that shouldn't interfere with each other + for eni in enis_created: + client.delete_network_interface(NetworkInterfaceId=eni["NetworkInterfaceId"]) + client.delete_subnet(SubnetId=subnet["SubnetId"]) diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index d5bfdf782..5f81e2cf6 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -1,6 +1,7 @@ import random import boto3 import json +import sure # noqa from moto.events import mock_events from botocore.exceptions import ClientError @@ -204,6 +205,53 @@ def test_permissions(): assert resp_policy["Statement"][0]["Sid"] == "Account1" +@mock_events +def test_put_permission_errors(): + client = boto3.client("events", "us-east-1") + client.create_event_bus(Name="test-bus") + + client.put_permission.when.called_with( + EventBusName="non-existing", + Action="events:PutEvents", + Principal="111111111111", + StatementId="test", + ).should.throw(ClientError, "Event bus non-existing does not exist.") + + client.put_permission.when.called_with( + EventBusName="test-bus", + Action="events:PutPermission", + Principal="111111111111", + StatementId="test", + ).should.throw( + ClientError, "Provided value in parameter 'action' is not supported." + ) + + +@mock_events +def test_remove_permission_errors(): + client = boto3.client("events", "us-east-1") + client.create_event_bus(Name="test-bus") + + client.remove_permission.when.called_with( + EventBusName="non-existing", StatementId="test" + ).should.throw(ClientError, "Event bus non-existing does not exist.") + + client.remove_permission.when.called_with( + EventBusName="test-bus", StatementId="test" + ).should.throw(ClientError, "EventBus does not have a policy.") + + client.put_permission( + EventBusName="test-bus", + Action="events:PutEvents", + Principal="111111111111", + StatementId="test", + ) + + client.remove_permission.when.called_with( + EventBusName="test-bus", StatementId="non-existing" + ).should.throw(ClientError, "Statement with the provided id does not exist.") + + @mock_events def test_put_events(): client = boto3.client("events", "eu-central-1") @@ -220,3 +268,177 @@ def test_put_events(): with assert_raises(ClientError): client.put_events(Entries=[event] * 20) + + +@mock_events +def test_create_event_bus(): + client = boto3.client("events", "us-east-1") + response = client.create_event_bus(Name="test-bus") + + response["EventBusArn"].should.equal( + "arn:aws:events:us-east-1:123456789012:event-bus/test-bus" + ) + + +@mock_events +def test_create_event_bus_errors(): + client = boto3.client("events", "us-east-1") + client.create_event_bus(Name="test-bus") + + client.create_event_bus.when.called_with(Name="test-bus").should.throw( + ClientError, "Event bus test-bus already exists." + ) + + # the 'default' name is already used for the account's default event bus. + client.create_event_bus.when.called_with(Name="default").should.throw( + ClientError, "Event bus default already exists." + ) + + # non partner event buses can't contain the '/' character + client.create_event_bus.when.called_with(Name="test/test-bus").should.throw( + ClientError, "Event bus name must not contain '/'." + ) + + client.create_event_bus.when.called_with( + Name="aws.partner/test/test-bus", EventSourceName="aws.partner/test/test-bus" + ).should.throw( + ClientError, "Event source aws.partner/test/test-bus does not exist." + ) + + +@mock_events +def test_describe_event_bus(): + client = boto3.client("events", "us-east-1") + + response = client.describe_event_bus() + + response["Name"].should.equal("default") + response["Arn"].should.equal( + "arn:aws:events:us-east-1:123456789012:event-bus/default" + ) + response.should_not.have.key("Policy") + + client.create_event_bus(Name="test-bus") + client.put_permission( + EventBusName="test-bus", + Action="events:PutEvents", + Principal="111111111111", + StatementId="test", + ) + + response = client.describe_event_bus(Name="test-bus") + + response["Name"].should.equal("test-bus") + response["Arn"].should.equal( + "arn:aws:events:us-east-1:123456789012:event-bus/test-bus" + ) + json.loads(response["Policy"]).should.equal( + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "test", + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::111111111111:root"}, + "Action": "events:PutEvents", + "Resource": "arn:aws:events:us-east-1:123456789012:event-bus/test-bus", + } + ], + } + ) + + +@mock_events +def test_describe_event_bus_errors(): + client = boto3.client("events", "us-east-1") + + client.describe_event_bus.when.called_with(Name="non-existing").should.throw( + ClientError, "Event bus non-existing does not exist." + ) + + +@mock_events +def test_list_event_buses(): + client = boto3.client("events", "us-east-1") + client.create_event_bus(Name="test-bus-1") + client.create_event_bus(Name="test-bus-2") + client.create_event_bus(Name="other-bus-1") + client.create_event_bus(Name="other-bus-2") + + response = client.list_event_buses() + + response["EventBuses"].should.have.length_of(5) + sorted(response["EventBuses"], key=lambda i: i["Name"]).should.equal( + [ + { + "Name": "default", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/default", + }, + { + "Name": "other-bus-1", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/other-bus-1", + }, + { + "Name": "other-bus-2", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/other-bus-2", + }, + { + "Name": "test-bus-1", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/test-bus-1", + }, + { + "Name": "test-bus-2", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/test-bus-2", + }, + ] + ) + + response = client.list_event_buses(NamePrefix="other-bus") + + response["EventBuses"].should.have.length_of(2) + sorted(response["EventBuses"], key=lambda i: i["Name"]).should.equal( + [ + { + "Name": "other-bus-1", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/other-bus-1", + }, + { + "Name": "other-bus-2", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/other-bus-2", + }, + ] + ) + + +@mock_events +def test_delete_event_bus(): + client = boto3.client("events", "us-east-1") + client.create_event_bus(Name="test-bus") + + response = client.list_event_buses() + response["EventBuses"].should.have.length_of(2) + + client.delete_event_bus(Name="test-bus") + + response = client.list_event_buses() + response["EventBuses"].should.have.length_of(1) + response["EventBuses"].should.equal( + [ + { + "Name": "default", + "Arn": "arn:aws:events:us-east-1:123456789012:event-bus/default", + } + ] + ) + + # deleting non existing event bus should be successful + client.delete_event_bus(Name="non-existing") + + +@mock_events +def test_delete_event_bus_errors(): + client = boto3.client("events", "us-east-1") + + client.delete_event_bus.when.called_with(Name="default").should.throw( + ClientError, "Cannot delete event bus default." + ) diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 8eea201d0..c5e856b68 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -408,6 +408,21 @@ def test_create_policy(): ) +@mock_iam +def test_create_policy_already_exists(): + conn = boto3.client("iam", region_name="us-east-1") + response = conn.create_policy( + PolicyName="TestCreatePolicy", PolicyDocument=MOCK_POLICY + ) + with assert_raises(conn.exceptions.EntityAlreadyExistsException) as ex: + response = conn.create_policy( + PolicyName="TestCreatePolicy", PolicyDocument=MOCK_POLICY + ) + ex.exception.response["Error"]["Code"].should.equal("EntityAlreadyExists") + ex.exception.response["ResponseMetadata"]["HTTPStatusCode"].should.equal(409) + ex.exception.response["Error"]["Message"].should.contain("TestCreatePolicy") + + @mock_iam def test_delete_policy(): conn = boto3.client("iam", region_name="us-east-1") diff --git a/tests/test_iam/test_iam_groups.py b/tests/test_iam/test_iam_groups.py index 7fd299281..7b73e89ea 100644 --- a/tests/test_iam/test_iam_groups.py +++ b/tests/test_iam/test_iam_groups.py @@ -8,6 +8,7 @@ import sure # noqa from nose.tools import assert_raises from boto.exception import BotoServerError +from botocore.exceptions import ClientError from moto import mock_iam, mock_iam_deprecated MOCK_POLICY = """ @@ -182,3 +183,25 @@ def test_list_group_policies(): conn.list_group_policies(GroupName="my-group")["PolicyNames"].should.equal( ["my-policy"] ) + + +@mock_iam +def test_delete_group(): + conn = boto3.client("iam", region_name="us-east-1") + conn.create_group(GroupName="my-group") + groups = conn.list_groups() + assert groups["Groups"][0]["GroupName"] == "my-group" + assert len(groups["Groups"]) == 1 + conn.delete_group(GroupName="my-group") + conn.list_groups()["Groups"].should.be.empty + + +@mock_iam +def test_delete_unknown_group(): + conn = boto3.client("iam", region_name="us-east-1") + with assert_raises(ClientError) as err: + conn.delete_group(GroupName="unknown-group") + err.exception.response["Error"]["Code"].should.equal("NoSuchEntity") + err.exception.response["Error"]["Message"].should.equal( + "The group with name unknown-group cannot be found." + ) diff --git a/tests/test_organizations/test_organizations_boto3.py b/tests/test_organizations/test_organizations_boto3.py index f8eb1328e..3b4a51557 100644 --- a/tests/test_organizations/test_organizations_boto3.py +++ b/tests/test_organizations/test_organizations_boto3.py @@ -159,6 +159,17 @@ def test_create_account(): create_status["AccountName"].should.equal(mockname) +@mock_organizations +def test_describe_create_account_status(): + client = boto3.client("organizations", region_name="us-east-1") + client.create_organization(FeatureSet="ALL")["Organization"] + request_id = client.create_account(AccountName=mockname, Email=mockemail)[ + "CreateAccountStatus" + ]["Id"] + response = client.describe_create_account_status(CreateAccountRequestId=request_id) + validate_create_account_status(response["CreateAccountStatus"]) + + @mock_organizations def test_describe_account(): client = boto3.client("organizations", region_name="us-east-1") diff --git a/tests/test_secretsmanager/test_secretsmanager.py b/tests/test_secretsmanager/test_secretsmanager.py index bf688ec12..a7c7a6862 100644 --- a/tests/test_secretsmanager/test_secretsmanager.py +++ b/tests/test_secretsmanager/test_secretsmanager.py @@ -45,7 +45,7 @@ def test_get_secret_that_does_not_exist(): result = conn.get_secret_value(SecretId="i-dont-exist") assert_equal( - "Secrets Manager can\u2019t find the specified secret.", + "Secrets Manager can't find the specified secret.", cm.exception.response["Error"]["Message"], ) @@ -61,7 +61,7 @@ def test_get_secret_that_does_not_match(): result = conn.get_secret_value(SecretId="i-dont-match") assert_equal( - "Secrets Manager can\u2019t find the specified secret.", + "Secrets Manager can't find the specified secret.", cm.exception.response["Error"]["Message"], ) @@ -88,7 +88,7 @@ def test_get_secret_that_has_no_value(): result = conn.get_secret_value(SecretId="java-util-test-password") assert_equal( - "Secrets Manager can\u2019t find the specified secret value for staging label: AWSCURRENT", + "Secrets Manager can't find the specified secret value for staging label: AWSCURRENT", cm.exception.response["Error"]["Message"], ) diff --git a/tests/test_secretsmanager/test_server.py b/tests/test_secretsmanager/test_server.py index 89cb90185..9501c7c7c 100644 --- a/tests/test_secretsmanager/test_server.py +++ b/tests/test_secretsmanager/test_server.py @@ -48,9 +48,7 @@ def test_get_secret_that_does_not_exist(): headers={"X-Amz-Target": "secretsmanager.GetSecretValue"}, ) json_data = json.loads(get_secret.data.decode("utf-8")) - assert ( - json_data["message"] == "Secrets Manager can\u2019t find the specified secret." - ) + assert json_data["message"] == "Secrets Manager can't find the specified secret." assert json_data["__type"] == "ResourceNotFoundException" @@ -70,9 +68,7 @@ def test_get_secret_that_does_not_match(): headers={"X-Amz-Target": "secretsmanager.GetSecretValue"}, ) json_data = json.loads(get_secret.data.decode("utf-8")) - assert ( - json_data["message"] == "Secrets Manager can\u2019t find the specified secret." - ) + assert json_data["message"] == "Secrets Manager can't find the specified secret." assert json_data["__type"] == "ResourceNotFoundException" @@ -95,7 +91,7 @@ def test_get_secret_that_has_no_value(): json_data = json.loads(get_secret.data.decode("utf-8")) assert ( json_data["message"] - == "Secrets Manager can\u2019t find the specified secret value for staging label: AWSCURRENT" + == "Secrets Manager can't find the specified secret value for staging label: AWSCURRENT" ) assert json_data["__type"] == "ResourceNotFoundException" @@ -178,9 +174,7 @@ def test_describe_secret_that_does_not_exist(): ) json_data = json.loads(describe_secret.data.decode("utf-8")) - assert ( - json_data["message"] == "Secrets Manager can\u2019t find the specified secret." - ) + assert json_data["message"] == "Secrets Manager can't find the specified secret." assert json_data["__type"] == "ResourceNotFoundException" @@ -202,9 +196,7 @@ def test_describe_secret_that_does_not_match(): ) json_data = json.loads(describe_secret.data.decode("utf-8")) - assert ( - json_data["message"] == "Secrets Manager can\u2019t find the specified secret." - ) + assert json_data["message"] == "Secrets Manager can't find the specified secret." assert json_data["__type"] == "ResourceNotFoundException" @@ -306,9 +298,7 @@ def test_rotate_secret_that_does_not_exist(): ) json_data = json.loads(rotate_secret.data.decode("utf-8")) - assert ( - json_data["message"] == "Secrets Manager can\u2019t find the specified secret." - ) + assert json_data["message"] == "Secrets Manager can't find the specified secret." assert json_data["__type"] == "ResourceNotFoundException" @@ -330,9 +320,7 @@ def test_rotate_secret_that_does_not_match(): ) json_data = json.loads(rotate_secret.data.decode("utf-8")) - assert ( - json_data["message"] == "Secrets Manager can\u2019t find the specified secret." - ) + assert json_data["message"] == "Secrets Manager can't find the specified secret." assert json_data["__type"] == "ResourceNotFoundException"