Merge branch 'master' into feature/scaffold

This commit is contained in:
Toshiya Kawasaki 2017-09-23 17:29:18 +09:00 committed by GitHub
commit aaa5f9ef6b
27 changed files with 14101 additions and 216 deletions

View File

@ -1,11 +1,15 @@
FROM python:2 FROM alpine:3.6
ADD . /moto/ ADD . /moto/
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
WORKDIR /moto/ WORKDIR /moto/
RUN pip install ".[server]" RUN apk add --no-cache python3 && \
python3 -m ensurepip && \
rm -r /usr/lib/python*/ensurepip && \
pip3 --no-cache-dir install --upgrade pip setuptools && \
pip3 --no-cache-dir install ".[server]"
CMD ["moto_server"] ENTRYPOINT ["/usr/bin/moto_server", "-H", "0.0.0.0"]
EXPOSE 5000 EXPOSE 5000

View File

@ -15,11 +15,21 @@ test: lint
test_server: test_server:
@TEST_SERVER_MODE=true nosetests -sv --with-coverage --cover-html ./tests/ @TEST_SERVER_MODE=true nosetests -sv --with-coverage --cover-html ./tests/
publish: aws_managed_policies:
scripts/update_managed_policies.py
upload_pypi_artifact:
python setup.py sdist bdist_wheel upload 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 tag `python setup.py --version`
git push origin `python setup.py --version` git push origin `python setup.py --version`
publish: upload_pypi_artifact build_dockerhub_image tag_github_release
scaffold: scaffold:
@pip install -r requirements-dev.txt > /dev/null @pip install -r requirements-dev.txt > /dev/null
@python scripts/scaffold.py @python scripts/scaffold.py

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 | | SES | @mock_ses | core endpoints done |
|------------------------------------------------------------------------------| |------------------------------------------------------------------------------|
| SNS | @mock_sns | core endpoints done | | SNS | @mock_sns | all endpoints done |
|------------------------------------------------------------------------------| |------------------------------------------------------------------------------|
| SQS | @mock_sqs | core 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 boto.ec2.cloudwatch
import datetime import datetime
from .utils import make_arn_for_dashboard
DEFAULT_ACCOUNT_ID = 123456789012
class Dimension(object): class Dimension(object):
@ -44,10 +49,34 @@ class MetricDatum(BaseModel):
'value']) for dimension in dimensions] '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): class CloudWatchBackend(BaseBackend):
def __init__(self): def __init__(self):
self.alarms = {} self.alarms = {}
self.dashboards = {}
self.metric_data = [] self.metric_data = []
def put_metric_alarm(self, name, namespace, metric_name, comparison_operator, evaluation_periods, 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): def get_all_metrics(self):
return self.metric_data 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): class LogGroup(BaseModel):

View File

@ -1,9 +1,18 @@
import json
from moto.core.responses import BaseResponse from moto.core.responses import BaseResponse
from .models import cloudwatch_backends from .models import cloudwatch_backends
class CloudWatchResponse(BaseResponse): 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): def put_metric_alarm(self):
name = self._get_param('AlarmName') name = self._get_param('AlarmName')
namespace = self._get_param('Namespace') namespace = self._get_param('Namespace')
@ -20,15 +29,14 @@ class CloudWatchResponse(BaseResponse):
insufficient_data_actions = self._get_multi_param( insufficient_data_actions = self._get_multi_param(
"InsufficientDataActions.member") "InsufficientDataActions.member")
unit = self._get_param('Unit') unit = self._get_param('Unit')
cloudwatch_backend = cloudwatch_backends[self.region] alarm = self.cloudwatch_backend.put_metric_alarm(name, namespace, metric_name,
alarm = cloudwatch_backend.put_metric_alarm(name, namespace, metric_name, comparison_operator,
comparison_operator, evaluation_periods, period,
evaluation_periods, period, threshold, statistic,
threshold, statistic, description, dimensions,
description, dimensions, alarm_actions, ok_actions,
alarm_actions, ok_actions, insufficient_data_actions,
insufficient_data_actions, unit)
unit)
template = self.response_template(PUT_METRIC_ALARM_TEMPLATE) template = self.response_template(PUT_METRIC_ALARM_TEMPLATE)
return template.render(alarm=alarm) return template.render(alarm=alarm)
@ -37,28 +45,26 @@ class CloudWatchResponse(BaseResponse):
alarm_name_prefix = self._get_param('AlarmNamePrefix') alarm_name_prefix = self._get_param('AlarmNamePrefix')
alarm_names = self._get_multi_param('AlarmNames.member') alarm_names = self._get_multi_param('AlarmNames.member')
state_value = self._get_param('StateValue') state_value = self._get_param('StateValue')
cloudwatch_backend = cloudwatch_backends[self.region]
if action_prefix: if action_prefix:
alarms = cloudwatch_backend.get_alarms_by_action_prefix( alarms = self.cloudwatch_backend.get_alarms_by_action_prefix(
action_prefix) action_prefix)
elif alarm_name_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) alarm_name_prefix)
elif alarm_names: 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: 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: else:
alarms = cloudwatch_backend.get_all_alarms() alarms = self.cloudwatch_backend.get_all_alarms()
template = self.response_template(DESCRIBE_ALARMS_TEMPLATE) template = self.response_template(DESCRIBE_ALARMS_TEMPLATE)
return template.render(alarms=alarms) return template.render(alarms=alarms)
def delete_alarms(self): def delete_alarms(self):
alarm_names = self._get_multi_param('AlarmNames.member') alarm_names = self._get_multi_param('AlarmNames.member')
cloudwatch_backend = cloudwatch_backends[self.region] self.cloudwatch_backend.delete_alarms(alarm_names)
cloudwatch_backend.delete_alarms(alarm_names)
template = self.response_template(DELETE_METRIC_ALARMS_TEMPLATE) template = self.response_template(DELETE_METRIC_ALARMS_TEMPLATE)
return template.render() return template.render()
@ -89,17 +95,77 @@ class CloudWatchResponse(BaseResponse):
dimension_index += 1 dimension_index += 1
metric_data.append([metric_name, value, dimensions]) metric_data.append([metric_name, value, dimensions])
metric_index += 1 metric_index += 1
cloudwatch_backend = cloudwatch_backends[self.region] self.cloudwatch_backend.put_metric_data(namespace, metric_data)
cloudwatch_backend.put_metric_data(namespace, metric_data)
template = self.response_template(PUT_METRIC_DATA_TEMPLATE) template = self.response_template(PUT_METRIC_DATA_TEMPLATE)
return template.render() return template.render()
def list_metrics(self): def list_metrics(self):
cloudwatch_backend = cloudwatch_backends[self.region] metrics = self.cloudwatch_backend.get_all_metrics()
metrics = cloudwatch_backend.get_all_metrics()
template = self.response_template(LIST_METRICS_TEMPLATE) template = self.response_template(LIST_METRICS_TEMPLATE)
return template.render(metrics=metrics) 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/"> PUT_METRIC_ALARM_TEMPLATE = """<PutMetricAlarmResponse xmlns="http://monitoring.amazonaws.com/doc/2010-08-01/">
<ResponseMetadata> <ResponseMetadata>
@ -199,3 +265,58 @@ LIST_METRICS_TEMPLATE = """<ListMetricsResponse xmlns="http://monitoring.amazona
</NextToken> </NextToken>
</ListMetricsResult> </ListMetricsResult>
</ListMetricsResponse>""" </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 param_index += 1
return results return results
def _get_map_prefix(self, param_prefix): def _get_map_prefix(self, param_prefix, key_end='.key', value_end='.value'):
results = {} results = {}
param_index = 1 param_index = 1
while 1: while 1:
@ -319,9 +319,9 @@ class BaseResponse(_TemplateEnvironmentMixin):
k, v = None, None k, v = None, None
for key, value in self.querystring.items(): for key, value in self.querystring.items():
if key.startswith(index_prefix): if key.startswith(index_prefix):
if key.endswith('.key'): if key.endswith(key_end):
k = value[0] k = value[0]
elif key.endswith('.value'): elif key.endswith(value_end):
v = value[0] v = value[0]
if not (k and v): if not (k and v):

