This commit is contained in:
Chris Keogh 2017-09-25 11:40:02 +13:00
commit 631252dd50
19 changed files with 758 additions and 74 deletions

View File

@ -18,7 +18,14 @@ test_server:
aws_managed_policies:
scripts/update_managed_policies.py
publish:
upload_pypi_artifact:
python setup.py sdist bdist_wheel upload
build_dockerhub_image:
docker build -t motoserver/moto .
tag_github_release:
git tag `python setup.py --version`
git push origin `python setup.py --version`
publish: upload_pypi_artifact build_dockerhub_image tag_github_release

View File

@ -110,7 +110,7 @@ It gets even better! Moto isn't just for Python code and it isn't just for S3. L
|------------------------------------------------------------------------------|
| SES | @mock_ses | core endpoints done |
|------------------------------------------------------------------------------|
| SNS | @mock_sns | core endpoints done |
| SNS | @mock_sns | all endpoints done |
|------------------------------------------------------------------------------|
| SQS | @mock_sqs | core endpoints done |
|------------------------------------------------------------------------------|

View File

@ -2,6 +2,11 @@ from moto.core import BaseBackend, BaseModel
import boto.ec2.cloudwatch
import datetime
from .utils import make_arn_for_dashboard
DEFAULT_ACCOUNT_ID = 123456789012
class Dimension(object):
@ -44,10 +49,34 @@ class MetricDatum(BaseModel):
'value']) for dimension in dimensions]
class Dashboard(BaseModel):
def __init__(self, name, body):
# Guaranteed to be unique for now as the name is also the key of a dictionary where they are stored
self.arn = make_arn_for_dashboard(DEFAULT_ACCOUNT_ID, name)
self.name = name
self.body = body
self.last_modified = datetime.datetime.now()
@property
def last_modified_iso(self):
return self.last_modified.isoformat()
@property
def size(self):
return len(self)
def __len__(self):
return len(self.body)
def __repr__(self):
return '<CloudWatchDashboard {0}>'.format(self.name)
class CloudWatchBackend(BaseBackend):
def __init__(self):
self.alarms = {}
self.dashboards = {}
self.metric_data = []
def put_metric_alarm(self, name, namespace, metric_name, comparison_operator, evaluation_periods,
@ -110,6 +139,31 @@ class CloudWatchBackend(BaseBackend):
def get_all_metrics(self):
return self.metric_data
def put_dashboard(self, name, body):
self.dashboards[name] = Dashboard(name, body)
def list_dashboards(self, prefix=''):
for key, value in self.dashboards.items():
if key.startswith(prefix):
yield value
def delete_dashboards(self, dashboards):
to_delete = set(dashboards)
all_dashboards = set(self.dashboards.keys())
left_over = to_delete - all_dashboards
if len(left_over) > 0:
# Some dashboards are not found
return False, 'The specified dashboard does not exist. [{0}]'.format(', '.join(left_over))
for dashboard in to_delete:
del self.dashboards[dashboard]
return True, None
def get_dashboard(self, dashboard):
return self.dashboards.get(dashboard)
class LogGroup(BaseModel):

View File

@ -1,9 +1,18 @@
import json
from moto.core.responses import BaseResponse
from .models import cloudwatch_backends
class CloudWatchResponse(BaseResponse):
@property
def cloudwatch_backend(self):
return cloudwatch_backends[self.region]
def _error(self, code, message, status=400):
template = self.response_template(ERROR_RESPONSE_TEMPLATE)
return template.render(code=code, message=message), dict(status=status)
def put_metric_alarm(self):
name = self._get_param('AlarmName')
namespace = self._get_param('Namespace')
@ -20,15 +29,14 @@ class CloudWatchResponse(BaseResponse):
insufficient_data_actions = self._get_multi_param(
"InsufficientDataActions.member")
unit = self._get_param('Unit')
cloudwatch_backend = cloudwatch_backends[self.region]
alarm = cloudwatch_backend.put_metric_alarm(name, namespace, metric_name,
comparison_operator,
evaluation_periods, period,
threshold, statistic,
description, dimensions,
alarm_actions, ok_actions,
insufficient_data_actions,
unit)
alarm = self.cloudwatch_backend.put_metric_alarm(name, namespace, metric_name,
comparison_operator,
evaluation_periods, period,
threshold, statistic,
description, dimensions,
alarm_actions, ok_actions,
insufficient_data_actions,
unit)
template = self.response_template(PUT_METRIC_ALARM_TEMPLATE)
return template.render(alarm=alarm)
@ -37,28 +45,26 @@ class CloudWatchResponse(BaseResponse):
alarm_name_prefix = self._get_param('AlarmNamePrefix')
alarm_names = self._get_multi_param('AlarmNames.member')
state_value = self._get_param('StateValue')
cloudwatch_backend = cloudwatch_backends[self.region]
if action_prefix:
alarms = cloudwatch_backend.get_alarms_by_action_prefix(
alarms = self.cloudwatch_backend.get_alarms_by_action_prefix(
action_prefix)
elif alarm_name_prefix:
alarms = cloudwatch_backend.get_alarms_by_alarm_name_prefix(
alarms = self.cloudwatch_backend.get_alarms_by_alarm_name_prefix(
alarm_name_prefix)
elif alarm_names:
alarms = cloudwatch_backend.get_alarms_by_alarm_names(alarm_names)
alarms = self.cloudwatch_backend.get_alarms_by_alarm_names(alarm_names)
elif state_value:
alarms = cloudwatch_backend.get_alarms_by_state_value(state_value)
alarms = self.cloudwatch_backend.get_alarms_by_state_value(state_value)
else:
alarms = cloudwatch_backend.get_all_alarms()
alarms = self.cloudwatch_backend.get_all_alarms()
template = self.response_template(DESCRIBE_ALARMS_TEMPLATE)
return template.render(alarms=alarms)
def delete_alarms(self):
alarm_names = self._get_multi_param('AlarmNames.member')
cloudwatch_backend = cloudwatch_backends[self.region]
cloudwatch_backend.delete_alarms(alarm_names)
self.cloudwatch_backend.delete_alarms(alarm_names)
template = self.response_template(DELETE_METRIC_ALARMS_TEMPLATE)
return template.render()
@ -89,17 +95,77 @@ class CloudWatchResponse(BaseResponse):
dimension_index += 1
metric_data.append([metric_name, value, dimensions])
metric_index += 1
cloudwatch_backend = cloudwatch_backends[self.region]
cloudwatch_backend.put_metric_data(namespace, metric_data)
self.cloudwatch_backend.put_metric_data(namespace, metric_data)
template = self.response_template(PUT_METRIC_DATA_TEMPLATE)
return template.render()
def list_metrics(self):
cloudwatch_backend = cloudwatch_backends[self.region]
metrics = cloudwatch_backend.get_all_metrics()
metrics = self.cloudwatch_backend.get_all_metrics()
template = self.response_template(LIST_METRICS_TEMPLATE)
return template.render(metrics=metrics)
def delete_dashboards(self):
dashboards = self._get_multi_param('DashboardNames.member')
if dashboards is None:
return self._error('InvalidParameterValue', 'Need at least 1 dashboard')
status, error = self.cloudwatch_backend.delete_dashboards(dashboards)
if not status:
return self._error('ResourceNotFound', error)
template = self.response_template(DELETE_DASHBOARD_TEMPLATE)
return template.render()
def describe_alarm_history(self):
raise NotImplementedError()
def describe_alarms_for_metric(self):
raise NotImplementedError()
def disable_alarm_actions(self):
raise NotImplementedError()
def enable_alarm_actions(self):
raise NotImplementedError()
def get_dashboard(self):
dashboard_name = self._get_param('DashboardName')
dashboard = self.cloudwatch_backend.get_dashboard(dashboard_name)
if dashboard is None:
return self._error('ResourceNotFound', 'Dashboard does not exist')
template = self.response_template(GET_DASHBOARD_TEMPLATE)
return template.render(dashboard=dashboard)
def get_metric_statistics(self):
raise NotImplementedError()
def list_dashboards(self):
prefix = self._get_param('DashboardNamePrefix', '')
dashboards = self.cloudwatch_backend.list_dashboards(prefix)
template = self.response_template(LIST_DASHBOARD_RESPONSE)
return template.render(dashboards=dashboards)
def put_dashboard(self):
name = self._get_param('DashboardName')
body = self._get_param('DashboardBody')
try:
json.loads(body)
except ValueError:
return self._error('InvalidParameterInput', 'Body is invalid JSON')
self.cloudwatch_backend.put_dashboard(name, body)
template = self.response_template(PUT_DASHBOARD_RESPONSE)
return template.render()
def set_alarm_state(self):
raise NotImplementedError()
PUT_METRIC_ALARM_TEMPLATE = """<PutMetricAlarmResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
<ResponseMetadata>
@ -199,3 +265,58 @@ LIST_METRICS_TEMPLATE = """<ListMetricsResponse xmlns="http://monitoring.amazona
</NextToken>
</ListMetricsResult>
</ListMetricsResponse>"""
PUT_DASHBOARD_RESPONSE = """<PutDashboardResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
<PutDashboardResult>
<DashboardValidationMessages/>
</PutDashboardResult>
<ResponseMetadata>
<RequestId>44b1d4d8-9fa3-11e7-8ad3-41b86ac5e49e</RequestId>
</ResponseMetadata>
</PutDashboardResponse>"""
LIST_DASHBOARD_RESPONSE = """<ListDashboardsResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
<ListDashboardsResult>
<DashboardEntries>
{% for dashboard in dashboards %}
<member>
<DashboardArn>{{ dashboard.arn }}</DashboardArn>
<LastModified>{{ dashboard.last_modified_iso }}</LastModified>
<Size>{{ dashboard.size }}</Size>
<DashboardName>{{ dashboard.name }}</DashboardName>
</member>
{% endfor %}
</DashboardEntries>
</ListDashboardsResult>
<ResponseMetadata>
<RequestId>c3773873-9fa5-11e7-b315-31fcc9275d62</RequestId>
</ResponseMetadata>
</ListDashboardsResponse>"""
DELETE_DASHBOARD_TEMPLATE = """<DeleteDashboardsResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
<DeleteDashboardsResult/>
<ResponseMetadata>
<RequestId>68d1dc8c-9faa-11e7-a694-df2715690df2</RequestId>
</ResponseMetadata>
</DeleteDashboardsResponse>"""
GET_DASHBOARD_TEMPLATE = """<GetDashboardResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
<GetDashboardResult>
<DashboardArn>{{ dashboard.arn }}</DashboardArn>
<DashboardBody>{{ dashboard.body }}</DashboardBody>
<DashboardName>{{ dashboard.name }}</DashboardName>
</GetDashboardResult>
<ResponseMetadata>
<RequestId>e3c16bb0-9faa-11e7-b315-31fcc9275d62</RequestId>
</ResponseMetadata>
</GetDashboardResponse>
"""
ERROR_RESPONSE_TEMPLATE = """<ErrorResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
<Error>
<Type>Sender</Type>
<Code>{{ code }}</Code>
<Message>{{ message }}</Message>
</Error>
<RequestId>5e45fd1e-9fa3-11e7-b720-89e8821d38c4</RequestId>
</ErrorResponse>"""

5
moto/cloudwatch/utils.py Normal file
View File

@ -0,0 +1,5 @@
from __future__ import unicode_literals
def make_arn_for_dashboard(account_id, name):
return "arn:aws:cloudwatch::{0}dashboard/{1}".format(account_id, name)

View File

@ -310,7 +310,7 @@ class BaseResponse(_TemplateEnvironmentMixin):
param_index += 1
return results
def _get_map_prefix(self, param_prefix):
def _get_map_prefix(self, param_prefix, key_end='.key', value_end='.value'):
results = {}
param_index = 1
while 1:
@ -319,9 +319,9 @@ class BaseResponse(_TemplateEnvironmentMixin):
k, v = None, None
for key, value in self.querystring.items():
if key.startswith(index_prefix):
if key.endswith('.key'):
if key.endswith(key_end):
k = value[0]
elif key.endswith('.value'):
elif key.endswith(value_end):
v = value[0]
if not (k and v):

View File

@ -547,9 +547,15 @@ class ResponseObject(_TemplateEnvironmentMixin):
# ACL and checking for the mere presence of an Authorization
# header.
if 'Authorization' not in request.headers:
if hasattr(request, 'url'):
signed_url = 'Signature=' in request.url
elif hasattr(request, 'requestline'):
signed_url = 'Signature=' in request.path
key = self.backend.get_key(bucket_name, key_name)
if key and not key.acl.public_read:
return 403, {}, ""
if key:
if not key.acl.public_read and not signed_url:
return 403, {}, ""
if hasattr(request, 'body'):
# Boto

View File

@ -77,6 +77,7 @@ class Subscription(BaseModel):
self.protocol = protocol
self.arn = make_arn_for_subscription(self.topic.arn)
self.attributes = {}
self.confirmed = False
def publish(self, message, message_id):
if self.protocol == 'sqs':
@ -172,12 +173,18 @@ class SNSBackend(BaseBackend):
self.applications = {}
self.platform_endpoints = {}
self.region_name = region_name
self.sms_attributes = {}
self.opt_out_numbers = ['+447420500600', '+447420505401', '+447632960543', '+447632960028', '+447700900149', '+447700900550', '+447700900545', '+447700900907']
self.permissions = {}
def reset(self):
region_name = self.region_name
self.__dict__ = {}
self.__init__(region_name)
def update_sms_attributes(self, attrs):
self.sms_attributes.update(attrs)
def create_topic(self, name):
topic = Topic(name, self)
self.topics[topic.arn] = topic

View File

@ -1,5 +1,7 @@
from __future__ import unicode_literals
import json
import re
from collections import defaultdict
from moto.core.responses import BaseResponse
from moto.core.utils import camelcase_to_underscores
@ -7,11 +9,17 @@ from .models import sns_backends
class SNSResponse(BaseResponse):
SMS_ATTR_REGEX = re.compile(r'^attributes\.entry\.(?P<index>\d+)\.(?P<type>key|value)$')
OPT_OUT_PHONE_NUMBER_REGEX = re.compile(r'^\+?\d+$')
@property
def backend(self):
return sns_backends[self.region]
def _error(self, code, message, sender='Sender'):
template = self.response_template(ERROR_RESPONSE)
return template.render(code=code, message=message, sender=sender)
def _get_attributes(self):
attributes = self._get_list_prefix('Attributes.entry')
return dict(
@ -459,6 +467,131 @@ class SNSResponse(BaseResponse):
template = self.response_template(SET_SUBSCRIPTION_ATTRIBUTES_TEMPLATE)
return template.render()
def set_sms_attributes(self):
# attributes.entry.1.key
# attributes.entry.1.value
# to
# 1: {key:X, value:Y}
temp_dict = defaultdict(dict)
for key, value in self.querystring.items():
match = self.SMS_ATTR_REGEX.match(key)
if match is not None:
temp_dict[match.group('index')][match.group('type')] = value[0]
# 1: {key:X, value:Y}
# to
# X: Y
# All of this, just to take into account when people provide invalid stuff.
result = {}
for item in temp_dict.values():
if 'key' in item and 'value' in item:
result[item['key']] = item['value']
self.backend.update_sms_attributes(result)
template = self.response_template(SET_SMS_ATTRIBUTES_TEMPLATE)
return template.render()
def get_sms_attributes(self):
filter_list = set()
for key, value in self.querystring.items():
if key.startswith('attributes.member.1'):
filter_list.add(value[0])
if len(filter_list) > 0:
result = {k: v for k, v in self.backend.sms_attributes.items() if k in filter_list}
else:
result = self.backend.sms_attributes
template = self.response_template(GET_SMS_ATTRIBUTES_TEMPLATE)
return template.render(attributes=result)
def check_if_phone_number_is_opted_out(self):
number = self._get_param('phoneNumber')
if self.OPT_OUT_PHONE_NUMBER_REGEX.match(number) is None:
error_response = self._error(
code='InvalidParameter',
message='Invalid parameter: PhoneNumber Reason: input incorrectly formatted'
)
return error_response, dict(status=400)
# There should be a nicer way to set if a nubmer has opted out
template = self.response_template(CHECK_IF_OPTED_OUT_TEMPLATE)
return template.render(opt_out=str(number.endswith('99')).lower())
def list_phone_numbers_opted_out(self):
template = self.response_template(LIST_OPTOUT_TEMPLATE)
return template.render(opt_outs=self.backend.opt_out_numbers)
def opt_in_phone_number(self):
number = self._get_param('phoneNumber')
try:
self.backend.opt_out_numbers.remove(number)
except ValueError:
pass
template = self.response_template(OPT_IN_NUMBER_TEMPLATE)
return template.render()
def add_permission(self):
arn = self._get_param('TopicArn')
label = self._get_param('Label')
accounts = self._get_multi_param('AWSAccountId.member.')
action = self._get_multi_param('ActionName.member.')
if arn not in self.backend.topics:
error_response = self._error('NotFound', 'Topic does not exist')
return error_response, dict(status=404)
key = (arn, label)
self.backend.permissions[key] = {'accounts': accounts, 'action': action}
template = self.response_template(ADD_PERMISSION_TEMPLATE)
return template.render()
def remove_permission(self):
arn = self._get_param('TopicArn')
label = self._get_param('Label')
if arn not in self.backend.topics:
error_response = self._error('NotFound', 'Topic does not exist')
return error_response, dict(status=404)
try:
key = (arn, label)
del self.backend.permissions[key]
except KeyError:
pass
template = self.response_template(DEL_PERMISSION_TEMPLATE)
return template.render()
def confirm_subscription(self):
arn = self._get_param('TopicArn')
if arn not in self.backend.topics:
error_response = self._error('NotFound', 'Topic does not exist')
return error_response, dict(status=404)
# Once Tokens are stored by the `subscribe` endpoint and distributed
# to the client somehow, then we can check validity of tokens
# presented to this method. The following code works, all thats
# needed is to perform a token check and assign that value to the
# `already_subscribed` variable.
#
# token = self._get_param('Token')
# auth = self._get_param('AuthenticateOnUnsubscribe')
# if already_subscribed:
# error_response = self._error(
# code='AuthorizationError',
# message='Subscription already confirmed'
# )
# return error_response, dict(status=400)
template = self.response_template(CONFIRM_SUBSCRIPTION_TEMPLATE)
return template.render(sub_arn='{0}:68762e72-e9b1-410a-8b3b-903da69ee1d5'.format(arn))
CREATE_TOPIC_TEMPLATE = """<CreateTopicResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<CreateTopicResult>
@ -758,3 +891,85 @@ SET_SUBSCRIPTION_ATTRIBUTES_TEMPLATE = """<SetSubscriptionAttributesResponse xml
<RequestId>a8763b99-33a7-11df-a9b7-05d48da6f042</RequestId>
</ResponseMetadata>
</SetSubscriptionAttributesResponse>"""
SET_SMS_ATTRIBUTES_TEMPLATE = """<SetSMSAttributesResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<SetSMSAttributesResult/>
<ResponseMetadata>
<RequestId>26332069-c04a-5428-b829-72524b56a364</RequestId>
</ResponseMetadata>
</SetSMSAttributesResponse>"""
GET_SMS_ATTRIBUTES_TEMPLATE = """<GetSMSAttributesResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<GetSMSAttributesResult>
<attributes>
{% for name, value in attributes.items() %}
<entry>
<key>{{ name }}</key>
<value>{{ value }}</value>
</entry>
{% endfor %}
</attributes>
</GetSMSAttributesResult>
<ResponseMetadata>
<RequestId>287f9554-8db3-5e66-8abc-c76f0186db7e</RequestId>
</ResponseMetadata>
</GetSMSAttributesResponse>"""
CHECK_IF_OPTED_OUT_TEMPLATE = """<CheckIfPhoneNumberIsOptedOutResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<CheckIfPhoneNumberIsOptedOutResult>
<isOptedOut>{{ opt_out }}</isOptedOut>
</CheckIfPhoneNumberIsOptedOutResult>
<ResponseMetadata>
<RequestId>287f9554-8db3-5e66-8abc-c76f0186db7e</RequestId>
</ResponseMetadata>
</CheckIfPhoneNumberIsOptedOutResponse>"""
ERROR_RESPONSE = """<ErrorResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<Error>
<Type>{{ sender }}</Type>
<Code>{{ code }}</Code>
<Message>{{ message }}</Message>
</Error>
<RequestId>9dd01905-5012-5f99-8663-4b3ecd0dfaef</RequestId>
</ErrorResponse>"""
LIST_OPTOUT_TEMPLATE = """<ListPhoneNumbersOptedOutResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ListPhoneNumbersOptedOutResult>
<phoneNumbers>
{% for item in opt_outs %}
<member>{{ item }}</member>
{% endfor %}
</phoneNumbers>
</ListPhoneNumbersOptedOutResult>
<ResponseMetadata>
<RequestId>985e196d-a237-51b6-b33a-4b5601276b38</RequestId>
</ResponseMetadata>
</ListPhoneNumbersOptedOutResponse>"""
OPT_IN_NUMBER_TEMPLATE = """<OptInPhoneNumberResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<OptInPhoneNumberResult/>
<ResponseMetadata>
<RequestId>4c61842c-0796-50ef-95ac-d610c0bc8cf8</RequestId>
</ResponseMetadata>
</OptInPhoneNumberResponse>"""
ADD_PERMISSION_TEMPLATE = """<AddPermissionResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ResponseMetadata>
<RequestId>c046e713-c5ff-5888-a7bc-b52f0e4f1299</RequestId>
</ResponseMetadata>
</AddPermissionResponse>"""
DEL_PERMISSION_TEMPLATE = """<RemovePermissionResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ResponseMetadata>
<RequestId>e767cc9f-314b-5e1b-b283-9ea3fd4e38a3</RequestId>
</ResponseMetadata>
</RemovePermissionResponse>"""
CONFIRM_SUBSCRIPTION_TEMPLATE = """<ConfirmSubscriptionResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<ConfirmSubscriptionResult>
<SubscriptionArn>{{ sub_arn }}</SubscriptionArn>
</ConfirmSubscriptionResult>
<ResponseMetadata>
<RequestId>16eb4dde-7b3c-5b3e-a22a-1fe2a92d3293</RequestId>
</ResponseMetadata>
</ConfirmSubscriptionResponse>"""

View File

@ -12,10 +12,7 @@ import boto.sqs
from moto.core import BaseBackend, BaseModel
from moto.core.utils import camelcase_to_underscores, get_random_message_id, unix_time, unix_time_millis
from .utils import generate_receipt_handle
from .exceptions import (
ReceiptHandleIsInvalid,
MessageNotInflight
)
from .exceptions import ReceiptHandleIsInvalid, MessageNotInflight, MessageAttributesInvalid
DEFAULT_ACCOUNT_ID = 123456789012
DEFAULT_SENDER_ID = "AIDAIT2UOQQY3AUEKVGXU"
@ -151,8 +148,12 @@ class Queue(BaseModel):
camelcase_attributes = ['ApproximateNumberOfMessages',
'ApproximateNumberOfMessagesDelayed',
'ApproximateNumberOfMessagesNotVisible',
'ContentBasedDeduplication',
'CreatedTimestamp',
'DelaySeconds',
'FifoQueue',
'KmsDataKeyReusePeriodSeconds',
'KmsMasterKeyId',
'LastModifiedTimestamp',
'MaximumMessageSize',
'MessageRetentionPeriod',
@ -161,25 +162,35 @@ class Queue(BaseModel):
'VisibilityTimeout',
'WaitTimeSeconds']
def __init__(self, name, visibility_timeout, wait_time_seconds, region):
def __init__(self, name, region, **kwargs):
self.name = name
self.visibility_timeout = visibility_timeout or 30
self.visibility_timeout = int(kwargs.get('VisibilityTimeout', 30))
self.region = region
# wait_time_seconds will be set to immediate return messages
self.wait_time_seconds = int(wait_time_seconds) if wait_time_seconds else 0
self._messages = []
now = unix_time()
# kwargs can also have:
# [Policy, RedrivePolicy]
self.fifo_queue = kwargs.get('FifoQueue', 'false') == 'true'
self.content_based_deduplication = kwargs.get('ContentBasedDeduplication', 'false') == 'true'
self.kms_master_key_id = kwargs.get('KmsMasterKeyId', 'alias/aws/sqs')
self.kms_data_key_reuse_period_seconds = int(kwargs.get('KmsDataKeyReusePeriodSeconds', 300))
self.created_timestamp = now
self.delay_seconds = 0
self.delay_seconds = int(kwargs.get('DelaySeconds', 0))
self.last_modified_timestamp = now
self.maximum_message_size = 64 << 10
self.message_retention_period = 86400 * 4 # four days
self.queue_arn = 'arn:aws:sqs:{0}:123456789012:{1}'.format(
self.region, self.name)
self.receive_message_wait_time_seconds = 0
self.maximum_message_size = int(kwargs.get('MaximumMessageSize', 64 << 10))
self.message_retention_period = int(kwargs.get('MessageRetentionPeriod', 86400 * 4)) # four days
self.queue_arn = 'arn:aws:sqs:{0}:123456789012:{1}'.format(self.region, self.name)
self.receive_message_wait_time_seconds = int(kwargs.get('ReceiveMessageWaitTimeSeconds', 0))
# wait_time_seconds will be set to immediate return messages
self.wait_time_seconds = int(kwargs.get('WaitTimeSeconds', 0))
# Check some conditions
if self.fifo_queue and not self.name.endswith('.fifo'):
raise MessageAttributesInvalid('Queue name must end in .fifo for FIFO queues')
@classmethod
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
@ -188,8 +199,8 @@ class Queue(BaseModel):
sqs_backend = sqs_backends[region_name]
return sqs_backend.create_queue(
name=properties['QueueName'],
visibility_timeout=properties.get('VisibilityTimeout'),
wait_time_seconds=properties.get('WaitTimeSeconds')
region=region_name,
**properties
)
@classmethod
@ -233,8 +244,10 @@ class Queue(BaseModel):
def attributes(self):
result = {}
for attribute in self.camelcase_attributes:
result[attribute] = getattr(
self, camelcase_to_underscores(attribute))
attr = getattr(self, camelcase_to_underscores(attribute))
if isinstance(attr, bool):
attr = str(attr).lower()
result[attribute] = attr
return result
def url(self, request_url):
@ -268,11 +281,14 @@ class SQSBackend(BaseBackend):
self.__dict__ = {}
self.__init__(region_name)
def create_queue(self, name, visibility_timeout, wait_time_seconds):
def create_queue(self, name, **kwargs):
queue = self.queues.get(name)
if queue is None:
queue = Queue(name, visibility_timeout,
wait_time_seconds, self.region_name)
try:
kwargs.pop('region')
except KeyError:
pass
queue = Queue(name, region=self.region_name, **kwargs)
self.queues[name] = queue
return queue

View File

@ -28,8 +28,7 @@ class SQSResponse(BaseResponse):
@property
def attribute(self):
if not hasattr(self, '_attribute'):
self._attribute = dict([(a['name'], a['value'])
for a in self._get_list_prefix('Attribute')])
self._attribute = self._get_map_prefix('Attribute', key_end='Name', value_end='Value')
return self._attribute
def _get_queue_name(self):
@ -58,17 +57,25 @@ class SQSResponse(BaseResponse):
return 404, headers, ERROR_INEXISTENT_QUEUE
return status_code, headers, body
def _error(self, code, message, status=400):
template = self.response_template(ERROR_TEMPLATE)
return template.render(code=code, message=message), dict(status=status)
def create_queue(self):
request_url = urlparse(self.uri)
queue_name = self.querystring.get("QueueName")[0]
queue = self.sqs_backend.create_queue(queue_name, visibility_timeout=self.attribute.get('VisibilityTimeout'),
wait_time_seconds=self.attribute.get('WaitTimeSeconds'))
queue_name = self._get_param("QueueName")
try:
queue = self.sqs_backend.create_queue(queue_name, **self.attribute)
except MessageAttributesInvalid as e:
return self._error('InvalidParameterValue', e.description)
template = self.response_template(CREATE_QUEUE_RESPONSE)
return template.render(queue=queue, request_url=request_url)
def get_queue_url(self):
request_url = urlparse(self.uri)
queue_name = self.querystring.get("QueueName")[0]
queue_name = self._get_param("QueueName")
queue = self.sqs_backend.get_queue(queue_name)
if queue:
template = self.response_template(GET_QUEUE_URL_RESPONSE)
@ -78,14 +85,14 @@ class SQSResponse(BaseResponse):
def list_queues(self):
request_url = urlparse(self.uri)
queue_name_prefix = self.querystring.get("QueueNamePrefix", [None])[0]
queue_name_prefix = self._get_param('QueueNamePrefix')
queues = self.sqs_backend.list_queues(queue_name_prefix)
template = self.response_template(LIST_QUEUES_RESPONSE)
return template.render(queues=queues, request_url=request_url)
def change_message_visibility(self):
queue_name = self._get_queue_name()
receipt_handle = self.querystring.get("ReceiptHandle")[0]
receipt_handle = self._get_param('ReceiptHandle')
try:
visibility_timeout = self._get_validated_visibility_timeout()
@ -111,19 +118,15 @@ class SQSResponse(BaseResponse):
return template.render(queue=queue)
def set_queue_attributes(self):
# TODO validate self.get_param('QueueUrl')
queue_name = self._get_queue_name()
if "Attribute.Name" in self.querystring:
key = camelcase_to_underscores(
self.querystring.get("Attribute.Name")[0])
value = self.querystring.get("Attribute.Value")[0]
self.sqs_backend.set_queue_attribute(queue_name, key, value)
for a in self._get_list_prefix("Attribute"):
key = camelcase_to_underscores(a["name"])
value = a["value"]
for key, value in self.attribute.items():
key = camelcase_to_underscores(key)
self.sqs_backend.set_queue_attribute(queue_name, key, value)
return SET_QUEUE_ATTRIBUTE_RESPONSE
def delete_queue(self):
# TODO validate self.get_param('QueueUrl')
queue_name = self._get_queue_name()
queue = self.sqs_backend.delete_queue(queue_name)
if not queue:
@ -133,17 +136,12 @@ class SQSResponse(BaseResponse):
return template.render(queue=queue)
def send_message(self):
message = self.querystring.get("MessageBody")[0]
delay_seconds = self.querystring.get('DelaySeconds')
message = self._get_param('MessageBody')
delay_seconds = int(self._get_param('DelaySeconds', 0))
if len(message) > MAXIMUM_MESSAGE_LENGTH:
return ERROR_TOO_LONG_RESPONSE, dict(status=400)
if delay_seconds:
delay_seconds = int(delay_seconds[0])
else:
delay_seconds = 0
try:
message_attributes = parse_message_attributes(self.querystring)
except MessageAttributesInvalid as e:
@ -470,3 +468,13 @@ ERROR_INEXISTENT_QUEUE = """<ErrorResponse xmlns="http://queue.amazonaws.com/doc
</Error>
<RequestId>b8bc806b-fa6b-53b5-8be8-cfa2f9836bc3</RequestId>
</ErrorResponse>"""
ERROR_TEMPLATE = """<ErrorResponse xmlns="http://queue.amazonaws.com/doc/2012-11-05/">
<Error>
<Type>Sender</Type>
<Code>{{ code }}</Code>
<Message>{{ message }}</Message>
<Detail/>
</Error>
<RequestId>6fde8d1e-52cd-4581-8cd9-c512f4c64223</RequestId>
</ErrorResponse>"""

View File

@ -24,7 +24,7 @@ extras_require = {
setup(
name='moto',
version='1.1.12',
version='1.1.13',
description='A library that allows your python tests to easily'
' mock out the boto library',
author='Steve Pulec',

View File

@ -0,0 +1,94 @@
from __future__ import unicode_literals
import boto3
from botocore.exceptions import ClientError
import sure # noqa
from moto import mock_cloudwatch
@mock_cloudwatch
def test_put_list_dashboard():
client = boto3.client('cloudwatch', region_name='eu-central-1')
widget = '{"widgets": [{"type": "text", "x": 0, "y": 7, "width": 3, "height": 3, "properties": {"markdown": "Hello world"}}]}'
client.put_dashboard(DashboardName='test1', DashboardBody=widget)
resp = client.list_dashboards()
len(resp['DashboardEntries']).should.equal(1)
@mock_cloudwatch
def test_put_list_prefix_nomatch_dashboard():
client = boto3.client('cloudwatch', region_name='eu-central-1')
widget = '{"widgets": [{"type": "text", "x": 0, "y": 7, "width": 3, "height": 3, "properties": {"markdown": "Hello world"}}]}'
client.put_dashboard(DashboardName='test1', DashboardBody=widget)
resp = client.list_dashboards(DashboardNamePrefix='nomatch')
len(resp['DashboardEntries']).should.equal(0)
@mock_cloudwatch
def test_delete_dashboard():
client = boto3.client('cloudwatch', region_name='eu-central-1')
widget = '{"widgets": [{"type": "text", "x": 0, "y": 7, "width": 3, "height": 3, "properties": {"markdown": "Hello world"}}]}'
client.put_dashboard(DashboardName='test1', DashboardBody=widget)
client.put_dashboard(DashboardName='test2', DashboardBody=widget)
client.put_dashboard(DashboardName='test3', DashboardBody=widget)
client.delete_dashboards(DashboardNames=['test2', 'test1'])
resp = client.list_dashboards(DashboardNamePrefix='test3')
len(resp['DashboardEntries']).should.equal(1)
@mock_cloudwatch
def test_delete_dashboard_fail():
client = boto3.client('cloudwatch', region_name='eu-central-1')
widget = '{"widgets": [{"type": "text", "x": 0, "y": 7, "width": 3, "height": 3, "properties": {"markdown": "Hello world"}}]}'
client.put_dashboard(DashboardName='test1', DashboardBody=widget)
client.put_dashboard(DashboardName='test2', DashboardBody=widget)
client.put_dashboard(DashboardName='test3', DashboardBody=widget)
# Doesnt delete anything if all dashboards to be deleted do not exist
try:
client.delete_dashboards(DashboardNames=['test2', 'test1', 'test_no_match'])
except ClientError as err:
err.response['Error']['Code'].should.equal('ResourceNotFound')
else:
raise RuntimeError('Should of raised error')
resp = client.list_dashboards()
len(resp['DashboardEntries']).should.equal(3)
@mock_cloudwatch
def test_get_dashboard():
client = boto3.client('cloudwatch', region_name='eu-central-1')
widget = '{"widgets": [{"type": "text", "x": 0, "y": 7, "width": 3, "height": 3, "properties": {"markdown": "Hello world"}}]}'
client.put_dashboard(DashboardName='test1', DashboardBody=widget)
resp = client.get_dashboard(DashboardName='test1')
resp.should.contain('DashboardArn')
resp.should.contain('DashboardBody')
resp['DashboardName'].should.equal('test1')
@mock_cloudwatch
def test_get_dashboard_fail():
client = boto3.client('cloudwatch', region_name='eu-central-1')
try:
client.get_dashboard(DashboardName='test1')
except ClientError as err:
err.response['Error']['Code'].should.equal('ResourceNotFound')
else:
raise RuntimeError('Should of raised error')

View File

@ -884,6 +884,10 @@ def test_s3_object_in_public_bucket():
s3_anonymous.Object(key='file.txt', bucket_name='test-bucket').get()
exc.exception.response['Error']['Code'].should.equal('403')
params = {'Bucket': 'test-bucket','Key': 'file.txt'}
presigned_url = boto3.client('s3').generate_presigned_url('get_object', params, ExpiresIn=900)
response = requests.get(presigned_url)
assert response.status_code == 200
@mock_s3
def test_s3_object_in_private_bucket():

View File

@ -321,3 +321,30 @@ def test_publish_to_disabled_platform_endpoint():
MessageStructure="json",
TargetArn=endpoint_arn,
).should.throw(ClientError)
@mock_sns
def test_set_sms_attributes():
conn = boto3.client('sns', region_name='us-east-1')
conn.set_sms_attributes(attributes={'DefaultSMSType': 'Transactional', 'test': 'test'})
response = conn.get_sms_attributes()
response.should.contain('attributes')
response['attributes'].should.contain('DefaultSMSType')
response['attributes'].should.contain('test')
response['attributes']['DefaultSMSType'].should.equal('Transactional')
response['attributes']['test'].should.equal('test')
@mock_sns
def test_get_sms_attributes_filtered():
conn = boto3.client('sns', region_name='us-east-1')
conn.set_sms_attributes(attributes={'DefaultSMSType': 'Transactional', 'test': 'test'})
response = conn.get_sms_attributes(attributes=['DefaultSMSType'])
response.should.contain('attributes')
response['attributes'].should.contain('DefaultSMSType')
response['attributes'].should_not.contain('test')
response['attributes']['DefaultSMSType'].should.equal('Transactional')

View File

@ -34,6 +34,7 @@ def test_creating_subscription():
"ListSubscriptionsResult"]["Subscriptions"]
subscriptions.should.have.length_of(0)
@mock_sns_deprecated
def test_deleting_subscriptions_by_deleting_topic():
conn = boto.connect_sns()
@ -66,6 +67,7 @@ def test_deleting_subscriptions_by_deleting_topic():
"ListSubscriptionsResult"]["Subscriptions"]
subscriptions.should.have.length_of(0)
@mock_sns_deprecated
def test_getting_subscriptions_by_topic():
conn = boto.connect_sns()

View File

@ -37,6 +37,7 @@ def test_creating_subscription():
subscriptions = conn.list_subscriptions()["Subscriptions"]
subscriptions.should.have.length_of(0)
@mock_sns
def test_deleting_subscriptions_by_deleting_topic():
conn = boto3.client('sns', region_name='us-east-1')
@ -68,6 +69,7 @@ def test_deleting_subscriptions_by_deleting_topic():
subscriptions = conn.list_subscriptions()["Subscriptions"]
subscriptions.should.have.length_of(0)
@mock_sns
def test_getting_subscriptions_by_topic():
conn = boto3.client('sns', region_name='us-east-1')
@ -197,3 +199,67 @@ def test_set_subscription_attributes():
AttributeName='InvalidName',
AttributeValue='true'
)
@mock_sns
def test_check_not_opted_out():
conn = boto3.client('sns', region_name='us-east-1')
response = conn.check_if_phone_number_is_opted_out(phoneNumber='+447428545375')
response.should.contain('isOptedOut')
response['isOptedOut'].should.be(False)
@mock_sns
def test_check_opted_out():
# Phone number ends in 99 so is hardcoded in the endpoint to return opted
# out status
conn = boto3.client('sns', region_name='us-east-1')
response = conn.check_if_phone_number_is_opted_out(phoneNumber='+447428545399')
response.should.contain('isOptedOut')
response['isOptedOut'].should.be(True)
@mock_sns
def test_check_opted_out_invalid():
conn = boto3.client('sns', region_name='us-east-1')
# Invalid phone number
with assert_raises(ClientError):
conn.check_if_phone_number_is_opted_out(phoneNumber='+44742LALALA')
@mock_sns
def test_list_opted_out():
conn = boto3.client('sns', region_name='us-east-1')
response = conn.list_phone_numbers_opted_out()
response.should.contain('phoneNumbers')
len(response['phoneNumbers']).should.be.greater_than(0)
@mock_sns
def test_opt_in():
conn = boto3.client('sns', region_name='us-east-1')
response = conn.list_phone_numbers_opted_out()
current_len = len(response['phoneNumbers'])
assert current_len > 0
conn.opt_in_phone_number(phoneNumber=response['phoneNumbers'][0])
response = conn.list_phone_numbers_opted_out()
len(response['phoneNumbers']).should.be.greater_than(0)
len(response['phoneNumbers']).should.be.lower_than(current_len)
@mock_sns
def test_confirm_subscription():
conn = boto3.client('sns', region_name='us-east-1')
response = conn.create_topic(Name='testconfirm')
conn.confirm_subscription(
TopicArn=response['TopicArn'],
Token='2336412f37fb687f5d51e6e241d59b68c4e583a5cee0be6f95bbf97ab8d2441cf47b99e848408adaadf4c197e65f03473d53c4ba398f6abbf38ce2e8ebf7b4ceceb2cd817959bcde1357e58a2861b05288c535822eb88cac3db04f592285249971efc6484194fc4a4586147f16916692',
AuthenticateOnUnsubscribe='true'
)

View File

@ -129,3 +129,20 @@ def test_topic_paging():
response.shouldnt.have("NextToken")
topics_list.should.have.length_of(int(DEFAULT_PAGE_SIZE / 2))
@mock_sns
def test_add_remove_permissions():
conn = boto3.client('sns', region_name='us-east-1')
response = conn.create_topic(Name='testpermissions')
conn.add_permission(
TopicArn=response['TopicArn'],
Label='Test1234',
AWSAccountId=['999999999999'],
ActionName=['AddPermission']
)
conn.remove_permission(
TopicArn=response['TopicArn'],
Label='Test1234'
)

View File

@ -8,7 +8,6 @@ from boto.exception import SQSError
from boto.sqs.message import RawMessage, Message
import base64
import requests
import sure # noqa
import time
@ -18,6 +17,39 @@ import tests.backport_assert_raises # noqa
from nose.tools import assert_raises
@mock_sqs
def test_create_fifo_queue_fail():
sqs = boto3.client('sqs', region_name='us-east-1')
try:
sqs.create_queue(
QueueName='test-queue',
Attributes={
'FifoQueue': 'true',
}
)
except botocore.exceptions.ClientError as err:
err.response['Error']['Code'].should.equal('InvalidParameterValue')
else:
raise RuntimeError('Should of raised InvalidParameterValue Exception')
@mock_sqs
def test_create_fifo_queue():
sqs = boto3.client('sqs', region_name='us-east-1')
resp = sqs.create_queue(
QueueName='test-queue.fifo',
Attributes={
'FifoQueue': 'true',
}
)
queue_url = resp['QueueUrl']
response = sqs.get_queue_attributes(QueueUrl=queue_url)
response['Attributes'].should.contain('FifoQueue')
response['Attributes']['FifoQueue'].should.equal('true')
@mock_sqs
def test_create_queue():
sqs = boto3.resource('sqs', region_name='us-east-1')
@ -39,6 +71,7 @@ def test_get_inexistent_queue():
sqs.get_queue_by_name.when.called_with(
QueueName='nonexisting-queue').should.throw(botocore.exceptions.ClientError)
@mock_sqs
def test_message_send_without_attributes():
sqs = boto3.resource('sqs', region_name='us-east-1')
@ -56,6 +89,7 @@ def test_message_send_without_attributes():
messages = queue.receive_messages()
messages.should.have.length_of(1)
@mock_sqs
def test_message_send_with_attributes():
sqs = boto3.resource('sqs', region_name='us-east-1')
@ -229,6 +263,7 @@ def test_send_receive_message_without_attributes():
message1.shouldnt.have.key('MD5OfMessageAttributes')
message2.shouldnt.have.key('MD5OfMessageAttributes')
@mock_sqs
def test_send_receive_message_with_attributes():
sqs = boto3.resource('sqs', region_name='us-east-1')