Merge pull request #53 from spulec/master

Merge upstream
This commit is contained in:
Bert Blommers 2020-07-31 10:20:06 +01:00 committed by GitHub
commit 368fe4cbe6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 610 additions and 44 deletions

View File

@ -317,6 +317,12 @@ def generate_resource_name(resource_type, stack_name, logical_id):
if truncated_name_prefix.endswith("-"):
truncated_name_prefix = truncated_name_prefix[:-1]
return "{0}-{1}".format(truncated_name_prefix, my_random_suffix)
elif resource_type == "AWS::S3::Bucket":
right_hand_part_of_name = "-{0}-{1}".format(logical_id, random_suffix())
max_stack_name_portion_len = 63 - len(right_hand_part_of_name)
return "{0}{1}".format(
stack_name[:max_stack_name_portion_len], right_hand_part_of_name
).lower()
else:
return "{0}-{1}-{2}".format(stack_name, logical_id, random_suffix())

View File

@ -10,6 +10,31 @@ from moto.s3 import s3_backend
from moto.core import ACCOUNT_ID
from .models import cloudformation_backends
from .exceptions import ValidationError
from .utils import yaml_tag_constructor
def get_template_summary_response_from_template(template_body):
def get_resource_types(template_dict):
resources = {}
for key, value in template_dict.items():
if key == "Resources":
resources = value
resource_types = []
for key, value in resources.items():
resource_types.append(value["Type"])
return resource_types
yaml.add_multi_constructor("", yaml_tag_constructor)
try:
template_dict = yaml.load(template_body, Loader=yaml.Loader)
except (yaml.parser.ParserError, yaml.scanner.ScannerError):
template_dict = json.loads(template_body)
resources_types = get_resource_types(template_dict)
template_dict["resourceTypes"] = resources_types
return template_dict
class CloudFormationResponse(BaseResponse):
@ -269,6 +294,20 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(GET_TEMPLATE_RESPONSE_TEMPLATE)
return template.render(stack=stack)
def get_template_summary(self):
stack_name = self._get_param("StackName")
template_url = self._get_param("TemplateURL")
stack_body = self._get_param("TemplateBody")
if stack_name:
stack_body = self.cloudformation_backend.get_stack(stack_name).template
elif template_url:
stack_body = self._get_stack_from_s3_url(template_url)
template_summary = get_template_summary_response_from_template(stack_body)
template = self.response_template(GET_TEMPLATE_SUMMARY_TEMPLATE)
return template.render(template_summary=template_summary)
def update_stack(self):
stack_name = self._get_param("StackName")
role_arn = self._get_param("RoleARN")
@ -743,7 +782,6 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResponse>
</DescribeStacksResult>
</DescribeStacksResponse>"""
DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE = """<DescribeStackResourceResponse>
<DescribeStackResourceResult>
<StackResourceDetail>
@ -758,7 +796,6 @@ DESCRIBE_STACK_RESOURCE_RESPONSE_TEMPLATE = """<DescribeStackResourceResponse>
</DescribeStackResourceResult>
</DescribeStackResourceResponse>"""
DESCRIBE_STACK_RESOURCES_RESPONSE = """<DescribeStackResourcesResponse>
<DescribeStackResourcesResult>
<StackResources>
@ -777,7 +814,6 @@ DESCRIBE_STACK_RESOURCES_RESPONSE = """<DescribeStackResourcesResponse>
</DescribeStackResourcesResult>
</DescribeStackResourcesResponse>"""
DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
<DescribeStackEventsResult>
<StackEvents>
@ -802,7 +838,6 @@ DESCRIBE_STACK_EVENTS_RESPONSE = """<DescribeStackEventsResponse xmlns="http://c
</ResponseMetadata>
</DescribeStackEventsResponse>"""
LIST_CHANGE_SETS_RESPONSE = """<ListChangeSetsResponse>
<ListChangeSetsResult>
<Summaries>
@ -823,7 +858,6 @@ LIST_CHANGE_SETS_RESPONSE = """<ListChangeSetsResponse>
</ListChangeSetsResult>
</ListChangeSetsResponse>"""
LIST_STACKS_RESPONSE = """<ListStacksResponse>
<ListStacksResult>
<StackSummaries>
@ -840,7 +874,6 @@ LIST_STACKS_RESPONSE = """<ListStacksResponse>
</ListStacksResult>
</ListStacksResponse>"""
LIST_STACKS_RESOURCES_RESPONSE = """<ListStackResourcesResponse>
<ListStackResourcesResult>
<StackResourceSummaries>
@ -860,7 +893,6 @@ LIST_STACKS_RESOURCES_RESPONSE = """<ListStackResourcesResponse>
</ResponseMetadata>
</ListStackResourcesResponse>"""
GET_TEMPLATE_RESPONSE_TEMPLATE = """<GetTemplateResponse>
<GetTemplateResult>
<TemplateBody>{{ stack.template }}</TemplateBody>
@ -870,7 +902,6 @@ GET_TEMPLATE_RESPONSE_TEMPLATE = """<GetTemplateResponse>
</ResponseMetadata>
</GetTemplateResponse>"""
DELETE_STACK_RESPONSE_TEMPLATE = """<DeleteStackResponse>
<ResponseMetadata>
<RequestId>5ccc7dcd-744c-11e5-be70-example</RequestId>
@ -878,7 +909,6 @@ DELETE_STACK_RESPONSE_TEMPLATE = """<DeleteStackResponse>
</DeleteStackResponse>
"""
LIST_EXPORTS_RESPONSE = """<ListExportsResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
<ListExportsResult>
<Exports>
@ -1139,3 +1169,19 @@ LIST_STACK_SET_OPERATION_RESULTS_RESPONSE_TEMPLATE = (
</ListStackSetOperationResultsResponse>
"""
)
GET_TEMPLATE_SUMMARY_TEMPLATE = """<GetTemplateSummaryResponse xmlns="http://cloudformation.amazonaws.com/doc/2010-05-15/">
<GetTemplateSummaryResult>
<Description>{{ template_summary.Description }}</Description>
{% for resource in template_summary.resourceTypes %}
<ResourceTypes>
<ResourceType>{{ resource }}</ResourceType>
</ResourceTypes>
{% endfor %}
<Version>{{ template_summary.AWSTemplateFormatVersion }}</Version>
</GetTemplateSummaryResult>
<ResponseMetadata>
<RequestId>b9b4b068-3a41-11e5-94eb-example</RequestId>
</ResponseMetadata>
</GetTemplateSummaryResponse>
"""