View File

@ -412,7 +412,8 @@ class Table(BaseModel):
return None return None
def query(self, hash_key, range_comparison, range_objs, limit, def query(self, hash_key, range_comparison, range_objs, limit,
exclusive_start_key, scan_index_forward, index_name=None, **filter_kwargs): exclusive_start_key, scan_index_forward, projection_expression,
index_name=None, **filter_kwargs):
results = [] results = []
if index_name: if index_name:
all_indexes = (self.global_indexes or []) + (self.indexes or []) all_indexes = (self.global_indexes or []) + (self.indexes or [])
@ -483,6 +484,13 @@ class Table(BaseModel):
else: else:
results.sort(key=lambda item: item.range_key) results.sort(key=lambda item: item.range_key)
if projection_expression:
expressions = [x.strip() for x in projection_expression.split(',')]
for result in possible_results:
for attr in list(result.attrs):
if attr not in expressions:
result.attrs.pop(attr)
if scan_index_forward is False: if scan_index_forward is False:
results.reverse() results.reverse()
@ -678,7 +686,7 @@ class DynamoDBBackend(BaseBackend):
return table.get_item(hash_key, range_key) return table.get_item(hash_key, range_key)
def query(self, table_name, hash_key_dict, range_comparison, range_value_dicts, def query(self, table_name, hash_key_dict, range_comparison, range_value_dicts,
limit, exclusive_start_key, scan_index_forward, index_name=None, **filter_kwargs): limit, exclusive_start_key, scan_index_forward, projection_expression, index_name=None, **filter_kwargs):
table = self.tables.get(table_name) table = self.tables.get(table_name)
if not table: if not table:
return None, None return None, None
@ -688,7 +696,7 @@ class DynamoDBBackend(BaseBackend):
for range_value in range_value_dicts] for range_value in range_value_dicts]
return table.query(hash_key, range_comparison, range_values, limit, return table.query(hash_key, range_comparison, range_values, limit,
exclusive_start_key, scan_index_forward, index_name, **filter_kwargs) exclusive_start_key, scan_index_forward, projection_expression, index_name, **filter_kwargs)
def scan(self, table_name, filters, limit, exclusive_start_key): def scan(self, table_name, filters, limit, exclusive_start_key):
table = self.tables.get(table_name) table = self.tables.get(table_name)

View File

