diff --git a/.travis.yml b/.travis.yml index be2d97c86..3befdf927 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ matrix: env: BOTO_VERSION=2.36.0 install: - travis_retry pip install boto==$BOTO_VERSION + - travis_retry pip install boto3 - travis_retry pip install . - travis_retry pip install -r requirements-dev.txt - travis_retry pip install coveralls diff --git a/moto/__init__.py b/moto/__init__.py index 6aa89cad6..ee566ee68 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -3,7 +3,7 @@ import logging logging.getLogger('boto').setLevel(logging.CRITICAL) __title__ = 'moto' -__version__ = '0.4.7' +__version__ = '0.4.8' from .autoscaling import mock_autoscaling # flake8: noqa from .cloudformation import mock_cloudformation # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index de323a442..b45e0fa5e 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from moto.autoscaling import autoscaling_backend from moto.cloudwatch import cloudwatch_backend +from moto.cloudformation import cloudformation_backend from moto.dynamodb import dynamodb_backend from moto.dynamodb2 import dynamodb_backend2 from moto.ec2 import ec2_backend @@ -20,6 +21,7 @@ from moto.route53 import route53_backend BACKENDS = { 'autoscaling': autoscaling_backend, + 'cloudformation': cloudformation_backend, 'cloudwatch': cloudwatch_backend, 'dynamodb': dynamodb_backend, 'dynamodb2': dynamodb_backend2, diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py index fe0af6640..e0afa5fa3 100644 --- a/moto/cloudformation/responses.py +++ b/moto/cloudformation/responses.py @@ -161,7 +161,7 @@ LIST_STACKS_RESPONSE = """ {% for stack in stacks %} - {{ stack.id }} + {{ stack.stack_id }} {{ stack.status }} {{ stack.name }} 2011-05-23T15:47:44Z diff --git a/moto/dynamodb2/comparisons.py b/moto/dynamodb2/comparisons.py index 86f582179..808e120bc 100644 --- a/moto/dynamodb2/comparisons.py +++ b/moto/dynamodb2/comparisons.py @@ -1,12 +1,32 @@ from __future__ import unicode_literals # TODO add tests for all of these + +EQ_FUNCTION = lambda item_value, test_value: item_value == test_value +NE_FUNCTION = lambda item_value, test_value: item_value != test_value +LE_FUNCTION = lambda item_value, test_value: item_value <= test_value +LT_FUNCTION = lambda item_value, test_value: item_value < test_value +GE_FUNCTION = lambda item_value, test_value: item_value >= test_value +GT_FUNCTION = lambda item_value, test_value: item_value > test_value + COMPARISON_FUNCS = { - 'EQ': lambda item_value, test_value: item_value == test_value, - 'NE': lambda item_value, test_value: item_value != test_value, - 'LE': lambda item_value, test_value: item_value <= test_value, - 'LT': lambda item_value, test_value: item_value < test_value, - 'GE': lambda item_value, test_value: item_value >= test_value, - 'GT': lambda item_value, test_value: item_value > test_value, + 'EQ': EQ_FUNCTION, + '=': EQ_FUNCTION, + + 'NE': NE_FUNCTION, + '!=': NE_FUNCTION, + + 'LE': LE_FUNCTION, + '<=': LE_FUNCTION, + + 'LT': LT_FUNCTION, + '<': LT_FUNCTION, + + 'GE': GE_FUNCTION, + '>=': GE_FUNCTION, + + 'GT': GT_FUNCTION, + '>': GT_FUNCTION, + 'NULL': lambda item_value: item_value is None, 'NOT_NULL': lambda item_value: item_value is not None, 'CONTAINS': lambda item_value, test_value: test_value in item_value, diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py index 81b6a7f47..069d31229 100644 --- a/moto/dynamodb2/responses.py +++ b/moto/dynamodb2/responses.py @@ -228,28 +228,64 @@ class DynamoHandler(BaseResponse): def query(self): name = self.body['TableName'] - key_conditions = self.body['KeyConditions'] - hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name, key_conditions.keys()) - # hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name) - if hash_key_name is None: - er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException" - return self.error(er) - hash_key = key_conditions[hash_key_name]['AttributeValueList'][0] - if len(key_conditions) == 1: - range_comparison = None - range_values = [] - else: - if range_key_name is None: - er = "com.amazon.coral.validate#ValidationException" - return self.error(er) - else: - range_condition = key_conditions[range_key_name] - if range_condition: - range_comparison = range_condition['ComparisonOperator'] - range_values = range_condition['AttributeValueList'] + + # {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') + if key_condition_expression: + value_alias_map = self.body['ExpressionAttributeValues'] + + if " AND " in key_condition_expression: + expressions = key_condition_expression.split(" AND ", 1) + hash_key_expression = expressions[0] + # TODO implement more than one range expression and OR operators + range_key_expression = expressions[1].replace(")", "") + range_key_expression_components = range_key_expression.split() + range_comparison = range_key_expression_components[1] + if 'AND' in range_key_expression: + range_comparison = 'BETWEEN' + range_values = [ + value_alias_map[range_key_expression_components[2]], + value_alias_map[range_key_expression_components[4]], + ] + elif 'begins_with' in range_key_expression: + range_comparison = 'BEGINS_WITH' + range_values = [ + value_alias_map[range_key_expression_components[1]], + ] else: + range_values = [value_alias_map[range_key_expression_components[2]]] + else: + hash_key_expression = key_condition_expression + range_comparison = None + range_values = [] + + hash_key_value_alias = hash_key_expression.split("=")[1].strip() + hash_key = value_alias_map[hash_key_value_alias] + else: + # 'KeyConditions': {u'forum_name': {u'ComparisonOperator': u'EQ', u'AttributeValueList': [{u'S': u'the-key'}]}} + key_conditions = self.body.get('KeyConditions') + if key_conditions: + hash_key_name, range_key_name = dynamodb_backend2.get_table_keys_name(name, key_conditions.keys()) + if hash_key_name is None: + er = "'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException" + return self.error(er) + hash_key = key_conditions[hash_key_name]['AttributeValueList'][0] + if len(key_conditions) == 1: range_comparison = None range_values = [] + else: + if range_key_name is None: + er = "com.amazon.coral.validate#ValidationException" + return self.error(er) + else: + range_condition = key_conditions[range_key_name] + if range_condition: + range_comparison = range_condition['ComparisonOperator'] + range_values = range_condition['AttributeValueList'] + else: + range_comparison = None + range_values = [] + items, last_page = dynamodb_backend2.query(name, hash_key, range_comparison, range_values) if items is None: er = 'com.amazonaws.dynamodb.v20111205#ResourceNotFoundException' @@ -260,7 +296,7 @@ class DynamoHandler(BaseResponse): items = items[:limit] reversed = self.body.get("ScanIndexForward") - if reversed is not False: + if reversed is False: items.reverse() result = { diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 3f3fc4491..f800097d2 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -1,17 +1,17 @@ from __future__ import unicode_literals -from collections import defaultdict +import boto import copy -from datetime import datetime import itertools import re +import six -import boto +from collections import defaultdict +from datetime import datetime from boto.ec2.instance import Instance as BotoInstance, Reservation from boto.ec2.blockdevicemapping import BlockDeviceMapping, BlockDeviceType from boto.ec2.spotinstancerequest import SpotInstanceRequest as BotoSpotRequest from boto.ec2.launchspecification import LaunchSpecification -import six from moto.core import BaseBackend from moto.core.models import Model @@ -97,6 +97,10 @@ from .utils import ( ) +def utc_date_and_time(): + return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + + def validate_resource_ids(resource_ids): for resource_id in resource_ids: if not is_valid_resource_id(resource_id): @@ -309,14 +313,17 @@ class Instance(BotoInstance, TaggedEC2Resource): in_ec2_classic = not bool(self.subnet_id) self.key_name = kwargs.get("key_name") self.source_dest_check = "true" - self.launch_time = datetime.utcnow().isoformat() + self.launch_time = utc_date_and_time() associate_public_ip = kwargs.get("associate_public_ip", False) if in_ec2_classic: # If we are in EC2-Classic, autoassign a public IP associate_public_ip = True self.block_device_mapping = BlockDeviceMapping() - self.block_device_mapping['/dev/sda1'] = BlockDeviceType(volume_id=random_volume_id()) + # Default have an instance with root volume should you not wish to override with attach volume cmd. + # However this is a ghost volume and wont show up in get_all_volumes or snapshot-able. + self.block_device_mapping['/dev/sda1'] = BlockDeviceType(volume_id=random_volume_id(), status='attached', + attach_time=utc_date_and_time()) amis = self.ec2_backend.describe_images(filters={'image-id': image_id}) ami = amis[0] if amis else None @@ -343,6 +350,10 @@ class Instance(BotoInstance, TaggedEC2Resource): private_ip=kwargs.get("private_ip"), associate_public_ip=associate_public_ip) + @property + def get_block_device_mapping(self): + return self.block_device_mapping.items() + @property def private_ip(self): return self.nics[0].private_ip_address @@ -1349,6 +1360,7 @@ class SecurityGroupIngress(object): class VolumeAttachment(object): def __init__(self, volume, instance, device): self.volume = volume + self.attach_time = utc_date_and_time() self.instance = instance self.device = device @@ -1373,6 +1385,7 @@ class Volume(TaggedEC2Resource): self.id = volume_id self.size = size self.zone = zone + self.create_time = utc_date_and_time() self.attachment = None self.ec2_backend = ec2_backend @@ -1404,6 +1417,7 @@ class Snapshot(TaggedEC2Resource): self.id = snapshot_id self.volume = volume self.description = description + self.start_time = utc_date_and_time() self.create_volume_permission_groups = set() self.ec2_backend = ec2_backend @@ -1444,6 +1458,10 @@ class EBSBackend(object): return False volume.attachment = VolumeAttachment(volume, instance, device_path) + # Modify instance to capture mount of block device. + bdt = BlockDeviceType(volume_id=volume_id, status=volume.status, size=volume.size, + attach_time=utc_date_and_time()) + instance.block_device_mapping[device_path] = bdt return volume.attachment def detach_volume(self, volume_id, instance_id, device_path): diff --git a/moto/ec2/responses/elastic_block_store.py b/moto/ec2/responses/elastic_block_store.py index 96586a9bb..abb371260 100644 --- a/moto/ec2/responses/elastic_block_store.py +++ b/moto/ec2/responses/elastic_block_store.py @@ -42,12 +42,22 @@ class ElasticBlockStore(BaseResponse): return DELETE_VOLUME_RESPONSE def describe_snapshots(self): + # querystring for multiple snapshotids results in SnapshotId.1, SnapshotId.2 etc + snapshot_ids = ','.join([','.join(s[1]) for s in self.querystring.items() if 'SnapshotId' in s[0]]) snapshots = self.ec2_backend.describe_snapshots() + # Describe snapshots to handle filter on snapshot_ids + snapshots = [s for s in snapshots if s.id in snapshot_ids] if snapshot_ids else snapshots + # snapshots = self.ec2_backend.describe_snapshots() template = self.response_template(DESCRIBE_SNAPSHOTS_RESPONSE) return template.render(snapshots=snapshots) def describe_volumes(self): + # querystring for multiple volumeids results in VolumeId.1, VolumeId.2 etc + volume_ids = ','.join([','.join(v[1]) for v in self.querystring.items() if 'VolumeId' in v[0]]) volumes = self.ec2_backend.describe_volumes() + # Describe volumes to handle filter on volume_ids + volumes = [v for v in volumes if v.id in volume_ids] if volume_ids else volumes + # volumes = self.ec2_backend.describe_volumes() template = self.response_template(DESCRIBE_VOLUMES_RESPONSE) return template.render(volumes=volumes) @@ -103,7 +113,7 @@ CREATE_VOLUME_RESPONSE = """ @@ -174,7 +184,7 @@ CREATE_SNAPSHOT_RESPONSE = """ebs /dev/sda1 + {% for device_name,deviceobject in instance.get_block_device_mapping %} - /dev/sda1 + {{ device_name }} - {{ instance.block_device_mapping['/dev/sda1'].volume_id }} - attached - 2015-01-01T00:00:00.000Z - true + {{ deviceobject.volume_id }} + {{ deviceobject.status }} + {{ deviceobject.attach_time }} + {{ deviceobject.delete_on_termination }} + {{deviceobject.size}} + {% endfor %} {{ instance.virtualization_type }} ABCDE1234567890123 @@ -547,7 +550,7 @@ EC2_INSTANCE_STATUS = """ {% for instance in instances %} {{ instance.id }} - us-east-1d + {{ instance.placement }} {{ instance.state_code }} {{ instance.state }} diff --git a/moto/s3bucket_path/urls.py b/moto/s3bucket_path/urls.py index f37ab8cb7..c5dc86f2f 100644 --- a/moto/s3bucket_path/urls.py +++ b/moto/s3bucket_path/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from .responses import S3BucketPathResponseInstance as ro url_bases = [ - "https?://s3.amazonaws.com" + "https?://s3(.*).amazonaws.com" ] diff --git a/moto/server.py b/moto/server.py index af3ec55bd..4a7de1a70 100644 --- a/moto/server.py +++ b/moto/server.py @@ -79,10 +79,17 @@ def create_backend_app(service): else: endpoint = None - backend_app.route( + if endpoint in backend_app.view_functions: + # HACK: Sometimes we map the same view to multiple url_paths. Flask + # requries us to have different names. + endpoint += "2" + + backend_app.add_url_rule( url_path, endpoint=endpoint, - methods=HTTP_METHODS)(convert_flask_to_httpretty_response(handler)) + methods=HTTP_METHODS, + view_func=convert_flask_to_httpretty_response(handler), + ) return backend_app diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 69b3f63df..d20372762 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -11,10 +11,11 @@ from .exceptions import ( ) MAXIMUM_VISIBILTY_TIMEOUT = 43200 +DEFAULT_RECEIVED_MESSAGES = 1 SQS_REGION_REGEX = r'://(.+?)\.queue\.amazonaws\.com' -class QueuesResponse(BaseResponse): +class SQSResponse(BaseResponse): region_regex = SQS_REGION_REGEX @@ -22,6 +23,14 @@ class QueuesResponse(BaseResponse): def sqs_backend(self): return sqs_backends[self.region] + def _get_queue_name(self): + try: + queue_name = self.querystring.get('QueueUrl')[0].split("/")[-1] + except TypeError: + # Fallback to reading from the URL + queue_name = self.path.split("/")[-1] + return queue_name + def create_queue(self): visibility_timeout = None if 'Attribute.1.Name' in self.querystring and self.querystring.get('Attribute.1.Name')[0] == 'VisibilityTimeout': @@ -47,17 +56,8 @@ class QueuesResponse(BaseResponse): template = self.response_template(LIST_QUEUES_RESPONSE) return template.render(queues=queues) - -class QueueResponse(BaseResponse): - - region_regex = SQS_REGION_REGEX - - @property - def sqs_backend(self): - return sqs_backends[self.region] - def change_message_visibility(self): - queue_name = self.path.split("/")[-1] + queue_name = self._get_queue_name() receipt_handle = self.querystring.get("ReceiptHandle")[0] visibility_timeout = int(self.querystring.get("VisibilityTimeout")[0]) @@ -79,20 +79,20 @@ class QueueResponse(BaseResponse): return template.render() def get_queue_attributes(self): - queue_name = self.path.split("/")[-1] + queue_name = self._get_queue_name() queue = self.sqs_backend.get_queue(queue_name) template = self.response_template(GET_QUEUE_ATTRIBUTES_RESPONSE) return template.render(queue=queue) def set_queue_attributes(self): - queue_name = self.path.split("/")[-1] + queue_name = self._get_queue_name() 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) return SET_QUEUE_ATTRIBUTE_RESPONSE def delete_queue(self): - queue_name = self.path.split("/")[-1] + queue_name = self._get_queue_name() queue = self.sqs_backend.delete_queue(queue_name) if not queue: return "A queue with name {0} does not exist".format(queue_name), dict(status=404) @@ -113,7 +113,8 @@ class QueueResponse(BaseResponse): except MessageAttributesInvalid as e: return e.description, dict(status=e.status_code) - queue_name = self.path.split("/")[-1] + queue_name = self._get_queue_name() + message = self.sqs_backend.send_message( queue_name, message, @@ -135,7 +136,7 @@ class QueueResponse(BaseResponse): 'SendMessageBatchRequestEntry.2.DelaySeconds': ['0'], """ - queue_name = self.path.split("/")[-1] + queue_name = self._get_queue_name() messages = [] for index in range(1, 11): @@ -164,7 +165,7 @@ class QueueResponse(BaseResponse): return template.render(messages=messages) def delete_message(self): - queue_name = self.path.split("/")[-1] + queue_name = self._get_queue_name() receipt_handle = self.querystring.get("ReceiptHandle")[0] self.sqs_backend.delete_message(queue_name, receipt_handle) template = self.response_template(DELETE_MESSAGE_RESPONSE) @@ -180,7 +181,7 @@ class QueueResponse(BaseResponse): 'DeleteMessageBatchRequestEntry.2.ReceiptHandle': ['zxcvfda...'], ... """ - queue_name = self.path.split("/")[-1] + queue_name = self._get_queue_name() message_ids = [] for index in range(1, 11): @@ -201,14 +202,17 @@ class QueueResponse(BaseResponse): return template.render(message_ids=message_ids) def purge_queue(self): - queue_name = self.path.split("/")[-1] + queue_name = self._get_queue_name() self.sqs_backend.purge_queue(queue_name) template = self.response_template(PURGE_QUEUE_RESPONSE) return template.render() def receive_message(self): - queue_name = self.path.split("/")[-1] - message_count = int(self.querystring.get("MaxNumberOfMessages")[0]) + queue_name = self._get_queue_name() + try: + message_count = int(self.querystring.get("MaxNumberOfMessages")[0]) + except TypeError: + message_count = DEFAULT_RECEIVED_MESSAGES messages = self.sqs_backend.receive_messages(queue_name, message_count) template = self.response_template(RECEIVE_MESSAGE_RESPONSE) output = template.render(messages=messages) diff --git a/moto/sqs/urls.py b/moto/sqs/urls.py index 527a867c2..0780615ab 100644 --- a/moto/sqs/urls.py +++ b/moto/sqs/urls.py @@ -1,11 +1,13 @@ from __future__ import unicode_literals -from .responses import QueueResponse, QueuesResponse +from .responses import SQSResponse url_bases = [ "https?://(.*?)(queue|sqs)(.*?).amazonaws.com" ] +dispatch = SQSResponse().dispatch + url_paths = { - '{0}/$': QueuesResponse.dispatch, - '{0}/(?P\d+)/(?P[a-zA-Z0-9\-_]+)': QueueResponse.dispatch, + '{0}/$': dispatch, + '{0}/(?P\d+)/(?P[a-zA-Z0-9\-_]+)': dispatch, } diff --git a/setup.py b/setup.py index f9371cc3e..50ab56eea 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ if sys.version_info < (2, 7): setup( name='moto', - version='0.4.7', + version='0.4.8', description='A library that allows your python tests to easily' ' mock out the boto library', author='Steve Pulec', diff --git a/tests/test_cloudformation/test_server.py b/tests/test_cloudformation/test_server.py index baffc4882..47a094630 100644 --- a/tests/test_cloudformation/test_server.py +++ b/tests/test_cloudformation/test_server.py @@ -1 +1,41 @@ from __future__ import unicode_literals + +import json +from six.moves.urllib.parse import urlencode +import re +import sure # noqa + +import moto.server as server + +''' +Test the different server responses +''' + + +def test_cloudformation_server_get(): + backend = server.create_backend_app("cloudformation") + stack_name = 'test stack' + test_client = backend.test_client() + template_body = { + "Resources": {}, + } + res = test_client.get( + '/?{0}'.format( + urlencode({ + "Action": "CreateStack", + "StackName": stack_name, + "TemplateBody": json.dumps(template_body) + }) + ), + headers={"Host": "cloudformation.us-east-1.amazonaws.com"} + ) + stack_id = json.loads(res.data.decode("utf-8"))["CreateStackResponse"]["CreateStackResult"]["StackId"] + + res = test_client.get( + '/?Action=ListStacks', + headers={"Host": "cloudformation.us-east-1.amazonaws.com"} + ) + stacks = re.search("(.*)", res.data.decode('utf-8')) + + list_stack_id = stacks.groups()[0] + assert stack_id == list_stack_id diff --git a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py index e7ae60e7e..12e3aa15b 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_with_range_key.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals import boto +import boto3 +from boto3.dynamodb.conditions import Key import sure # noqa from freezegun import freeze_time from moto import mock_dynamodb2 @@ -253,31 +255,31 @@ def test_query(): table.count().should.equal(4) - results = table.query(forum_name__eq='the-key', subject__gt='1', consistent=True) + results = table.query_2(forum_name__eq='the-key', subject__gt='1', consistent=True) expected = ["123", "456", "789"] for index, item in enumerate(results): item["subject"].should.equal(expected[index]) - results = table.query(forum_name__eq="the-key", subject__gt='1', reverse=True) + results = table.query_2(forum_name__eq="the-key", subject__gt='1', reverse=True) for index, item in enumerate(results): item["subject"].should.equal(expected[len(expected) - 1 - index]) - results = table.query(forum_name__eq='the-key', subject__gt='1', consistent=True) + results = table.query_2(forum_name__eq='the-key', subject__gt='1', consistent=True) sum(1 for _ in results).should.equal(3) - results = table.query(forum_name__eq='the-key', subject__gt='234', consistent=True) + results = table.query_2(forum_name__eq='the-key', subject__gt='234', consistent=True) sum(1 for _ in results).should.equal(2) - results = table.query(forum_name__eq='the-key', subject__gt='9999') + results = table.query_2(forum_name__eq='the-key', subject__gt='9999') sum(1 for _ in results).should.equal(0) - results = table.query(forum_name__eq='the-key', subject__beginswith='12') + results = table.query_2(forum_name__eq='the-key', subject__beginswith='12') sum(1 for _ in results).should.equal(1) - results = table.query(forum_name__eq='the-key', subject__beginswith='7') + results = table.query_2(forum_name__eq='the-key', subject__beginswith='7') sum(1 for _ in results).should.equal(1) - results = table.query(forum_name__eq='the-key', subject__between=['567', '890']) + results = table.query_2(forum_name__eq='the-key', subject__between=['567', '890']) sum(1 for _ in results).should.equal(1) @@ -558,7 +560,6 @@ def test_lookup(): @mock_dynamodb2 def test_failed_overwrite(): - from decimal import Decimal table = Table.create('messages', schema=[ HashKey('id'), RangeKey('range'), @@ -567,19 +568,19 @@ def test_failed_overwrite(): 'write': 3, }) - data1 = {'id': '123', 'range': 'abc', 'data':'678'} + data1 = {'id': '123', 'range': 'abc', 'data': '678'} table.put_item(data=data1) - data2 = {'id': '123', 'range': 'abc', 'data':'345'} - table.put_item(data=data2, overwrite = True) + data2 = {'id': '123', 'range': 'abc', 'data': '345'} + table.put_item(data=data2, overwrite=True) - data3 = {'id': '123', 'range': 'abc', 'data':'812'} + data3 = {'id': '123', 'range': 'abc', 'data': '812'} table.put_item.when.called_with(data=data3).should.throw(ConditionalCheckFailedException) returned_item = table.lookup('123', 'abc') dict(returned_item).should.equal(data2) - data4 = {'id': '123', 'range': 'ghi', 'data':812} + data4 = {'id': '123', 'range': 'ghi', 'data': 812} table.put_item(data=data4) returned_item = table.lookup('123', 'ghi') @@ -593,7 +594,7 @@ def test_conflicting_writes(): RangeKey('range'), ]) - item_data = {'id': '123', 'range':'abc', 'data':'678'} + item_data = {'id': '123', 'range': 'abc', 'data': '678'} item1 = Item(table, item_data) item2 = Item(table, item_data) item1.save() @@ -603,3 +604,100 @@ def test_conflicting_writes(): item1.save() item2.save.when.called_with().should.throw(ConditionalCheckFailedException) + +""" +boto3 +""" + + +@mock_dynamodb2 +def test_boto3_conditions(): + 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' + }) + table.put_item(Item={ + 'forum_name': 'the-key', + 'subject': '456' + }) + table.put_item(Item={ + 'forum_name': 'the-key', + 'subject': '789' + }) + + # Test a query returning all items + results = table.query( + KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").gt('1'), + ScanIndexForward=True, + ) + expected = ["123", "456", "789"] + for index, item in enumerate(results['Items']): + item["subject"].should.equal(expected[index]) + + # Return all items again, but in reverse + results = table.query( + KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").gt('1'), + ScanIndexForward=False, + ) + for index, item in enumerate(reversed(results['Items'])): + item["subject"].should.equal(expected[index]) + + # Filter the subjects to only return some of the results + results = table.query( + KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").gt('234'), + ConsistentRead=True, + ) + results['Count'].should.equal(2) + + # Filter to return no results + results = table.query( + KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").gt('9999') + ) + results['Count'].should.equal(0) + + results = table.query( + KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").begins_with('12') + ) + results['Count'].should.equal(1) + + results = table.query( + KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").begins_with('7') + ) + results['Count'].should.equal(1) + + results = table.query( + KeyConditionExpression=Key('forum_name').eq('the-key') & Key("subject").between('567', '890') + ) + results['Count'].should.equal(1) diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py index 1710bd650..808805b8d 100644 --- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals import boto +import boto3 +from boto3.dynamodb.conditions import Key import sure # noqa from freezegun import freeze_time from boto.exception import JSONResponseError @@ -135,14 +137,6 @@ def test_item_put_without_table(): ).should.throw(JSONResponseError) -@requires_boto_gte("2.9") -@mock_dynamodb2 -def test_get_missing_item(): - table = create_table() - - table.get_item.when.called_with(test_hash=3241526475).should.throw(JSONResponseError) - - @requires_boto_gte("2.9") @mock_dynamodb2 def test_get_item_with_undeclared_table(): @@ -449,7 +443,6 @@ def test_update_item_set(): @mock_dynamodb2 def test_failed_overwrite(): - from decimal import Decimal table = Table.create('messages', schema=[ HashKey('id'), ], throughput={ @@ -457,19 +450,19 @@ def test_failed_overwrite(): 'write': 3, }) - data1 = {'id': '123', 'data':'678'} + data1 = {'id': '123', 'data': '678'} table.put_item(data=data1) - data2 = {'id': '123', 'data':'345'} - table.put_item(data=data2, overwrite = True) + data2 = {'id': '123', 'data': '345'} + table.put_item(data=data2, overwrite=True) - data3 = {'id': '123', 'data':'812'} + data3 = {'id': '123', 'data': '812'} table.put_item.when.called_with(data=data3).should.throw(ConditionalCheckFailedException) returned_item = table.lookup('123') dict(returned_item).should.equal(data2) - data4 = {'id': '124', 'data':812} + data4 = {'id': '124', 'data': 812} table.put_item(data=data4) returned_item = table.lookup('124') @@ -482,7 +475,7 @@ def test_conflicting_writes(): HashKey('id'), ]) - item_data = {'id': '123', 'data':'678'} + item_data = {'id': '123', 'data': '678'} item1 = Item(table, item_data) item2 = Item(table, item_data) item1.save() @@ -491,4 +484,46 @@ def test_conflicting_writes(): item2['data'] = '912' item1.save() - item2.save.when.called_with().should.throw(ConditionalCheckFailedException) \ No newline at end of file + item2.save.when.called_with().should.throw(ConditionalCheckFailedException) + + +""" +boto3 +""" + + +@mock_dynamodb2 +def test_boto3_conditions(): + dynamodb = boto3.resource('dynamodb', region_name='us-east-1') + + # Create the DynamoDB table. + table = dynamodb.create_table( + TableName='users', + KeySchema=[ + { + 'AttributeName': 'username', + 'KeyType': 'HASH' + }, + ], + AttributeDefinitions=[ + { + 'AttributeName': 'username', + 'AttributeType': 'S' + }, + ], + ProvisionedThroughput={ + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5 + } + ) + table = dynamodb.Table('users') + + table.put_item(Item={'username': 'johndoe'}) + table.put_item(Item={'username': 'janedoe'}) + + response = table.query( + KeyConditionExpression=Key('username').eq('johndoe') + ) + response['Count'].should.equal(1) + response['Items'].should.have.length_of(1) + response['Items'][0].should.equal({"username": "johndoe"}) diff --git a/tests/test_ec2/test_elastic_block_store.py b/tests/test_ec2/test_elastic_block_store.py index 50fe6b7ad..5ab5fcdc5 100644 --- a/tests/test_ec2/test_elastic_block_store.py +++ b/tests/test_ec2/test_elastic_block_store.py @@ -33,6 +33,19 @@ def test_create_and_delete_volume(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none +@mock_ec2 +def test_filter_volume_by_id(): + conn = boto.connect_ec2('the_key', 'the_secret') + volume1 = conn.create_volume(80, "us-east-1a") + volume2 = conn.create_volume(36, "us-east-1b") + volume3 = conn.create_volume(20, "us-east-1c") + vol1 = conn.get_all_volumes(volume_ids=volume3.id) + vol1.should.have.length_of(1) + vol1[0].size.should.equal(20) + vol1[0].zone.should.equal('us-east-1c') + vol2 = conn.get_all_volumes(volume_ids=[volume1.id, volume2.id]) + vol2.should.have.length_of(2) + @mock_ec2 def test_volume_attach_and_detach(): @@ -85,6 +98,7 @@ def test_create_snapshot(): snapshots = conn.get_all_snapshots() snapshots.should.have.length_of(1) snapshots[0].description.should.equal('a test snapshot') + snapshots[0].start_time.should_not.be.none # Create snapshot without description snapshot = volume.create_snapshot() @@ -100,6 +114,25 @@ def test_create_snapshot(): cm.exception.status.should.equal(400) cm.exception.request_id.should_not.be.none +@mock_ec2 +def test_filter_snapshot_by_id(): + conn = boto.connect_ec2('the_key', 'the_secret') + volume1 = conn.create_volume(36, "us-east-1a") + snap1 = volume1.create_snapshot('a test snapshot 1') + volume2 = conn.create_volume(42, 'us-east-1a') + snap2 = volume2.create_snapshot('a test snapshot 2') + volume3 = conn.create_volume(84, 'us-east-1a') + snap3 = volume3.create_snapshot('a test snapshot 3') + snapshots1 = conn.get_all_snapshots(snapshot_ids=snap2.id) + snapshots1.should.have.length_of(1) + snapshots1[0].volume_id.should.equal(volume2.id) + snapshots1[0].region.name.should.equal('us-east-1') + snapshots2 = conn.get_all_snapshots(snapshot_ids=[snap2.id, snap3.id]) + snapshots2.should.have.length_of(2) + for s in snapshots2: + s.start_time.should_not.be.none + s.volume_id.should.be.within([volume2.id, volume3.id]) + s.region.name.should.equal('us-east-1') @mock_ec2 def test_snapshot_attribute(): diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 5ee732d5e..629fc67ee 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -53,7 +53,7 @@ def test_instance_launch_and_terminate(): instances.should.have.length_of(1) instances[0].id.should.equal(instance.id) instances[0].state.should.equal('running') - instances[0].launch_time.should.equal("2014-01-01T05:00:00") + instances[0].launch_time.should.equal("2014-01-01T05:00:00Z") instances[0].vpc_id.should.equal(None) root_device_name = instances[0].root_device_name @@ -66,6 +66,35 @@ def test_instance_launch_and_terminate(): instance = reservations[0].instances[0] instance.state.should.equal('terminated') +@freeze_time("2014-01-01 05:00:00") +@mock_ec2 +def test_instance_attach_volume(): + conn = boto.connect_ec2('the_key', 'the_secret') + reservation = conn.run_instances('ami-1234abcd') + instance = reservation.instances[0] + + vol1 = conn.create_volume(size=36, zone=conn.region.name) + vol1.attach(instance.id, "/dev/sda1") + vol1.update() + vol2 = conn.create_volume(size=65, zone=conn.region.name) + vol2.attach(instance.id, "/dev/sdb1") + vol2.update() + vol3 = conn.create_volume(size=130, zone=conn.region.name) + vol3.attach(instance.id, "/dev/sdc1") + vol3.update() + + reservations = conn.get_all_instances() + instance = reservations[0].instances[0] + + instance.block_device_mapping.should.have.length_of(3) + + for v in conn.get_all_volumes(volume_ids=[instance.block_device_mapping['/dev/sdc1'].volume_id]): + v.attach_data.instance_id.should.equal(instance.id) + v.attach_data.attach_time.should.equal(instance.launch_time) # can do due to freeze_time decorator. + v.create_time.should.equal(instance.launch_time) # can do due to freeze_time decorator. + v.region.name.should.equal(instance.region.name) + v.status.should.equal('in-use') + @mock_ec2 def test_get_instances_by_id(): diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py index 8e1c35f5c..2b9fb7099 100644 --- a/tests/test_s3/test_s3.py +++ b/tests/test_s3/test_s3.py @@ -8,6 +8,7 @@ from io import BytesIO import json import boto +import boto3 from boto.exception import S3CreateError, S3ResponseError from boto.s3.connection import S3Connection from boto.s3.key import Key @@ -869,3 +870,18 @@ def test_policy(): bucket = conn.get_bucket(bucket_name) bucket.get_policy().decode('utf-8').should.equal(policy) + + +""" +boto3 +""" + + +@mock_s3 +def test_boto3_bucket_create(): + s3 = boto3.resource('s3', region_name='us-east-1') + s3.create_bucket(Bucket="blah") + + s3.Object('blah', 'hello.txt').put(Body="some text") + + s3.Object('blah', 'hello.txt').get()['Body'].read().decode("utf-8").should.equal("some text") diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py index bc6d36a4e..ba1e11e52 100644 --- a/tests/test_sqs/test_sqs.py +++ b/tests/test_sqs/test_sqs.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals import boto +import boto3 from boto.exception import SQSError from boto.sqs.message import RawMessage, Message @@ -462,3 +463,17 @@ def test_delete_message_after_visibility_timeout(): m1_retrieved.delete() assert new_queue.count() == 0 + +""" +boto3 +""" + + +@mock_sqs +def test_boto3_message_send(): + sqs = boto3.resource('sqs', region_name='us-east-1') + queue = sqs.create_queue(QueueName="blah") + queue.send_message(MessageBody="derp") + + messages = queue.receive_messages() + messages.should.have.length_of(1)