View File

@ -356,3 +356,14 @@ def tags_from_query_string(
else:
response_values[tag_key] = None
return response_values
def tags_from_cloudformation_tags_list(tags_list):
"""Return tags in dict form from cloudformation resource tags form (list of dicts)"""
tags = {}
for entry in tags_list:
key = entry["Key"]
value = entry["Value"]
tags[key] = value
return tags

View File

@ -818,13 +818,25 @@ EC2_DESCRIBE_INSTANCE_TYPES = """<?xml version="1.0" encoding="UTF-8"?>
<instanceTypeSet>
{% for instance_type in instance_types %}
<item>
<name>{{ instance_type.name }}</name>
<vcpu>{{ instance_type.cores }}</vcpu>
<memory>{{ instance_type.memory }}</memory>
<storageSize>{{ instance_type.disk }}</storageSize>
<storageCount>{{ instance_type.storageCount }}</storageCount>
<maxIpAddresses>{{ instance_type.maxIpAddresses }}</maxIpAddresses>
<ebsOptimizedAvailable>{{ instance_type.ebsOptimizedAvailable }}</ebsOptimizedAvailable>
<instanceType>{{ instance_type.name }}</instanceType>
<vCpuInfo>
<defaultVCpus>{{ instance_type.cores }}</defaultVCpus>
<defaultCores>{{ instance_type.cores }}</defaultCores>
<defaultThreadsPerCore>1</defaultThreadsPerCore>
</vCpuInfo>
<memoryInfo>
<sizeInMiB>{{ instance_type.memory }}</sizeInMiB>
</memoryInfo>
<instanceStorageInfo>
<totalSizeInGB>{{ instance_type.disk }}</totalSizeInGB>
</instanceStorageInfo>
<processorInfo>
<supportedArchitectures>
<item>
x86_64
</item>
</supportedArchitectures>
</processorInfo>
</item>
{% endfor %}
</instanceTypeSet>

View File

@ -946,6 +946,10 @@ class AccountSummary(BaseModel):
return len(self._iam_backend.users)
def filter_items_with_path_prefix(path_prefix, items):
return [role for role in items if role.path.startswith(path_prefix)]
class IAMBackend(BaseBackend):
def __init__(self):
self.instance_profiles = {}
@ -1490,7 +1494,11 @@ class IAMBackend(BaseBackend):
def list_users(self, path_prefix, marker, max_items):
users = None
try:
users = self.users.values()
if path_prefix:
users = filter_items_with_path_prefix(path_prefix, users)
except KeyError:
raise IAMNotFoundException(
"Users {0}, {1}, {2} not found".format(path_prefix, marker, max_items)

View File

@ -337,7 +337,6 @@ class IamResponse(BaseResponse):
def list_roles(self):
roles = iam_backend.get_roles()
template = self.response_template(LIST_ROLES_TEMPLATE)
return template.render(roles=roles)

View File

@ -4,9 +4,14 @@ from .responses import ResourceGroupsResponse
url_bases = ["https?://resource-groups(-fips)?.(.+).amazonaws.com"]
url_paths = {
"{0}/delete-group$": ResourceGroupsResponse.dispatch,
"{0}/get-group$": ResourceGroupsResponse.dispatch,
"{0}/get-group-query$": ResourceGroupsResponse.dispatch,
"{0}/groups$": ResourceGroupsResponse.dispatch,
"{0}/groups/(?P<resource_group_name>[^/]+)$": ResourceGroupsResponse.dispatch,
"{0}/groups/(?P<resource_group_name>[^/]+)/query$": ResourceGroupsResponse.dispatch,
"{0}/groups-list$": ResourceGroupsResponse.dispatch,
"{0}/resources/(?P<resource_arn>[^/]+)/tags$": ResourceGroupsResponse.dispatch,
"{0}/update-group$": ResourceGroupsResponse.dispatch,
"{0}/update-group-query$": ResourceGroupsResponse.dispatch,
}

View File

@ -580,7 +580,12 @@ class SNSBackend(BaseBackend):
return subscription.attributes
def set_subscription_attributes(self, arn, name, value):
if name not in ["RawMessageDelivery", "DeliveryPolicy", "FilterPolicy"]:
if name not in [
"RawMessageDelivery",
"DeliveryPolicy",
"FilterPolicy",
"RedrivePolicy",
]:
raise SNSInvalidParameter("AttributeName")
# TODO: should do validation

View File

@ -16,11 +16,13 @@ class ReceiptHandleIsInvalid(RESTError):
)
class MessageAttributesInvalid(Exception):
status_code = 400
class MessageAttributesInvalid(RESTError):
code = 400
def __init__(self, description):
self.description = description
super(MessageAttributesInvalid, self).__init__(
"MessageAttributesInvalid", description
)
class QueueDoesNotExist(RESTError):

View File

@ -18,6 +18,7 @@ from moto.core.utils import (
get_random_message_id,
unix_time,
unix_time_millis,
tags_from_cloudformation_tags_list,
)
from .utils import generate_receipt_handle
from .exceptions import (
@ -87,7 +88,19 @@ class Message(BaseModel):
struct_format = "!I".encode("ascii") # ensure it's a bytestring
for name in sorted(self.message_attributes.keys()):
attr = self.message_attributes[name]
data_type = attr["data_type"]
data_type_parts = attr["data_type"].split(".")
data_type = data_type_parts[0]
if data_type not in [
"String",
"Binary",
"Number",
]:
raise MessageAttributesInvalid(
"The message attribute '{0}' has an invalid message attribute type, the set of supported type prefixes is Binary, Number, and String.".format(
name[0]
)
)
encoded = utf8("")
# Each part of each attribute is encoded right after it's
@ -243,9 +256,7 @@ class Queue(BaseModel):
# Check some conditions
if self.fifo_queue and not self.name.endswith(".fifo"):
raise MessageAttributesInvalid(
"Queue name must end in .fifo for FIFO queues"
)
raise InvalidParameterValue("Queue name must end in .fifo for FIFO queues")
@property
def pending_messages(self):
@ -347,11 +358,17 @@ class Queue(BaseModel):
def create_from_cloudformation_json(
cls, resource_name, cloudformation_json, region_name
):
properties = cloudformation_json["Properties"]
properties = deepcopy(cloudformation_json["Properties"])
# remove Tags from properties and convert tags list to dict
tags = properties.pop("Tags", [])
tags_dict = tags_from_cloudformation_tags_list(tags)
sqs_backend = sqs_backends[region_name]
return sqs_backend.create_queue(
name=properties["QueueName"], region=region_name, **properties
name=properties["QueueName"],
tags=tags_dict,
region=region_name,
**properties
)
@classmethod

View File

@ -9,7 +9,6 @@ from six.moves.urllib.parse import urlparse
from .exceptions import (
EmptyBatchRequest,
InvalidAttributeName,
MessageAttributesInvalid,
MessageNotInflight,
ReceiptHandleIsInvalid,
)
@ -82,12 +81,7 @@ class SQSResponse(BaseResponse):
request_url = urlparse(self.uri)
queue_name = self._get_param("QueueName")
try:
queue = self.sqs_backend.create_queue(
queue_name, self.tags, **self.attribute
)
except MessageAttributesInvalid as e:
return self._error("InvalidParameterValue", e.description)
queue = self.sqs_backend.create_queue(queue_name, self.tags, **self.attribute)
template = self.response_template(CREATE_QUEUE_RESPONSE)
return template.render(queue_url=queue.url(request_url))
@ -225,10 +219,7 @@ class SQSResponse(BaseResponse):
if len(message) > MAXIMUM_MESSAGE_LENGTH:
return ERROR_TOO_LONG_RESPONSE, dict(status=400)
try:
message_attributes = parse_message_attributes(self.querystring)
except MessageAttributesInvalid as e:
return e.description, dict(status=e.status_code)
message_attributes = parse_message_attributes(self.querystring)
queue_name = self._get_queue_name()

View File

@ -34,7 +34,7 @@ def parse_message_attributes(querystring, base="", value_namespace="Value."):
)
data_type_parts = data_type[0].split(".")
if len(data_type_parts) > 2 or data_type_parts[0] not in [
if data_type_parts[0] not in [
"String",
"Binary",
"Number",

View File

@ -78,6 +78,13 @@ class InvalidDocumentOperation(JsonRESTError):
)
class AccessDeniedException(JsonRESTError):
code = 400
def __init__(self, message):
super(AccessDeniedException, self).__init__("AccessDeniedException", message)
class InvalidDocumentContent(JsonRESTError):
code = 400

View File

@ -27,6 +27,7 @@ from .exceptions import (
ParameterNotFound,
DocumentAlreadyExists,
InvalidDocumentOperation,
AccessDeniedException,
InvalidDocument,
InvalidDocumentContent,
InvalidDocumentVersion,
@ -965,6 +966,13 @@ class SimpleSystemManagerBackend(BaseBackend):
"The following filter key is not valid: Label. Valid filter keys include: [Path, Name, Type, KeyId, Tier]."
)
if by_path and key in ["Name", "Path", "Tier"]:
raise InvalidFilterKey(
"The following filter key is not valid: {key}. Valid filter keys include: [Type, KeyId].".format(
key=key
)
)
if not values:
raise InvalidFilterValue(
"The following filter values are missing : null for filter key Name."
@ -1024,7 +1032,10 @@ class SimpleSystemManagerBackend(BaseBackend):
)
)
if key != "Path" and option not in ["Equals", "BeginsWith"]:
allowed_options = ["Equals", "BeginsWith"]
if key == "Name":
allowed_options += ["Contains"]
if key != "Path" and option not in allowed_options:
raise InvalidFilterOption(
"The following filter option is not valid: {option}. Valid options include: [BeginsWith, Equals].".format(
option=option
@ -1084,6 +1095,9 @@ class SimpleSystemManagerBackend(BaseBackend):
max_results=10,
):
"""Implement the get-parameters-by-path-API in the backend."""
self._validate_parameter_filters(filters, by_path=True)
result = []
# path could be with or without a trailing /. we handle this
# difference here.
@ -1134,7 +1148,8 @@ class SimpleSystemManagerBackend(BaseBackend):
what = parameter.keyid
elif key == "Name":
what = "/" + parameter.name.lstrip("/")
values = ["/" + value.lstrip("/") for value in values]
if option != "Contains":
values = ["/" + value.lstrip("/") for value in values]
elif key == "Path":
what = "/" + parameter.name.lstrip("/")
values = ["/" + value.strip("/") for value in values]
@ -1147,6 +1162,8 @@ class SimpleSystemManagerBackend(BaseBackend):
what.startswith(value) for value in values
):
return False
elif option == "Contains" and not any(value in what for value in values):
return False
elif option == "Equals" and not any(what == value for value in values):
return False
elif option == "OneLevel":
@ -1172,8 +1189,33 @@ class SimpleSystemManagerBackend(BaseBackend):
return True
def get_parameter(self, name, with_decryption):
if name in self._parameters:
return self._parameters[name][-1]
name_parts = name.split(":")
name_prefix = name_parts[0]
if len(name_parts) > 2:
return None
if name_prefix in self._parameters:
if len(name_parts) == 1:
return self._parameters[name][-1]
if len(name_parts) == 2:
version_or_label = name_parts[1]
parameters = self._parameters[name_prefix]
if version_or_label.isdigit():
result = list(
filter(lambda x: str(x.version) == version_or_label, parameters)
)
if len(result) > 0:
return result[-1]
result = list(
filter(lambda x: version_or_label in x.labels, parameters)
)
if len(result) > 0:
return result[-1]
return None
def label_parameter_version(self, name, version, labels):
@ -1238,6 +1280,23 @@ class SimpleSystemManagerBackend(BaseBackend):
def put_parameter(
self, name, description, value, type, allowed_pattern, keyid, overwrite
):
if name.lower().lstrip("/").startswith("aws") or name.lower().lstrip(
"/"
).startswith("ssm"):
is_path = name.count("/") > 1
if name.lower().startswith("/aws") and is_path:
raise AccessDeniedException(
"No access to reserved parameter name: {name}.".format(name=name)
)
if not is_path:
invalid_prefix_error = 'Parameter name: can\'t be prefixed with "aws" or "ssm" (case-insensitive).'
else:
invalid_prefix_error = (
'Parameter name: can\'t be prefixed with "ssm" (case-insensitive). '
"If formed as a path, it can consist of sub-paths divided by slash symbol; each sub-path can be "
"formed as a mix of letters, numbers and the following 3 symbols .-_"
)
raise ValidationException(invalid_prefix_error)
previous_parameter_versions = self._parameters[name]
if len(previous_parameter_versions) == 0:
previous_parameter = None

View File

@ -35,6 +35,14 @@ dummy_template = {
},
}
dummy_template3 = {
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Stack 3",
"Resources": {
"VPC": {"Properties": {"CidrBlock": "192.168.0.0/16"}, "Type": "AWS::EC2::VPC"}
},
}
dummy_template_yaml = """---
AWSTemplateFormatVersion: 2010-09-09
Description: Stack1 with yaml template
@ -646,6 +654,31 @@ def test_boto3_create_stack():
)
@mock_cloudformation
def test_boto3_create_stack_s3_long_name():
cf_conn = boto3.client("cloudformation", region_name="us-east-1")
stack_name = "MyLongStackName01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012"
template = '{"Resources":{"HelloBucket":{"Type":"AWS::S3::Bucket"}}}'
cf_conn.create_stack(StackName=stack_name, TemplateBody=template)
cf_conn.get_template(StackName=stack_name)["TemplateBody"].should.equal(
json.loads(template, object_pairs_hook=OrderedDict)
)
provisioned_resource = cf_conn.list_stack_resources(StackName=stack_name)[
"StackResourceSummaries"
][0]
provisioned_bucket_name = provisioned_resource["PhysicalResourceId"]
len(provisioned_bucket_name).should.be.lower_than(64)
logical_name_lower_case = provisioned_resource["LogicalResourceId"].lower()
bucket_name_stack_name_prefix = provisioned_bucket_name[
: provisioned_bucket_name.index("-" + logical_name_lower_case)
]
stack_name.lower().should.contain(bucket_name_stack_name_prefix)
@mock_cloudformation
def test_boto3_create_stack_with_yaml():
cf_conn = boto3.client("cloudformation", region_name="us-east-1")
@ -668,6 +701,48 @@ def test_boto3_create_stack_with_short_form_func_yaml():
)
@mock_s3
@mock_cloudformation
def test_get_template_summary():
s3 = boto3.client("s3")
s3_conn = boto3.resource("s3", region_name="us-east-1")
conn = boto3.client("cloudformation", region_name="us-east-1")
result = conn.get_template_summary(TemplateBody=json.dumps(dummy_template3))
result["ResourceTypes"].should.equal(["AWS::EC2::VPC"])
result["Version"].should.equal("2010-09-09")
result["Description"].should.equal("Stack 3")
conn.create_stack(StackName="test_stack", TemplateBody=json.dumps(dummy_template3))
result = conn.get_template_summary(StackName="test_stack")
result["ResourceTypes"].should.equal(["AWS::EC2::VPC"])
result["Version"].should.equal("2010-09-09")
result["Description"].should.equal("Stack 3")
s3_conn.create_bucket(Bucket="foobar")
s3_conn.Object("foobar", "template-key").put(Body=json.dumps(dummy_template3))
key_url = s3.generate_presigned_url(
ClientMethod="get_object", Params={"Bucket": "foobar", "Key": "template-key"}
)
conn.create_stack(StackName="stack_from_url", TemplateURL=key_url)
result = conn.get_template_summary(TemplateURL=key_url)
result["ResourceTypes"].should.equal(["AWS::EC2::VPC"])
result["Version"].should.equal("2010-09-09")
result["Description"].should.equal("Stack 3")
conn = boto3.client("cloudformation", region_name="us-east-1")
result = conn.get_template_summary(TemplateBody=dummy_template_yaml)
result["ResourceTypes"].should.equal(["AWS::EC2::Instance"])
result["Version"].should.equal("2010-09-09")
result["Description"].should.equal("Stack1 with yaml template")
@mock_cloudformation
def test_boto3_create_stack_with_ref_yaml():
cf_conn = boto3.client("cloudformation", region_name="us-east-1")