@ -21,8 +21,8 @@ class DynamoHandler(BaseResponse):
if match: if match:
return match.split(".")[1] return match.split(".")[1]
def error(self, type_, status=400): def error(self, type_, message, status=400):
return status, self.response_headers, dynamo_json_dump({'__type': type_}) return status, self.response_headers, dynamo_json_dump({'__type': type_, 'message': message})
def call_action(self): def call_action(self):
self.body = json.loads(self.body or '{}') self.body = json.loads(self.body or '{}')
@ -82,7 +82,7 @@ class DynamoHandler(BaseResponse):
return dynamo_json_dump(table.describe()) return dynamo_json_dump(table.describe())
else: else:
er = 'com.amazonaws.dynamodb.v20111205#ResourceInUseException' er = 'com.amazonaws.dynamodb.v20111205#ResourceInUseException'
return self.error(er) return self.error(er, 'Resource in use')
def delete_table(self): def delete_table(self):
name = self.body['TableName'] name = self.body['TableName']
@ -91,7 +91,7 @@ class DynamoHandler(BaseResponse):
return dynamo_json_dump(table.describe()) return dynamo_json_dump(table.describe())
else: else:
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
return self.error(er) return self.error(er, 'Requested resource not found')
def tag_resource(self): def tag_resource(self):
tags = self.body['Tags'] tags = self.body['Tags']
@ -120,7 +120,7 @@ class DynamoHandler(BaseResponse):
return json.dumps({'Tags': tags_resp}) return json.dumps({'Tags': tags_resp})
except AttributeError: except AttributeError:
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
return self.error(er) return self.error(er, 'Requested resource not found')
def update_table(self): def update_table(self):
name = self.body['TableName'] name = self.body['TableName']
@ -138,7 +138,7 @@ class DynamoHandler(BaseResponse):
table = dynamodb_backend2.tables[name] table = dynamodb_backend2.tables[name]
except KeyError: except KeyError:
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
return self.error(er) return self.error(er, 'Requested resource not found')
return dynamo_json_dump(table.describe(base_key='Table')) return dynamo_json_dump(table.describe(base_key='Table'))
def put_item(self): def put_item(self):
@ -190,7 +190,7 @@ class DynamoHandler(BaseResponse):
name, item, expected, overwrite) name, item, expected, overwrite)
except ValueError: except ValueError:
er = 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException' er = 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException'
return self.error(er) return self.error(er, 'A condition specified in the operation could not be evaluated.')
if result: if result:
item_dict = result.to_json() item_dict = result.to_json()
@ -198,7 +198,7 @@ class DynamoHandler(BaseResponse):
return dynamo_json_dump(item_dict) return dynamo_json_dump(item_dict)
else: else:
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
return self.error(er) return self.error(er, 'Requested resource not found')
def batch_write_item(self): def batch_write_item(self):
table_batches = self.body['RequestItems'] table_batches = self.body['RequestItems']
@ -235,15 +235,14 @@ class DynamoHandler(BaseResponse):
item = dynamodb_backend2.get_item(name, key) item = dynamodb_backend2.get_item(name, key)
except ValueError: except ValueError:
er = 'com.amazon.coral.validate#ValidationException' er = 'com.amazon.coral.validate#ValidationException'
return self.error(er, status=400) return self.error(er, 'Validation Exception')
if item: if item:
item_dict = item.describe_attrs(attributes=None) item_dict = item.describe_attrs(attributes=None)
item_dict['ConsumedCapacityUnits'] = 0.5 item_dict['ConsumedCapacityUnits'] = 0.5
return dynamo_json_dump(item_dict) return dynamo_json_dump(item_dict)
else: else:
# Item not found # Item not found
er = '{}' return 200, self.response_headers, '{}'
return self.error(er, status=200)
def batch_get_item(self): def batch_get_item(self):
table_batches = self.body['RequestItems'] table_batches = self.body['RequestItems']
@ -277,11 +276,26 @@ class DynamoHandler(BaseResponse):
name = self.body['TableName'] name = self.body['TableName']
# {u'KeyConditionExpression': u'#n0 = :v0', u'ExpressionAttributeValues': {u':v0': {u'S': u'johndoe'}}, u'ExpressionAttributeNames': {u'#n0': u'username'}} # {u'KeyConditionExpression': u'#n0 = :v0', u'ExpressionAttributeValues': {u':v0': {u'S': u'johndoe'}}, u'ExpressionAttributeNames': {u'#n0': u'username'}}
key_condition_expression = self.body.get('KeyConditionExpression') key_condition_expression = self.body.get('KeyConditionExpression')
projection_expression = self.body.get('ProjectionExpression')
expression_attribute_names = self.body.get('ExpressionAttributeNames')
if projection_expression and expression_attribute_names:
expressions = [x.strip() for x in projection_expression.split(',')]
for expression in expressions:
if expression in expression_attribute_names:
projection_expression = projection_expression.replace(expression, expression_attribute_names[expression])
filter_kwargs = {} filter_kwargs = {}
if key_condition_expression: if key_condition_expression:
value_alias_map = self.body['ExpressionAttributeValues'] value_alias_map = self.body['ExpressionAttributeValues']
table = dynamodb_backend2.get_table(name) table = dynamodb_backend2.get_table(name)
# If table does not exist
if table is None:
return self.error('com.amazonaws.dynamodb.v20120810#ResourceNotFoundException',
'Requested resource not found')
index_name = self.body.get('IndexName') index_name = self.body.get('IndexName')
if index_name: if index_name:
all_indexes = (table.global_indexes or []) + \ all_indexes = (table.global_indexes or []) + \
@ -350,7 +364,7 @@ class DynamoHandler(BaseResponse):
filter_kwargs[key] = value filter_kwargs[key] = value
if hash_key_name is None: if hash_key_name is None:
er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException" er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException"
return self.error(er) return self.error(er, 'Requested resource not found')
hash_key = key_conditions[hash_key_name][ hash_key = key_conditions[hash_key_name][
'AttributeValueList'][0] 'AttributeValueList'][0]
if len(key_conditions) == 1: if len(key_conditions) == 1:
@ -359,7 +373,7 @@ class DynamoHandler(BaseResponse):
else: else:
if range_key_name is None and not filter_kwargs: if range_key_name is None and not filter_kwargs:
er = "com.amazon.coral.validate#ValidationException" er = "com.amazon.coral.validate#ValidationException"
return self.error(er) return self.error(er, 'Validation Exception')
else: else:
range_condition = key_conditions.get(range_key_name) range_condition = key_conditions.get(range_key_name)
if range_condition: if range_condition:
@ -378,16 +392,20 @@ class DynamoHandler(BaseResponse):
scan_index_forward = self.body.get("ScanIndexForward") scan_index_forward = self.body.get("ScanIndexForward")
items, scanned_count, last_evaluated_key = dynamodb_backend2.query( items, scanned_count, last_evaluated_key = dynamodb_backend2.query(
name, hash_key, range_comparison, range_values, limit, name, hash_key, range_comparison, range_values, limit,
exclusive_start_key, scan_index_forward, index_name=index_name, **filter_kwargs) exclusive_start_key, scan_index_forward, projection_expression, index_name=index_name, **filter_kwargs)
if items is None: if items is None:
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
return self.error(er) return self.error(er, 'Requested resource not found')
result = { result = {
"Count": len(items), "Count": len(items),
"ConsumedCapacityUnits": 1, 'ConsumedCapacity': {
'TableName': name,
'CapacityUnits': 1,
},
"ScannedCount": scanned_count "ScannedCount": scanned_count
} }
if self.body.get('Select', '').upper() != 'COUNT': if self.body.get('Select', '').upper() != 'COUNT':
result["Items"] = [item.attrs for item in items] result["Items"] = [item.attrs for item in items]
@ -417,12 +435,15 @@ class DynamoHandler(BaseResponse):
if items is None: if items is None:
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
return self.error(er) return self.error(er, 'Requested resource not found')
result = { result = {
"Count": len(items), "Count": len(items),
"Items": [item.attrs for item in items], "Items": [item.attrs for item in items],
"ConsumedCapacityUnits": 1, 'ConsumedCapacity': {
'TableName': name,
'CapacityUnits': 1,
},
"ScannedCount": scanned_count "ScannedCount": scanned_count
} }
if last_evaluated_key is not None: if last_evaluated_key is not None:
@ -436,7 +457,7 @@ class DynamoHandler(BaseResponse):
table = dynamodb_backend2.get_table(name) table = dynamodb_backend2.get_table(name)
if not table: if not table:
er = 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException' er = 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException'
return self.error(er) return self.error(er, 'A condition specified in the operation could not be evaluated.')
item = dynamodb_backend2.delete_item(name, keys) item = dynamodb_backend2.delete_item(name, keys)
if item and return_values == 'ALL_OLD': if item and return_values == 'ALL_OLD':
@ -496,10 +517,10 @@ class DynamoHandler(BaseResponse):
expected) expected)
except ValueError: except ValueError:
er = 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException' er = 'com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException'
return self.error(er) return self.error(er, 'A condition specified in the operation could not be evaluated.')
except TypeError: except TypeError:
er = 'com.amazonaws.dynamodb.v20111205#ValidationException' er = 'com.amazonaws.dynamodb.v20111205#ValidationException'
return self.error(er) return self.error(er, 'Validation Exception')
item_dict = item.to_json() item_dict = item.to_json()
item_dict['ConsumedCapacityUnits'] = 0.5 item_dict['ConsumedCapacityUnits'] = 0.5

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import base64 import base64
from datetime import datetime from datetime import datetime
import json
import pytz import pytz
from moto.core import BaseBackend, BaseModel from moto.core import BaseBackend, BaseModel
from moto.core.utils import iso_8601_datetime_without_milliseconds from moto.core.utils import iso_8601_datetime_without_milliseconds
from .aws_managed_policies import aws_managed_policies_data
from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException
from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id
@ -92,6 +94,20 @@ class ManagedPolicy(Policy):
class AWSManagedPolicy(ManagedPolicy): class AWSManagedPolicy(ManagedPolicy):
"""AWS-managed policy.""" """AWS-managed policy."""
@classmethod
def from_data(cls, name, data):
return cls(name,
default_version_id=data.get('DefaultVersionId'),
path=data.get('Path'),
document=data.get('Document'))
# AWS defines some of its own managed policies and we periodically
# import them via `make aws_managed_policies`
aws_managed_policies = [
AWSManagedPolicy.from_data(name, d) for name, d
in json.loads(aws_managed_policies_data).items()]
class InlinePolicy(Policy): class InlinePolicy(Policy):
"""TODO: is this needed?""" """TODO: is this needed?"""
@ -388,115 +404,6 @@ class User(BaseModel):
) )
# predefine AWS managed policies
aws_managed_policies = [
AWSManagedPolicy(
'AmazonElasticMapReduceRole',
default_version_id='v6',
path='/service-role/',
document={
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Resource": "*",
"Action": [
"ec2:AuthorizeSecurityGroupEgress",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:CancelSpotInstanceRequests",
"ec2:CreateNetworkInterface",
"ec2:CreateSecurityGroup",
"ec2:CreateTags",
"ec2:DeleteNetworkInterface",
"ec2:DeleteSecurityGroup",
"ec2:DeleteTags",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeAccountAttributes",
"ec2:DescribeDhcpOptions",
"ec2:DescribeInstanceStatus",
"ec2:DescribeInstances",
"ec2:DescribeKeyPairs",
"ec2:DescribeNetworkAcls",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribePrefixLists",
"ec2:DescribeRouteTables",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSpotInstanceRequests",
"ec2:DescribeSpotPriceHistory",
"ec2:DescribeSubnets",
"ec2:DescribeVpcAttribute",
"ec2:DescribeVpcEndpoints",
"ec2:DescribeVpcEndpointServices",
"ec2:DescribeVpcs",
"ec2:DetachNetworkInterface",
"ec2:ModifyImageAttribute",
"ec2:ModifyInstanceAttribute",
"ec2:RequestSpotInstances",
"ec2:RevokeSecurityGroupEgress",
"ec2:RunInstances",
"ec2:TerminateInstances",
"ec2:DeleteVolume",
"ec2:DescribeVolumeStatus",
"ec2:DescribeVolumes",
"ec2:DetachVolume",
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListInstanceProfiles",
"iam:ListRolePolicies",
"iam:PassRole",
"s3:CreateBucket",
"s3:Get*",
"s3:List*",
"sdb:BatchPutAttributes",
"sdb:Select",
"sqs:CreateQueue",
"sqs:Delete*",
"sqs:GetQueue*",
"sqs:PurgeQueue",
"sqs:ReceiveMessage"
]
}]
}
),
AWSManagedPolicy(
'AmazonElasticMapReduceforEC2Role',
default_version_id='v2',
path='/service-role/',
document={
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Resource": "*",
"Action": [
"cloudwatch:*",
"dynamodb:*",
"ec2:Describe*",
"elasticmapreduce:Describe*",
"elasticmapreduce:ListBootstrapActions",
"elasticmapreduce:ListClusters",
"elasticmapreduce:ListInstanceGroups",
"elasticmapreduce:ListInstances",
"elasticmapreduce:ListSteps",
"kinesis:CreateStream",
"kinesis:DeleteStream",
"kinesis:DescribeStream",
"kinesis:GetRecords",
"kinesis:GetShardIterator",
"kinesis:MergeShards",
"kinesis:PutRecord",
"kinesis:SplitShard",
"rds:Describe*",
"s3:*",
"sdb:*",
"sns:*",
"sqs:*"
]
}]
}
)
]
# TODO: add more predefined AWS managed policies
class IAMBackend(BaseBackend): class IAMBackend(BaseBackend):
def __init__(self): def __init__(self):

