Merge branch 'master' into feature/scaffold
This commit is contained in:
commit
aaa5f9ef6b
10
Dockerfile
10
Dockerfile
@ -1,11 +1,15 @@
|
||||
FROM python:2
|
||||
FROM alpine:3.6
|
||||
|
||||
ADD . /moto/
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
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
|
||||
|
12
Makefile
12
Makefile
@ -15,11 +15,21 @@ test: lint
|
||||
test_server:
|
||||
@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
|
||||
|
||||
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
|
||||
|
||||
scaffold:
|
||||
@pip install -r requirements-dev.txt > /dev/null
|
||||
@python scripts/scaffold.py
|
||||
|
@ -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 |
|
||||
|------------------------------------------------------------------------------|
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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
5
moto/cloudwatch/utils.py
Normal 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)
|
@ -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):
|
||||
|
@ -412,7 +412,8 @@ class Table(BaseModel):
|
||||
return None
|
||||
|
||||
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 = []
|
||||
if index_name:
|
||||
all_indexes = (self.global_indexes or []) + (self.indexes or [])
|
||||
@ -483,6 +484,13 @@ class Table(BaseModel):
|
||||
else:
|
||||
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:
|
||||
results.reverse()
|
||||
|
||||
@ -678,7 +686,7 @@ class DynamoDBBackend(BaseBackend):
|
||||
return table.get_item(hash_key, range_key)
|
||||
|
||||
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)
|
||||
if not table:
|
||||
return None, None
|
||||
@ -688,7 +696,7 @@ class DynamoDBBackend(BaseBackend):
|
||||
for range_value in range_value_dicts]
|
||||
|
||||
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):
|
||||
table = self.tables.get(table_name)
|
||||
|
@ -21,8 +21,8 @@ class DynamoHandler(BaseResponse):
|
||||
if match:
|
||||
return match.split(".")[1]
|
||||
|
||||
def error(self, type_, status=400):
|
||||
return status, self.response_headers, dynamo_json_dump({'__type': type_})
|
||||
def error(self, type_, message, status=400):
|
||||
return status, self.response_headers, dynamo_json_dump({'__type': type_, 'message': message})
|
||||
|
||||
def call_action(self):
|
||||
self.body = json.loads(self.body or '{}')
|
||||
@ -82,7 +82,7 @@ class DynamoHandler(BaseResponse):
|
||||
return dynamo_json_dump(table.describe())
|
||||
else:
|
||||
er = 'com.amazonaws.dynamodb.v20111205#ResourceInUseException'
|
||||
return self.error(er)
|
||||
return self.error(er, 'Resource in use')
|
||||
|
||||
def delete_table(self):
|
||||
name = self.body['TableName']
|
||||
@ -91,7 +91,7 @@ class DynamoHandler(BaseResponse):
|
||||
return dynamo_json_dump(table.describe())
|
||||
else:
|
||||
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
|
||||
return self.error(er)
|
||||
return self.error(er, 'Requested resource not found')
|
||||
|
||||
def tag_resource(self):
|
||||
tags = self.body['Tags']
|
||||
@ -120,7 +120,7 @@ class DynamoHandler(BaseResponse):
|
||||
return json.dumps({'Tags': tags_resp})
|
||||
except AttributeError:
|
||||
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
|
||||
return self.error(er)
|
||||
return self.error(er, 'Requested resource not found')
|
||||
|
||||
def update_table(self):
|
||||
name = self.body['TableName']
|
||||
@ -138,7 +138,7 @@ class DynamoHandler(BaseResponse):
|
||||
table = dynamodb_backend2.tables[name]
|
||||
except KeyError:
|
||||
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'))
|
||||
|
||||
def put_item(self):
|
||||
@ -190,7 +190,7 @@ class DynamoHandler(BaseResponse):
|
||||
name, item, expected, overwrite)
|
||||
except ValueError:
|
||||
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:
|
||||
item_dict = result.to_json()
|
||||
@ -198,7 +198,7 @@ class DynamoHandler(BaseResponse):
|
||||
return dynamo_json_dump(item_dict)
|
||||
else:
|
||||
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
|
||||
return self.error(er)
|
||||
return self.error(er, 'Requested resource not found')
|
||||
|
||||
def batch_write_item(self):
|
||||
table_batches = self.body['RequestItems']
|
||||
@ -235,15 +235,14 @@ class DynamoHandler(BaseResponse):
|
||||
item = dynamodb_backend2.get_item(name, key)
|
||||
except ValueError:
|
||||
er = 'com.amazon.coral.validate#ValidationException'
|
||||
return self.error(er, status=400)
|
||||
return self.error(er, 'Validation Exception')
|
||||
if item:
|
||||
item_dict = item.describe_attrs(attributes=None)
|
||||
item_dict['ConsumedCapacityUnits'] = 0.5
|
||||
return dynamo_json_dump(item_dict)
|
||||
else:
|
||||
# Item not found
|
||||
er = '{}'
|
||||
return self.error(er, status=200)
|
||||
return 200, self.response_headers, '{}'
|
||||
|
||||
def batch_get_item(self):
|
||||
table_batches = self.body['RequestItems']
|
||||
@ -277,11 +276,26 @@ class DynamoHandler(BaseResponse):
|
||||
name = self.body['TableName']
|
||||
# {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')
|
||||
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 = {}
|
||||
if key_condition_expression:
|
||||
value_alias_map = self.body['ExpressionAttributeValues']
|
||||
|
||||
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')
|
||||
if index_name:
|
||||
all_indexes = (table.global_indexes or []) + \
|
||||
@ -350,7 +364,7 @@ class DynamoHandler(BaseResponse):
|
||||
filter_kwargs[key] = value
|
||||
if hash_key_name is None:
|
||||
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][
|
||||
'AttributeValueList'][0]
|
||||
if len(key_conditions) == 1:
|
||||
@ -359,7 +373,7 @@ class DynamoHandler(BaseResponse):
|
||||
else:
|
||||
if range_key_name is None and not filter_kwargs:
|
||||
er = "com.amazon.coral.validate#ValidationException"
|
||||
return self.error(er)
|
||||
return self.error(er, 'Validation Exception')
|
||||
else:
|
||||
range_condition = key_conditions.get(range_key_name)
|
||||
if range_condition:
|
||||
@ -378,16 +392,20 @@ class DynamoHandler(BaseResponse):
|
||||
scan_index_forward = self.body.get("ScanIndexForward")
|
||||
items, scanned_count, last_evaluated_key = dynamodb_backend2.query(
|
||||
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:
|
||||
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
|
||||
return self.error(er)
|
||||
return self.error(er, 'Requested resource not found')
|
||||
|
||||
result = {
|
||||
"Count": len(items),
|
||||
"ConsumedCapacityUnits": 1,
|
||||
'ConsumedCapacity': {
|
||||
'TableName': name,
|
||||
'CapacityUnits': 1,
|
||||
},
|
||||
"ScannedCount": scanned_count
|
||||
}
|
||||
|
||||
if self.body.get('Select', '').upper() != 'COUNT':
|
||||
result["Items"] = [item.attrs for item in items]
|
||||
|
||||
@ -417,12 +435,15 @@ class DynamoHandler(BaseResponse):
|
||||
|
||||
if items is None:
|
||||
er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException'
|
||||
return self.error(er)
|
||||
return self.error(er, 'Requested resource not found')
|
||||
|
||||
result = {
|
||||
"Count": len(items),
|
||||
"Items": [item.attrs for item in items],
|
||||
"ConsumedCapacityUnits": 1,
|
||||
'ConsumedCapacity': {
|
||||
'TableName': name,
|
||||
'CapacityUnits': 1,
|
||||
},
|
||||
"ScannedCount": scanned_count
|
||||
}
|
||||
if last_evaluated_key is not None:
|
||||
@ -436,7 +457,7 @@ class DynamoHandler(BaseResponse):
|
||||
table = dynamodb_backend2.get_table(name)
|
||||
if not table:
|
||||
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)
|
||||
if item and return_values == 'ALL_OLD':
|
||||
@ -496,10 +517,10 @@ class DynamoHandler(BaseResponse):
|
||||
expected)
|
||||
except ValueError:
|
||||
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:
|
||||
er = 'com.amazonaws.dynamodb.v20111205#ValidationException'
|
||||
return self.error(er)
|
||||
return self.error(er, 'Validation Exception')
|
||||
|
||||
item_dict = item.to_json()
|
||||
item_dict['ConsumedCapacityUnits'] = 0.5
|
||||
|
12944
moto/iam/aws_managed_policies.py
Normal file
12944
moto/iam/aws_managed_policies.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,11 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
import base64
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
import pytz
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
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 .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id
|
||||
|
||||
@ -92,6 +94,20 @@ class ManagedPolicy(Policy):
|
||||
class AWSManagedPolicy(ManagedPolicy):
|
||||
"""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):
|
||||
"""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):
|
||||
|
||||
def __init__(self):
|
||||
|
@ -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
|
||||
@ -636,6 +642,8 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
||||
|
||||
storage_class = request.headers.get('x-amz-storage-class', 'STANDARD')
|
||||
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)
|
||||
|
||||
if 'acl' in query:
|
||||
@ -740,7 +748,7 @@ class ResponseObject(_TemplateEnvironmentMixin):
|
||||
if grants:
|
||||
return FakeAcl(grants)
|
||||
else:
|
||||
return get_canned_acl('private')
|
||||
return None
|
||||
|
||||
def _tagging_from_headers(self, headers):
|
||||
if headers.get('x-amz-tagging'):
|
||||
|
@ -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
|
||||
|
@ -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>"""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>"""
|
||||
|
63
scripts/update_managed_policies.py
Executable file
63
scripts/update_managed_policies.py
Executable 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))
|
2
setup.py
2
setup.py
@ -24,7 +24,7 @@ extras_require = {
|
||||
|
||||
setup(
|
||||
name='moto',
|
||||
version='1.1.11',
|
||||
version='1.1.13',
|
||||
description='A library that allows your python tests to easily'
|
||||
' mock out the boto library',
|
||||
author='Steve Pulec',
|
||||
|
94
tests/test_cloudwatch/test_cloudwatch_boto3.py
Normal file
94
tests/test_cloudwatch/test_cloudwatch_boto3.py
Normal 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')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -9,6 +9,7 @@ from moto import mock_dynamodb2, mock_dynamodb2_deprecated
|
||||
from moto.dynamodb2 import dynamodb_backend2
|
||||
from boto.exception import JSONResponseError
|
||||
from botocore.exceptions import ClientError
|
||||
from boto3.dynamodb.conditions import Key
|
||||
from tests.helpers import requires_boto_gte
|
||||
import tests.backport_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(
|
||||
'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'
|
||||
|
@ -525,8 +525,14 @@ def test_managed_policy():
|
||||
path='/mypolicy/',
|
||||
description='my user managed policy')
|
||||
|
||||
aws_policies = conn.list_policies(scope='AWS')['list_policies_response'][
|
||||
'list_policies_result']['policies']
|
||||
marker = 0
|
||||
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['policy_name'] for p in aws_policies))
|
||||
|
||||
@ -535,8 +541,14 @@ def test_managed_policy():
|
||||
set(['UserManagedPolicy']).should.equal(
|
||||
set(p['policy_name'] for p in user_policies))
|
||||
|
||||
all_policies = conn.list_policies()['list_policies_response'][
|
||||
'list_policies_result']['policies']
|
||||
marker = 0
|
||||
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 +
|
||||
user_policies).should.equal(set(p['policy_name'] for p in all_policies))
|
||||
|
||||
|
@ -870,7 +870,7 @@ def test_s3_object_in_public_bucket():
|
||||
s3 = boto3.resource('s3')
|
||||
bucket = s3.Bucket('test-bucket')
|
||||
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.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()
|
||||
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():
|
||||
|
@ -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')
|
||||
|
@ -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()
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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'
|
||||
)
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user