View File

@ -0,0 +1,18 @@
from __future__ import unicode_literals
import boto3
import sure # noqa
from moto import mock_ec2
@mock_ec2
def test_describe_instance_types():
client = boto3.client("ec2", "us-east-1")
instance_types = client.describe_instance_types()
instance_types.should.have.key("InstanceTypes")
instance_types["InstanceTypes"].should_not.be.empty
instance_types["InstanceTypes"][0].should.have.key("InstanceType")
instance_types["InstanceTypes"][0].should.have.key("MemoryInfo")
instance_types["InstanceTypes"][0]["MemoryInfo"].should.have.key("SizeInMiB")

View File

@ -762,6 +762,12 @@ def test_list_users():
user["Path"].should.equal("/")
user["Arn"].should.equal("arn:aws:iam::{}:user/my-user".format(ACCOUNT_ID))
conn.create_user(UserName="my-user-1", Path="myUser")
response = conn.list_users(PathPrefix="my")
user = response["Users"][0]
user["UserName"].should.equal("my-user-1")
user["Path"].should.equal("myUser")
@mock_iam()
def test_user_policies():

View File

@ -17,12 +17,34 @@ from boto.exception import SQSError
from boto.sqs.message import Message, RawMessage
from botocore.exceptions import ClientError
from freezegun import freeze_time
from moto import mock_sqs, mock_sqs_deprecated, settings
from moto import mock_sqs, mock_sqs_deprecated, mock_cloudformation, settings
from nose import SkipTest
from nose.tools import assert_raises
from tests.helpers import requires_boto_gte
from moto.core import ACCOUNT_ID
sqs_template_with_tags = """
{
"AWSTemplateFormatVersion": "2010-09-09",
"Resources": {
"SQSQueue": {
"Type": "AWS::SQS::Queue",
"Properties": {
"Tags" : [
{
"Key" : "keyname1",
"Value" : "value1"
},
{
"Key" : "keyname2",
"Value" : "value2"
}
]
}
}
}
}"""
@mock_sqs
def test_create_fifo_queue_fail():
@ -248,6 +270,50 @@ def test_message_with_complex_attributes():
messages.should.have.length_of(1)
@mock_sqs
def test_message_with_attributes_have_labels():
sqs = boto3.resource("sqs", region_name="us-east-1")
queue = sqs.create_queue(QueueName="blah")
msg = queue.send_message(
MessageBody="derp",
MessageAttributes={
"timestamp": {
"DataType": "Number.java.lang.Long",
"StringValue": "1493147359900",
}
},
)
msg.get("MD5OfMessageBody").should.equal("58fd9edd83341c29f1aebba81c31e257")
msg.get("MD5OfMessageAttributes").should.equal("235c5c510d26fb653d073faed50ae77c")
msg.get("MessageId").should_not.contain(" \n")
messages = queue.receive_messages()
messages.should.have.length_of(1)
@mock_sqs
def test_message_with_attributes_invalid_datatype():
sqs = boto3.resource("sqs", region_name="us-east-1")
queue = sqs.create_queue(QueueName="blah")
with assert_raises(ClientError) as e:
queue.send_message(
MessageBody="derp",
MessageAttributes={
"timestamp": {
"DataType": "InvalidNumber",
"StringValue": "149314735990a",
}
},
)
ex = e.exception
ex.response["Error"]["Code"].should.equal("MessageAttributesInvalid")
ex.response["Error"]["Message"].should.equal(
"The message attribute 'timestamp' has an invalid message attribute type, the set of supported type "
"prefixes is Binary, Number, and String."
)
@mock_sqs
def test_send_message_with_message_group_id():
sqs = boto3.resource("sqs", region_name="us-east-1")
@ -532,6 +598,54 @@ def test_send_receive_message_with_attributes():
)
@mock_sqs
def test_send_receive_message_with_attributes_with_labels():
sqs = boto3.resource("sqs", region_name="us-east-1")
conn = boto3.client("sqs", region_name="us-east-1")
conn.create_queue(QueueName="test-queue")
queue = sqs.Queue("test-queue")
body_one = "this is a test message"
body_two = "this is another test message"
queue.send_message(
MessageBody=body_one,
MessageAttributes={
"timestamp": {
"StringValue": "1493147359900",
"DataType": "Number.java.lang.Long",
}
},
)
queue.send_message(
MessageBody=body_two,
MessageAttributes={
"timestamp": {
"StringValue": "1493147359901",
"DataType": "Number.java.lang.Long",
}
},
)
messages = conn.receive_message(QueueUrl=queue.url, MaxNumberOfMessages=2)[
"Messages"
]
message1 = messages[0]
message2 = messages[1]
message1.get("Body").should.equal(body_one)
message2.get("Body").should.equal(body_two)
message1.get("MD5OfMessageAttributes").should.equal(
"235c5c510d26fb653d073faed50ae77c"
)
message2.get("MD5OfMessageAttributes").should.equal(
"994258b45346a2cc3f9cbb611aa7af30"
)
@mock_sqs
def test_send_receive_message_timestamps():
sqs = boto3.resource("sqs", region_name="us-east-1")
@ -1841,3 +1955,17 @@ def test_send_messages_to_fifo_without_message_group_id():
ex.response["Error"]["Message"].should.equal(
"The request must contain the parameter MessageGroupId."
)
@mock_sqs
@mock_cloudformation
def test_create_from_cloudformation_json_with_tags():
cf = boto3.client("cloudformation", region_name="us-east-1")
client = boto3.client("sqs", region_name="us-east-1")
cf.create_stack(StackName="test-sqs", TemplateBody=sqs_template_with_tags)
queue_url = client.list_queues()["QueueUrls"][0]
queue_tags = client.list_queue_tags(QueueUrl=queue_url)["Tags"]
queue_tags.should.equal({"keyname1": "value1", "keyname2": "value2"})