View File

@ -547,9 +547,15 @@ class ResponseObject(_TemplateEnvironmentMixin):
# ACL and checking for the mere presence of an Authorization # ACL and checking for the mere presence of an Authorization
# header. # header.
if 'Authorization' not in request.headers: 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) 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'): if hasattr(request, 'body'):
# Boto # Boto
@ -636,6 +642,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
storage_class = request.headers.get('x-amz-storage-class', 'STANDARD') storage_class = request.headers.get('x-amz-storage-class', 'STANDARD')
acl = self._acl_from_headers(request.headers) acl = self._acl_from_headers(request.headers)
if acl is None:
acl = self.backend.get_bucket(bucket_name).acl
tagging = self._tagging_from_headers(request.headers) tagging = self._tagging_from_headers(request.headers)
if 'acl' in query: if 'acl' in query:
@ -740,7 +748,7 @@ class ResponseObject(_TemplateEnvironmentMixin):
if grants: if grants:
return FakeAcl(grants) return FakeAcl(grants)
else: else:
return get_canned_acl('private') return None
def _tagging_from_headers(self, headers): def _tagging_from_headers(self, headers):
if headers.get('x-amz-tagging'): if headers.get('x-amz-tagging'):

View File

@ -77,6 +77,7 @@ class Subscription(BaseModel):
self.protocol = protocol self.protocol = protocol
self.arn = make_arn_for_subscription(self.topic.arn) self.arn = make_arn_for_subscription(self.topic.arn)
self.attributes = {} self.attributes = {}
self.confirmed = False
def publish(self, message, message_id): def publish(self, message, message_id):
if self.protocol == 'sqs': if self.protocol == 'sqs':
@ -172,12 +173,18 @@ class SNSBackend(BaseBackend):
self.applications = {} self.applications = {}
self.platform_endpoints = {} self.platform_endpoints = {}
self.region_name = region_name 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): def reset(self):
region_name = self.region_name region_name = self.region_name
self.__dict__ = {} self.__dict__ = {}
self.__init__(region_name) self.__init__(region_name)
def update_sms_attributes(self, attrs):
self.sms_attributes.update(attrs)
def create_topic(self, name): def create_topic(self, name):
topic = Topic(name, self) topic = Topic(name, self)
self.topics[topic.arn] = topic self.topics[topic.arn] = topic

