diff --git a/moto/ecs/models.py b/moto/ecs/models.py index 33d4dcf72..1a385226b 100644 --- a/moto/ecs/models.py +++ b/moto/ecs/models.py @@ -121,6 +121,7 @@ class TaskDefinition(BaseObject): network_mode=None, volumes=None, tags=None, + placement_constraints=None, ): self.family = family self.revision = revision @@ -137,6 +138,9 @@ class TaskDefinition(BaseObject): self.network_mode = "bridge" else: self.network_mode = network_mode + self.placement_constraints = ( + placement_constraints if placement_constraints is not None else [] + ) @property def response_object(self): @@ -558,7 +562,13 @@ class EC2ContainerServiceBackend(BaseBackend): raise Exception("{0} is not a cluster".format(cluster_name)) def register_task_definition( - self, family, container_definitions, volumes=None, network_mode=None, tags=None + self, + family, + container_definitions, + volumes=None, + network_mode=None, + tags=None, + placement_constraints=None, ): if family in self.task_definitions: last_id = self._get_last_task_definition_revision_id(family) @@ -574,6 +584,7 @@ class EC2ContainerServiceBackend(BaseBackend): volumes=volumes, network_mode=network_mode, tags=tags, + placement_constraints=placement_constraints, ) self.task_definitions[family][revision] = task_definition diff --git a/moto/ecs/responses.py b/moto/ecs/responses.py index 49bf022b4..c8f1e06ce 100644 --- a/moto/ecs/responses.py +++ b/moto/ecs/responses.py @@ -63,12 +63,14 @@ class EC2ContainerServiceResponse(BaseResponse): volumes = self._get_param("volumes") tags = self._get_param("tags") network_mode = self._get_param("networkMode") + placement_constraints = self._get_param("placementConstraints") task_definition = self.ecs_backend.register_task_definition( family, container_definitions, volumes=volumes, network_mode=network_mode, tags=tags, + placement_constraints=placement_constraints, ) return json.dumps({"taskDefinition": task_definition.response_object}) diff --git a/moto/s3/responses.py b/moto/s3/responses.py index e1ab93860..98f28f012 100644 --- a/moto/s3/responses.py +++ b/moto/s3/responses.py @@ -137,8 +137,13 @@ def parse_key_name(pth): def is_delete_keys(request, path, bucket_name): - return path == "/?delete" or ( - path == "/" and getattr(request, "query_string", "") == "delete" + # GOlang sends a request as url/?delete= (treating it as a normal key=value, even if the value is empty) + # Python sends a request as url/?delete (treating it as a flag) + # https://github.com/spulec/moto/issues/2937 + return ( + path == "/?delete" + or path == "/?delete=" + or (path == "/" and getattr(request, "query_string", "") == "delete") ) diff --git a/moto/sqs/models.py b/moto/sqs/models.py index a54d91c43..f88d906b9 100644 --- a/moto/sqs/models.py +++ b/moto/sqs/models.py @@ -230,7 +230,7 @@ class Queue(BaseModel): "FifoQueue": "false", "KmsDataKeyReusePeriodSeconds": 300, # five minutes "KmsMasterKeyId": None, - "MaximumMessageSize": int(64 << 10), + "MaximumMessageSize": int(64 << 12), "MessageRetentionPeriod": 86400 * 4, # four days "Policy": None, "ReceiveMessageWaitTimeSeconds": 0, diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 8acea0799..f5481cc10 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -285,6 +285,9 @@ class SQSResponse(BaseResponse): "MessageAttributes": message_attributes, } + if entries == {}: + raise EmptyBatchRequest() + messages = self.sqs_backend.send_message_batch(queue_name, entries) template = self.response_template(SEND_MESSAGE_BATCH_RESPONSE) diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py index 0f45e0244..470c5f8ff 100644 --- a/tests/test_dynamodb2/test_dynamodb.py +++ b/tests/test_dynamodb2/test_dynamodb.py @@ -4946,3 +4946,86 @@ def test_multiple_updates(): "id": {"S": "1"}, } assert result == expected_result + + +@mock_dynamodb2 +def test_update_item_atomic_counter(): + table = "table_t" + ddb_mock = boto3.client("dynamodb", region_name="eu-west-3") + ddb_mock.create_table( + TableName=table, + KeySchema=[{"AttributeName": "t_id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "t_id", "AttributeType": "S"}], + BillingMode="PAY_PER_REQUEST", + ) + + key = {"t_id": {"S": "item1"}} + + ddb_mock.put_item( + TableName=table, + Item={"t_id": {"S": "item1"}, "n_i": {"N": "5"}, "n_f": {"N": "5.3"}}, + ) + + ddb_mock.update_item( + TableName=table, + Key=key, + UpdateExpression="set n_i = n_i + :inc1, n_f = n_f + :inc2", + ExpressionAttributeValues={":inc1": {"N": "1.2"}, ":inc2": {"N": "0.05"}}, + ) + updated_item = ddb_mock.get_item(TableName=table, Key=key)["Item"] + updated_item["n_i"]["N"].should.equal("6.2") + updated_item["n_f"]["N"].should.equal("5.35") + + +@mock_dynamodb2 +def test_update_item_atomic_counter_return_values(): + table = "table_t" + ddb_mock = boto3.client("dynamodb", region_name="eu-west-3") + ddb_mock.create_table( + TableName=table, + KeySchema=[{"AttributeName": "t_id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "t_id", "AttributeType": "S"}], + BillingMode="PAY_PER_REQUEST", + ) + + key = {"t_id": {"S": "item1"}} + + ddb_mock.put_item(TableName=table, Item={"t_id": {"S": "item1"}, "v": {"N": "5"}}) + + response = ddb_mock.update_item( + TableName=table, + Key=key, + UpdateExpression="set v = v + :inc", + ExpressionAttributeValues={":inc": {"N": "1"}}, + ReturnValues="UPDATED_OLD", + ) + assert ( + "v" in response["Attributes"] + ), "v has been updated, and should be returned here" + response["Attributes"]["v"]["N"].should.equal("5") + + # second update + response = ddb_mock.update_item( + TableName=table, + Key=key, + UpdateExpression="set v = v + :inc", + ExpressionAttributeValues={":inc": {"N": "1"}}, + ReturnValues="UPDATED_OLD", + ) + assert ( + "v" in response["Attributes"] + ), "v has been updated, and should be returned here" + response["Attributes"]["v"]["N"].should.equal("6") + + # third update + response = ddb_mock.update_item( + TableName=table, + Key=key, + UpdateExpression="set v = v + :inc", + ExpressionAttributeValues={":inc": {"N": "1"}}, + ReturnValues="UPDATED_NEW", + ) + assert ( + "v" in response["Attributes"] + ), "v has been updated, and should be returned here" + response["Attributes"]["v"]["N"].should.equal("8") diff --git a/tests/test_ecs/test_ecs_boto3.py b/tests/test_ecs/test_ecs_boto3.py index 7fd90b412..f6de59597 100644 --- a/tests/test_ecs/test_ecs_boto3.py +++ b/tests/test_ecs/test_ecs_boto3.py @@ -2604,3 +2604,36 @@ def test_ecs_service_untag_resource_multiple_tags(): resourceArn=response["service"]["serviceArn"] ) response["tags"].should.equal([{"key": "hello", "value": "world"}]) + + +@mock_ecs +def test_ecs_task_definition_placement_constraints(): + client = boto3.client("ecs", region_name="us-east-1") + response = client.register_task_definition( + family="test_ecs_task", + containerDefinitions=[ + { + "name": "hello_world", + "image": "docker/hello-world:latest", + "cpu": 1024, + "memory": 400, + "essential": True, + "environment": [ + {"name": "AWS_ACCESS_KEY_ID", "value": "SOME_ACCESS_KEY"} + ], + "logConfiguration": {"logDriver": "json-file"}, + } + ], + networkMode="bridge", + tags=[ + {"key": "createdBy", "value": "moto-unittest"}, + {"key": "foo", "value": "bar"}, + ], + placementConstraints=[ + {"type": "memberOf", "expression": "attribute:ecs.instance-type =~ t2.*"} + ], + ) + type(response["taskDefinition"]["placementConstraints"]).should.be(list) + response["taskDefinition"]["placementConstraints"].should.equal( + [{"type": "memberOf", "expression": "attribute:ecs.instance-type =~ t2.*"}] + ) diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 86b892315..f60e0293e 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -3765,7 +3765,7 @@ def test_paths_with_leading_slashes_work(): @mock_s3 def test_root_dir_with_empty_name_works(): - if os.environ.get("TEST_SERVER_MODE", "false").lower() == "true": + if settings.TEST_SERVER_MODE: raise SkipTest("Does not work in server mode due to error in Workzeug") store_and_read_back_a_key("/") diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index f2ab8c37c..01e34de0b 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -384,7 +384,7 @@ def test_get_queue_attributes(): response["Attributes"]["CreatedTimestamp"].should.be.a(six.string_types) response["Attributes"]["DelaySeconds"].should.equal("0") response["Attributes"]["LastModifiedTimestamp"].should.be.a(six.string_types) - response["Attributes"]["MaximumMessageSize"].should.equal("65536") + response["Attributes"]["MaximumMessageSize"].should.equal("262144") response["Attributes"]["MessageRetentionPeriod"].should.equal("345600") response["Attributes"]["QueueArn"].should.equal( "arn:aws:sqs:us-east-1:{}:test-queue".format(ACCOUNT_ID) @@ -406,7 +406,7 @@ def test_get_queue_attributes(): response["Attributes"].should.equal( { "ApproximateNumberOfMessages": "0", - "MaximumMessageSize": "65536", + "MaximumMessageSize": "262144", "QueueArn": "arn:aws:sqs:us-east-1:{}:test-queue".format(ACCOUNT_ID), "VisibilityTimeout": "30", "RedrivePolicy": json.dumps( @@ -1147,6 +1147,21 @@ def test_send_message_batch_errors(): ) +@mock_sqs +def test_send_message_batch_with_empty_list(): + client = boto3.client("sqs", region_name="us-east-1") + + response = client.create_queue(QueueName="test-queue") + queue_url = response["QueueUrl"] + + client.send_message_batch.when.called_with( + QueueUrl=queue_url, Entries=[] + ).should.throw( + ClientError, + "There should be at least one SendMessageBatchRequestEntry in the request.", + ) + + @mock_sqs def test_batch_change_message_visibility(): if os.environ.get("TEST_SERVER_MODE", "false").lower() == "true":