View File

@ -198,6 +198,33 @@ def test_get_parameters_by_path():
len(response["Parameters"]).should.equal(1)
response.should_not.have.key("NextToken")
filters = [{"Key": "Name", "Values": ["error"]}]
client.get_parameters_by_path.when.called_with(
Path="/baz", ParameterFilters=filters
).should.throw(
ClientError,
"The following filter key is not valid: Name. "
"Valid filter keys include: [Type, KeyId].",
)
filters = [{"Key": "Path", "Values": ["/error"]}]
client.get_parameters_by_path.when.called_with(
Path="/baz", ParameterFilters=filters
).should.throw(
ClientError,
"The following filter key is not valid: Path. "
"Valid filter keys include: [Type, KeyId].",
)
filters = [{"Key": "Tier", "Values": ["Standard"]}]
client.get_parameters_by_path.when.called_with(
Path="/baz", ParameterFilters=filters
).should.throw(
ClientError,
"The following filter key is not valid: Tier. "
"Valid filter keys include: [Type, KeyId].",
)
@mock_ssm
def test_put_parameter():
@ -272,6 +299,73 @@ def test_put_parameter():
)
@mock_ssm
def test_put_parameter_invalid_names():
client = boto3.client("ssm", region_name="us-east-1")
invalid_prefix_err = (
'Parameter name: can\'t be prefixed with "aws" or "ssm" (case-insensitive).'
)
client.put_parameter.when.called_with(
Name="ssm_test", Value="value", Type="String"
).should.throw(
ClientError, invalid_prefix_err,
)
client.put_parameter.when.called_with(
Name="SSM_TEST", Value="value", Type="String"
).should.throw(
ClientError, invalid_prefix_err,
)
client.put_parameter.when.called_with(
Name="aws_test", Value="value", Type="String"
).should.throw(
ClientError, invalid_prefix_err,
)
client.put_parameter.when.called_with(
Name="AWS_TEST", Value="value", Type="String"
).should.throw(
ClientError, invalid_prefix_err,
)
ssm_path = "/ssm_test/path/to/var"
client.put_parameter.when.called_with(
Name=ssm_path, Value="value", Type="String"
).should.throw(
ClientError,
'Parameter name: can\'t be prefixed with "ssm" (case-insensitive). If formed as a path, it can consist of '
"sub-paths divided by slash symbol; each sub-path can be formed as a mix of letters, numbers and the following "
"3 symbols .-_",
)
ssm_path = "/SSM/PATH/TO/VAR"
client.put_parameter.when.called_with(
Name=ssm_path, Value="value", Type="String"
).should.throw(
ClientError,
'Parameter name: can\'t be prefixed with "ssm" (case-insensitive). If formed as a path, it can consist of '
"sub-paths divided by slash symbol; each sub-path can be formed as a mix of letters, numbers and the following "
"3 symbols .-_",
)
aws_path = "/aws_test/path/to/var"
client.put_parameter.when.called_with(
Name=aws_path, Value="value", Type="String"
).should.throw(
ClientError, "No access to reserved parameter name: {}.".format(aws_path),
)
aws_path = "/AWS/PATH/TO/VAR"
client.put_parameter.when.called_with(
Name=aws_path, Value="value", Type="String"
).should.throw(
ClientError, "No access to reserved parameter name: {}.".format(aws_path),
)
@mock_ssm
def test_put_parameter_china():
client = boto3.client("ssm", region_name="cn-north-1")
@ -302,6 +396,64 @@ def test_get_parameter():
)
@mock_ssm
def test_get_parameter_with_version_and_labels():
client = boto3.client("ssm", region_name="us-east-1")
client.put_parameter(
Name="test-1", Description="A test parameter", Value="value", Type="String"
)
client.put_parameter(
Name="test-2", Description="A test parameter", Value="value", Type="String"
)
client.label_parameter_version(
Name="test-2", ParameterVersion=1, Labels=["test-label"]
)
response = client.get_parameter(Name="test-1:1", WithDecryption=False)
response["Parameter"]["Name"].should.equal("test-1")
response["Parameter"]["Value"].should.equal("value")
response["Parameter"]["Type"].should.equal("String")
response["Parameter"]["LastModifiedDate"].should.be.a(datetime.datetime)
response["Parameter"]["ARN"].should.equal(
"arn:aws:ssm:us-east-1:1234567890:parameter/test-1"
)
response = client.get_parameter(Name="test-2:1", WithDecryption=False)
response["Parameter"]["Name"].should.equal("test-2")
response["Parameter"]["Value"].should.equal("value")
response["Parameter"]["Type"].should.equal("String")
response["Parameter"]["LastModifiedDate"].should.be.a(datetime.datetime)
response["Parameter"]["ARN"].should.equal(
"arn:aws:ssm:us-east-1:1234567890:parameter/test-2"
)
response = client.get_parameter(Name="test-2:test-label", WithDecryption=False)
response["Parameter"]["Name"].should.equal("test-2")
response["Parameter"]["Value"].should.equal("value")
response["Parameter"]["Type"].should.equal("String")
response["Parameter"]["LastModifiedDate"].should.be.a(datetime.datetime)
response["Parameter"]["ARN"].should.equal(
"arn:aws:ssm:us-east-1:1234567890:parameter/test-2"
)
with assert_raises(ClientError) as ex:
client.get_parameter(Name="test-2:2:3", WithDecryption=False)
ex.exception.response["Error"]["Code"].should.equal("ParameterNotFound")
ex.exception.response["Error"]["Message"].should.equal(
"Parameter test-2:2:3 not found."
)
with assert_raises(ClientError) as ex:
client.get_parameter(Name="test-2:2", WithDecryption=False)
ex.exception.response["Error"]["Code"].should.equal("ParameterNotFound")
ex.exception.response["Error"]["Message"].should.equal(
"Parameter test-2:2 not found."
)
@mock_ssm
def test_get_parameters_errors():
client = boto3.client("ssm", region_name="us-east-1")
@ -504,6 +656,9 @@ def test_describe_parameters_with_parameter_filters_name():
client = boto3.client("ssm", region_name="us-east-1")
client.put_parameter(Name="param", Value="value", Type="String")
client.put_parameter(Name="/param-2", Value="value-2", Type="String")
client.put_parameter(Name="/tangent-3", Value="value-3", Type="String")
client.put_parameter(Name="tangram-4", Value="value-4", Type="String")
client.put_parameter(Name="standby-5", Value="value-5", Type="String")
response = client.describe_parameters(
ParameterFilters=[{"Key": "Name", "Values": ["param"]}]
@ -543,6 +698,22 @@ def test_describe_parameters_with_parameter_filters_name():
parameters.should.have.length_of(2)
response.should_not.have.key("NextToken")
response = client.describe_parameters(
ParameterFilters=[{"Key": "Name", "Option": "Contains", "Values": ["ram"]}]
)
parameters = response["Parameters"]
parameters.should.have.length_of(3)
response.should_not.have.key("NextToken")
response = client.describe_parameters(
ParameterFilters=[{"Key": "Name", "Option": "Contains", "Values": ["/tan"]}]
)
parameters = response["Parameters"]
parameters.should.have.length_of(2)
response.should_not.have.key("NextToken")
@mock_ssm
def test_describe_parameters_with_parameter_filters_path():