View File

@ -1,5 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json import json
import re
from collections import defaultdict
from moto.core.responses import BaseResponse from moto.core.responses import BaseResponse
from moto.core.utils import camelcase_to_underscores from moto.core.utils import camelcase_to_underscores
@ -7,11 +9,17 @@ from .models import sns_backends
class SNSResponse(BaseResponse): 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 @property
def backend(self): def backend(self):
return sns_backends[self.region] 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): def _get_attributes(self):
attributes = self._get_list_prefix('Attributes.entry') attributes = self._get_list_prefix('Attributes.entry')
return dict( return dict(
@ -459,6 +467,131 @@ class SNSResponse(BaseResponse):
template = self.response_template(SET_SUBSCRIPTION_ATTRIBUTES_TEMPLATE) template = self.response_template(SET_SUBSCRIPTION_ATTRIBUTES_TEMPLATE)
return template.render() 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/"> CREATE_TOPIC_TEMPLATE = """<CreateTopicResponse xmlns="http://sns.amazonaws.com/doc/2010-03-31/">
<CreateTopicResult> <CreateTopicResult>
@ -758,3 +891,85 @@ SET_SUBSCRIPTION_ATTRIBUTES_TEMPLATE = """<SetSubscriptionAttributesResponse xml
<RequestId>a8763b99-33a7-11df-a9b7-05d48da6f042</RequestId> <RequestId>a8763b99-33a7-11df-a9b7-05d48da6f042</RequestId>
</ResponseMetadata> </ResponseMetadata>
</SetSubscriptionAttributesResponse>""" </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 import BaseBackend, BaseModel
from moto.core.utils import camelcase_to_underscores, get_random_message_id, unix_time, unix_time_millis from moto.core.utils import camelcase_to_underscores, get_random_message_id, unix_time, unix_time_millis
from .utils import generate_receipt_handle from .utils import generate_receipt_handle
from .exceptions import ( from .exceptions import ReceiptHandleIsInvalid, MessageNotInflight, MessageAttributesInvalid
ReceiptHandleIsInvalid,
MessageNotInflight
)
DEFAULT_ACCOUNT_ID = 123456789012 DEFAULT_ACCOUNT_ID = 123456789012
DEFAULT_SENDER_ID = "AIDAIT2UOQQY3AUEKVGXU" DEFAULT_SENDER_ID = "AIDAIT2UOQQY3AUEKVGXU"
@ -151,8 +148,12 @@ class Queue(BaseModel):
camelcase_attributes = ['ApproximateNumberOfMessages', camelcase_attributes = ['ApproximateNumberOfMessages',
'ApproximateNumberOfMessagesDelayed', 'ApproximateNumberOfMessagesDelayed',
'ApproximateNumberOfMessagesNotVisible', 'ApproximateNumberOfMessagesNotVisible',
'ContentBasedDeduplication',
'CreatedTimestamp', 'CreatedTimestamp',
'DelaySeconds', 'DelaySeconds',
'FifoQueue',
'KmsDataKeyReusePeriodSeconds',
'KmsMasterKeyId',
'LastModifiedTimestamp', 'LastModifiedTimestamp',
'MaximumMessageSize', 'MaximumMessageSize',
'MessageRetentionPeriod', 'MessageRetentionPeriod',
@ -161,25 +162,35 @@ class Queue(BaseModel):
'VisibilityTimeout', 'VisibilityTimeout',
'WaitTimeSeconds'] 'WaitTimeSeconds']
def __init__(self, name, visibility_timeout, wait_time_seconds, region): def __init__(self, name, region, **kwargs):
self.name = name self.name = name
self.visibility_timeout = visibility_timeout or 30 self.visibility_timeout = int(kwargs.get('VisibilityTimeout', 30))
self.region = region 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 = [] self._messages = []
now = unix_time() 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.created_timestamp = now
self.delay_seconds = 0 self.delay_seconds = int(kwargs.get('DelaySeconds', 0))
self.last_modified_timestamp = now self.last_modified_timestamp = now
self.maximum_message_size = 64 << 10 self.maximum_message_size = int(kwargs.get('MaximumMessageSize', 64 << 10))
self.message_retention_period = 86400 * 4 # four days self.message_retention_period = int(kwargs.get('MessageRetentionPeriod', 86400 * 4)) # four days
self.queue_arn = 'arn:aws:sqs:{0}:123456789012:{1}'.format( self.queue_arn = 'arn:aws:sqs:{0}:123456789012:{1}'.format(self.region, self.name)
self.region, self.name) self.receive_message_wait_time_seconds = int(kwargs.get('ReceiveMessageWaitTimeSeconds', 0))
self.receive_message_wait_time_seconds = 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 @classmethod
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name): 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] sqs_backend = sqs_backends[region_name]
return sqs_backend.create_queue( return sqs_backend.create_queue(
name=properties['QueueName'], name=properties['QueueName'],
visibility_timeout=properties.get('VisibilityTimeout'), region=region_name,
wait_time_seconds=properties.get('WaitTimeSeconds') **properties
) )
@classmethod @classmethod
@ -233,8 +244,10 @@ class Queue(BaseModel):
def attributes(self): def attributes(self):
result = {} result = {}
for attribute in self.camelcase_attributes: for attribute in self.camelcase_attributes:
result[attribute] = getattr( attr = getattr(self, camelcase_to_underscores(attribute))
self, camelcase_to_underscores(attribute)) if isinstance(attr, bool):
attr = str(attr).lower()
result[attribute] = attr
return result return result
def url(self, request_url): def url(self, request_url):
@ -268,11 +281,14 @@ class SQSBackend(BaseBackend):
self.__dict__ = {} self.__dict__ = {}
self.__init__(region_name) 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) queue = self.queues.get(name)
if queue is None: if queue is None:
queue = Queue(name, visibility_timeout, try:
wait_time_seconds, self.region_name) kwargs.pop('region')
except KeyError:
pass
queue = Queue(name, region=self.region_name, **kwargs)
self.queues[name] = queue self.queues[name] = queue
return queue return queue

