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"