View File

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

@ -0,0 +1,63 @@
#!/usr/bin/env python
# This updates our local copies of AWS' managed policies
# Invoked via `make update_managed_policies`
#
# Credit goes to
# https://gist.github.com/gene1wood/55b358748be3c314f956
from botocore.exceptions import NoCredentialsError
from datetime import datetime
import boto3
import json
import sys
output_file = "./moto/iam/aws_managed_policies.py"
def json_serial(obj):
"""JSON serializer for objects not serializable by default json code"""
if isinstance(obj, datetime):
serial = obj.isoformat()
return serial
raise TypeError("Type not serializable")
client = boto3.client('iam')
policies = {}
paginator = client.get_paginator('list_policies')
try:
response_iterator = paginator.paginate(Scope='AWS')
for response in response_iterator:
for policy in response['Policies']:
policies[policy['PolicyName']] = policy
except NoCredentialsError:
print("USAGE:")
print("Put your AWS credentials into ~/.aws/credentials and run:")
print(__file__)
print("")
print("Or specify them on the command line:")
print("AWS_ACCESS_KEY_ID=your_personal_access_key AWS_SECRET_ACCESS_KEY=your_personal_secret {}".format(__file__))
print("")
sys.exit(1)
for policy_name in policies:
response = client.get_policy_version(
PolicyArn=policies[policy_name]['Arn'],
VersionId=policies[policy_name]['DefaultVersionId'])
for key in response['PolicyVersion']:
policies[policy_name][key] = response['PolicyVersion'][key]
with open(output_file, 'w') as f:
triple_quote = '\"\"\"'
f.write("# Imported via `make aws_managed_policies`\n")
f.write('aws_managed_policies_data = {}\n'.format(triple_quote))
f.write(json.dumps(policies,
sort_keys=True,
indent=4,
separators=(',', ': '),
default=json_serial))
f.write('{}\n'.format(triple_quote))

View File

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

@ -9,6 +9,7 @@ from moto import mock_dynamodb2, mock_dynamodb2_deprecated
from moto.dynamodb2 import dynamodb_backend2 from moto.dynamodb2 import dynamodb_backend2
from boto.exception import JSONResponseError from boto.exception import JSONResponseError
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from boto3.dynamodb.conditions import Key
from tests.helpers import requires_boto_gte from tests.helpers import requires_boto_gte
import tests.backport_assert_raises import tests.backport_assert_raises
from nose.tools import assert_raises from nose.tools import assert_raises
@ -181,3 +182,239 @@ def test_item_add_empty_string_exception():
ex.exception.response['Error']['Message'].should.equal( ex.exception.response['Error']['Message'].should.equal(
'One or more parameter values were invalid: An AttributeValue may not contain an empty string' 'One or more parameter values were invalid: An AttributeValue may not contain an empty string'
) )
@requires_boto_gte("2.9")
@mock_dynamodb2
def test_query_invalid_table():
conn = boto3.client('dynamodb',
region_name='us-west-2',
aws_access_key_id="ak",
aws_secret_access_key="sk")
try:
conn.query(TableName='invalid_table', KeyConditionExpression='index1 = :partitionkeyval', ExpressionAttributeValues={':partitionkeyval': {'S':'test'}})
except ClientError as exception:
assert exception.response['Error']['Code'] == "ResourceNotFoundException"
@requires_boto_gte("2.9")
@mock_dynamodb2
def test_scan_returns_consumed_capacity():
name = 'TestTable'
conn = boto3.client('dynamodb',
region_name='us-west-2',
aws_access_key_id="ak",
aws_secret_access_key="sk")
conn.create_table(TableName=name,
KeySchema=[{'AttributeName':'forum_name','KeyType':'HASH'}],
AttributeDefinitions=[{'AttributeName':'forum_name','AttributeType':'S'}],
ProvisionedThroughput={'ReadCapacityUnits':5,'WriteCapacityUnits':5})
conn.put_item(
TableName=name,
Item={
'forum_name': { 'S': 'LOLCat Forum' },
'subject': { 'S': 'Check this out!' },
'Body': { 'S': 'http://url_to_lolcat.gif'},
'SentBy': { 'S': "test" },
'ReceivedTime': { 'S': '12/9/2011 11:36:03 PM'},
}
)
response = conn.scan(
TableName=name,
)
assert 'ConsumedCapacity' in response
assert 'CapacityUnits' in response['ConsumedCapacity']
assert response['ConsumedCapacity']['TableName'] == name
@requires_boto_gte("2.9")
@mock_dynamodb2
def test_query_returns_consumed_capacity():
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
# Create the DynamoDB table.
table = dynamodb.create_table(
TableName='users',
KeySchema=[
{
'AttributeName': 'forum_name',
'KeyType': 'HASH'
},
{
'AttributeName': 'subject',
'KeyType': 'RANGE'
},
],
AttributeDefinitions=[
{
'AttributeName': 'forum_name',
'AttributeType': 'S'
},
{
'AttributeName': 'subject',
'AttributeType': 'S'
},
],
ProvisionedThroughput={
'ReadCapacityUnits': 5,
'WriteCapacityUnits': 5
}
)
table = dynamodb.Table('users')
table.put_item(Item={
'forum_name': 'the-key',
'subject': '123',
'body': 'some test message'
})
results = table.query(
KeyConditionExpression=Key('forum_name').eq(
'the-key')
)
assert 'ConsumedCapacity' in results
assert 'CapacityUnits' in results['ConsumedCapacity']
assert results['ConsumedCapacity']['CapacityUnits'] == 1
@mock_dynamodb2
def test_basic_projection_expressions():
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
# Create the DynamoDB table.
table = dynamodb.create_table(
TableName='users',
KeySchema=[
{
'AttributeName': 'forum_name',
'KeyType': 'HASH'
},
{
'AttributeName': 'subject',
'KeyType': 'RANGE'
},
],
AttributeDefinitions=[
{
'AttributeName': 'forum_name',
'AttributeType': 'S'
},
{
'AttributeName': 'subject',
'AttributeType': 'S'
},
],
ProvisionedThroughput={
'ReadCapacityUnits': 5,
'WriteCapacityUnits': 5
}
)
table = dynamodb.Table('users')
table.put_item(Item={
'forum_name': 'the-key',
'subject': '123',
'body': 'some test message'
})
table.put_item(Item={
'forum_name': 'not-the-key',
'subject': '123',
'body': 'some other test message'
})
# Test a query returning all items
results = table.query(
KeyConditionExpression=Key('forum_name').eq(
'the-key'),
ProjectionExpression='body, subject'
)
assert 'body' in results['Items'][0]
assert results['Items'][0]['body'] == 'some test message'
assert 'subject' in results['Items'][0]
table.put_item(Item={
'forum_name': 'the-key',
'subject': '1234',
'body': 'yet another test message'
})
results = table.query(
KeyConditionExpression=Key('forum_name').eq(
'the-key'),
ProjectionExpression='body'
)
assert 'body' in results['Items'][0]
assert results['Items'][0]['body'] == 'some test message'
assert 'body' in results['Items'][1]
assert results['Items'][1]['body'] == 'yet another test message'
@mock_dynamodb2
def test_basic_projection_expressions_with_attr_expression_names():
dynamodb = boto3.resource('dynamodb', region_name='us-east-1')
# Create the DynamoDB table.
table = dynamodb.create_table(
TableName='users',
KeySchema=[
{
'AttributeName': 'forum_name',
'KeyType': 'HASH'
},
{
'AttributeName': 'subject',
'KeyType': 'RANGE'
},
],
AttributeDefinitions=[
{
'AttributeName': 'forum_name',
'AttributeType': 'S'
},
{
'AttributeName': 'subject',
'AttributeType': 'S'
},
],
ProvisionedThroughput={
'ReadCapacityUnits': 5,
'WriteCapacityUnits': 5
}
)
table = dynamodb.Table('users')
table.put_item(Item={
'forum_name': 'the-key',
'subject': '123',
'body': 'some test message',
'attachment': 'something'
})
table.put_item(Item={
'forum_name': 'not-the-key',
'subject': '123',
'body': 'some other test message',
'attachment': 'something'
})
# Test a query returning all items
results = table.query(
KeyConditionExpression=Key('forum_name').eq(
'the-key'),
ProjectionExpression='#rl, #rt, subject',
ExpressionAttributeNames={
'#rl': 'body',
'#rt': 'attachment'
},
)
assert 'body' in results['Items'][0]
assert results['Items'][0]['body'] == 'some test message'
assert 'subject' in results['Items'][0]
assert results['Items'][0]['subject'] == '123'
assert 'attachment' in results['Items'][0]
assert results['Items'][0]['attachment'] == 'something'

View File

@ -525,8 +525,14 @@ def test_managed_policy():
path='/mypolicy/', path='/mypolicy/',
description='my user managed policy') description='my user managed policy')
aws_policies = conn.list_policies(scope='AWS')['list_policies_response'][ marker = 0
'list_policies_result']['policies'] aws_policies = []
while marker is not None:
response = conn.list_policies(scope='AWS', marker=marker)[
'list_policies_response']['list_policies_result']
for policy in response['policies']:
aws_policies.append(policy)
marker = response.get('marker')
set(p.name for p in aws_managed_policies).should.equal( set(p.name for p in aws_managed_policies).should.equal(
set(p['policy_name'] for p in aws_policies)) set(p['policy_name'] for p in aws_policies))
@ -535,8 +541,14 @@ def test_managed_policy():
set(['UserManagedPolicy']).should.equal( set(['UserManagedPolicy']).should.equal(
set(p['policy_name'] for p in user_policies)) set(p['policy_name'] for p in user_policies))
all_policies = conn.list_policies()['list_policies_response'][ marker = 0
'list_policies_result']['policies'] all_policies = []
while marker is not None:
response = conn.list_policies(marker=marker)[
'list_policies_response']['list_policies_result']
for policy in response['policies']:
all_policies.append(policy)
marker = response.get('marker')
set(p['policy_name'] for p in aws_policies + set(p['policy_name'] for p in aws_policies +
user_policies).should.equal(set(p['policy_name'] for p in all_policies)) user_policies).should.equal(set(p['policy_name'] for p in all_policies))

View File

@ -870,7 +870,7 @@ def test_s3_object_in_public_bucket():
s3 = boto3.resource('s3') s3 = boto3.resource('s3')
bucket = s3.Bucket('test-bucket') bucket = s3.Bucket('test-bucket')
bucket.create(ACL='public-read') bucket.create(ACL='public-read')
bucket.put_object(ACL='public-read', Body=b'ABCD', Key='file.txt') bucket.put_object(Body=b'ABCD', Key='file.txt')
s3_anonymous = boto3.resource('s3') s3_anonymous = boto3.resource('s3')
s3_anonymous.meta.client.meta.events.register('choose-signer.s3.*', disable_signing) s3_anonymous.meta.client.meta.events.register('choose-signer.s3.*', disable_signing)
@ -884,6 +884,10 @@ def test_s3_object_in_public_bucket():
s3_anonymous.Object(key='file.txt', bucket_name='test-bucket').get() s3_anonymous.Object(key='file.txt', bucket_name='test-bucket').get()
exc.exception.response['Error']['Code'].should.equal('403') 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 @mock_s3
def test_s3_object_in_private_bucket(): def test_s3_object_in_private_bucket():

View File

@ -321,3 +321,30 @@ def test_publish_to_disabled_platform_endpoint():
MessageStructure="json", MessageStructure="json",
TargetArn=endpoint_arn, TargetArn=endpoint_arn,
).should.throw(ClientError) ).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"] "ListSubscriptionsResult"]["Subscriptions"]
subscriptions.should.have.length_of(0) subscriptions.should.have.length_of(0)
@mock_sns_deprecated @mock_sns_deprecated
def test_deleting_subscriptions_by_deleting_topic(): def test_deleting_subscriptions_by_deleting_topic():
conn = boto.connect_sns() conn = boto.connect_sns()
@ -66,6 +67,7 @@ def test_deleting_subscriptions_by_deleting_topic():
"ListSubscriptionsResult"]["Subscriptions"] "ListSubscriptionsResult"]["Subscriptions"]
subscriptions.should.have.length_of(0) subscriptions.should.have.length_of(0)
@mock_sns_deprecated @mock_sns_deprecated
def test_getting_subscriptions_by_topic(): def test_getting_subscriptions_by_topic():
conn = boto.connect_sns() conn = boto.connect_sns()

View File

@ -37,6 +37,7 @@ def test_creating_subscription():
subscriptions = conn.list_subscriptions()["Subscriptions"] subscriptions = conn.list_subscriptions()["Subscriptions"]
subscriptions.should.have.length_of(0) subscriptions.should.have.length_of(0)
@mock_sns @mock_sns
def test_deleting_subscriptions_by_deleting_topic(): def test_deleting_subscriptions_by_deleting_topic():
conn = boto3.client('sns', region_name='us-east-1') 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 = conn.list_subscriptions()["Subscriptions"]
subscriptions.should.have.length_of(0) subscriptions.should.have.length_of(0)
@mock_sns @mock_sns
def test_getting_subscriptions_by_topic(): def test_getting_subscriptions_by_topic():
conn = boto3.client('sns', region_name='us-east-1') conn = boto3.client('sns', region_name='us-east-1')
@ -197,3 +199,67 @@ def test_set_subscription_attributes():
AttributeName='InvalidName', AttributeName='InvalidName',
AttributeValue='true' 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") response.shouldnt.have("NextToken")
topics_list.should.have.length_of(int(DEFAULT_PAGE_SIZE / 2)) 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 from boto.sqs.message import RawMessage, Message
import base64 import base64
import requests
import sure # noqa import sure # noqa
import time import time
@ -18,6 +17,39 @@ import tests.backport_assert_raises # noqa
from nose.tools import assert_raises 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 @mock_sqs
def test_create_queue(): def test_create_queue():
sqs = boto3.resource('sqs', region_name='us-east-1') 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( sqs.get_queue_by_name.when.called_with(
QueueName='nonexisting-queue').should.throw(botocore.exceptions.ClientError) QueueName='nonexisting-queue').should.throw(botocore.exceptions.ClientError)
@mock_sqs @mock_sqs
def test_message_send_without_attributes(): def test_message_send_without_attributes():
sqs = boto3.resource('sqs', region_name='us-east-1') sqs = boto3.resource('sqs', region_name='us-east-1')
@ -56,6 +89,7 @@ def test_message_send_without_attributes():
messages = queue.receive_messages() messages = queue.receive_messages()
messages.should.have.length_of(1) messages.should.have.length_of(1)
@mock_sqs @mock_sqs
def test_message_send_with_attributes(): def test_message_send_with_attributes():
sqs = boto3.resource('sqs', region_name='us-east-1') 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') message1.shouldnt.have.key('MD5OfMessageAttributes')
message2.shouldnt.have.key('MD5OfMessageAttributes') message2.shouldnt.have.key('MD5OfMessageAttributes')
@mock_sqs @mock_sqs
def test_send_receive_message_with_attributes(): def test_send_receive_message_with_attributes():
sqs = boto3.resource('sqs', region_name='us-east-1') sqs = boto3.resource('sqs', region_name='us-east-1')