diff --git a/.gitignore b/.gitignore index 113b41964..e17800a77 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ moto.egg-info/* dist/* +.tox .coverage *.pyc +.noseids +build/ diff --git a/.travis.yml b/.travis.yml index b6026cbe9..037501a5b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: python python: + - 2.6 - 2.7 env: matrix: @@ -12,6 +13,7 @@ env: - BOTO_VERSION=2.7 install: - pip install boto==$BOTO_VERSION + - pip install https://github.com/gabrielfalcao/HTTPretty/tarball/8bbbdfc14326678b1aeba6a2d81af0d835a2cd6f - pip install . - pip install -r requirements.txt script: diff --git a/AUTHORS.md b/AUTHORS.md index 3a615ea97..1cf19dd76 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -6,3 +6,5 @@ Moto is written by Steve Pulec with contributions from: * [Dilshod Tadjibaev](https://github.com/antimora) * [Dan Berglund](https://github.com/cheif) * [Lincoln de Sousa](https://github.com/clarete) +* [mhock](https://github.com/mhock) +* [Ilya Sukhanov](https://github.com/IlyaSukhanov) \ No newline at end of file diff --git a/moto/__init__.py b/moto/__init__.py index 57e8eef38..76cc62c55 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -7,6 +7,7 @@ from .ec2 import mock_ec2 from .elb import mock_elb from .emr import mock_emr from .s3 import mock_s3 +from .s3bucket_path import mock_s3bucket_path from .ses import mock_ses from .sqs import mock_sqs from .sts import mock_sts diff --git a/moto/backends.py b/moto/backends.py index 6f375a8f1..0bc766fe3 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -4,6 +4,7 @@ from moto.ec2 import ec2_backend from moto.elb import elb_backend from moto.emr import emr_backend from moto.s3 import s3_backend +from moto.s3bucket_path import s3bucket_path_backend from moto.ses import ses_backend from moto.sqs import sqs_backend from moto.sts import sts_backend @@ -15,6 +16,7 @@ BACKENDS = { 'elb': elb_backend, 'emr': emr_backend, 's3': s3_backend, + 's3bucket_path': s3bucket_path_backend, 'ses': ses_backend, 'sqs': sqs_backend, 'sts': sts_backend, diff --git a/moto/core/models.py b/moto/core/models.py index f3e6ad701..17238fcb0 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -9,6 +9,7 @@ from .utils import convert_regex_to_flask_path class MockAWS(object): def __init__(self, backend): self.backend = backend + HTTPretty.reset() def __call__(self, func): return self.decorate_callable(func) diff --git a/moto/core/responses.py b/moto/core/responses.py index 7e896e961..3d8fa138c 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -46,7 +46,7 @@ class BaseResponse(object): status = new_headers.pop('status', 200) headers.update(new_headers) return status, headers, body - raise NotImplementedError("The {} action has not been implemented".format(action)) + raise NotImplementedError("The {0} action has not been implemented".format(action)) def metadata_response(request, full_url, headers): diff --git a/moto/core/utils.py b/moto/core/utils.py index 53418edbf..0d47478d7 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -31,7 +31,7 @@ def get_random_hex(length=8): def get_random_message_id(): - return '{}-{}-{}-{}-{}'.format(get_random_hex(8), get_random_hex(4), get_random_hex(4), get_random_hex(4), get_random_hex(12)) + return '{0}-{1}-{2}-{3}-{4}'.format(get_random_hex(8), get_random_hex(4), get_random_hex(4), get_random_hex(4), get_random_hex(12)) def convert_regex_to_flask_path(url_path): @@ -61,7 +61,7 @@ class convert_flask_to_httpretty_response(object): outer = self.callback.im_class.__name__ else: outer = self.callback.__module__ - return "{}.{}".format(outer, self.callback.__name__) + return "{0}.{1}".format(outer, self.callback.__name__) def __call__(self, args=None, **kwargs): headers = dict(request.headers) diff --git a/moto/dynamodb/models.py b/moto/dynamodb/models.py index 66612caa8..198b6dc38 100644 --- a/moto/dynamodb/models.py +++ b/moto/dynamodb/models.py @@ -1,7 +1,14 @@ -from collections import defaultdict, OrderedDict +from collections import defaultdict import datetime import json +try: + from collections import OrderedDict +except ImportError: + # python 2.6 or earlier, use backport + from ordereddict import OrderedDict + + from moto.core import BaseBackend from .comparisons import get_comparison_func from .utils import unix_time @@ -36,7 +43,7 @@ class DynamoType(object): ) def __repr__(self): - return "DynamoType: {}".format(self.to_json()) + return "DynamoType: {0}".format(self.to_json()) def to_json(self): return {self.type: self.value} @@ -62,7 +69,7 @@ class Item(object): self.attrs[key] = DynamoType(value) def __repr__(self): - return "Item: {}".format(self.to_json()) + return "Item: {0}".format(self.to_json()) def to_json(self): attributes = {} diff --git a/moto/dynamodb/utils.py b/moto/dynamodb/utils.py index e4787d105..5ca887da6 100644 --- a/moto/dynamodb/utils.py +++ b/moto/dynamodb/utils.py @@ -1,7 +1,5 @@ -import datetime +import calendar def unix_time(dt): - epoch = datetime.datetime.utcfromtimestamp(0) - delta = dt - epoch - return delta.total_seconds() + return calendar.timegm(dt.timetuple()) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 2150f2567..0a391c5d7 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -15,6 +15,9 @@ from .utils import ( random_subnet_id, random_volume_id, random_vpc_id, + random_eip_association_id, + random_eip_allocation_id, + random_ip, ) @@ -29,24 +32,24 @@ class Instance(BotoInstance): super(Instance, self).__init__() self.id = random_instance_id() self.image_id = image_id - self._state = InstanceState() + self._state = InstanceState("running", 16) self.user_data = user_data def start(self): - self._state.name = "pending" - self._state.code = 0 + self._state.name = "running" + self._state.code = 16 def stop(self): - self._state.name = "stopping" - self._state.code = 64 + self._state.name = "stopped" + self._state.code = 80 def terminate(self): - self._state.name = "shutting-down" - self._state.code = 32 + self._state.name = "terminated" + self._state.code = 48 def reboot(self): - self._state.name = "pending" - self._state.code = 0 + self._state.name = "running" + self._state.code = 16 def get_tags(self): tags = ec2_backend.describe_tags(self.id) @@ -215,8 +218,12 @@ class AmiBackend(object): self.amis[ami_id] = ami return ami - def describe_images(self): - return self.amis.values() + def describe_images(self, ami_ids=None): + if ami_ids: + images = [image for image in self.amis.values() if image.id in ami_ids] + else: + images = self.amis.values() + return images def deregister_image(self, ami_id): if ami_id in self.amis: @@ -280,7 +287,7 @@ class SecurityRule(object): @property def unique_representation(self): - return "{}-{}-{}-{}-{}".format( + return "{0}-{1}-{2}-{3}-{4}".format( self.ip_protocol, self.from_port, self.to_port, @@ -571,9 +578,92 @@ class SpotRequestBackend(object): return requests +class ElasticAddress(): + def __init__(self, domain): + self.public_ip = random_ip() + self.allocation_id = random_eip_allocation_id() if domain == "vpc" else None + self.domain = domain + self.instance = None + self.association_id = None + + +class ElasticAddressBackend(object): + + def __init__(self): + self.addresses = [] + super(ElasticAddressBackend, self).__init__() + + def allocate_address(self, domain): + address = ElasticAddress(domain) + self.addresses.append(address) + return address + + def address_by_ip(self, ips): + return [address for address in self.addresses + if address.public_ip in ips] + + def address_by_allocation(self, allocation_ids): + return [address for address in self.addresses + if address.allocation_id in allocation_ids] + + def address_by_association(self, association_ids): + return [address for address in self.addresses + if address.association_id in association_ids] + + def associate_address(self, instance, address=None, allocation_id=None, reassociate=False): + eips = [] + if address: + eips = self.address_by_ip([address]) + elif allocation_id: + eips = self.address_by_allocation([allocation_id]) + eip = eips[0] if len(eips) > 0 else None + + if eip and eip.instance is None or reassociate: + eip.instance = instance + if eip.domain == "vpc": + eip.association_id = random_eip_association_id() + return eip + else: + return None + + def describe_addresses(self): + return self.addresses + + def disassociate_address(self, address=None, association_id=None): + eips = [] + if address: + eips = self.address_by_ip([address]) + elif association_id: + eips = self.address_by_association([association_id]) + + if eips: + eip = eips[0] + eip.instance = None + eip.association_id = None + return True + else: + return False + + def release_address(self, address=None, allocation_id=None): + eips = [] + if address: + eips = self.address_by_ip([address]) + elif allocation_id: + eips = self.address_by_allocation([allocation_id]) + + if eips: + eip = eips[0] + self.disassociate_address(address=eip.public_ip) + eip.allocation_id = None + self.addresses.remove(eip) + return True + else: + return False + + class EC2Backend(BaseBackend, InstanceBackend, TagBackend, AmiBackend, RegionsAndZonesBackend, SecurityGroupBackend, EBSBackend, - VPCBackend, SubnetBackend, SpotRequestBackend): + VPCBackend, SubnetBackend, SpotRequestBackend, ElasticAddressBackend): pass diff --git a/moto/ec2/responses/amis.py b/moto/ec2/responses/amis.py index feddc89f1..10936e635 100644 --- a/moto/ec2/responses/amis.py +++ b/moto/ec2/responses/amis.py @@ -1,18 +1,21 @@ from jinja2 import Template from moto.ec2.models import ec2_backend -from moto.ec2.utils import instance_ids_from_querystring +from moto.ec2.utils import instance_ids_from_querystring, image_ids_from_querystring class AmisResponse(object): def create_image(self): name = self.querystring.get('Name')[0] - description = self.querystring.get('Description')[0] + if "Description" in self.querystring: + description = self.querystring.get('Description')[0] + else: + description = "" instance_ids = instance_ids_from_querystring(self.querystring) instance_id = instance_ids[0] image = ec2_backend.create_image(instance_id, name, description) if not image: - return "There is not instance with id {}".format(instance_id), dict(status=404) + return "There is not instance with id {0}".format(instance_id), dict(status=404) template = Template(CREATE_IMAGE_RESPONSE) return template.render(image=image) @@ -30,7 +33,8 @@ class AmisResponse(object): raise NotImplementedError('AMIs.describe_image_attribute is not yet implemented') def describe_images(self): - images = ec2_backend.describe_images() + ami_ids = image_ids_from_querystring(self.querystring) + images = ec2_backend.describe_images(ami_ids=ami_ids) template = Template(DESCRIBE_IMAGES_RESPONSE) return template.render(images=images) diff --git a/moto/ec2/responses/elastic_block_store.py b/moto/ec2/responses/elastic_block_store.py index d81c61c9d..64ad65b30 100644 --- a/moto/ec2/responses/elastic_block_store.py +++ b/moto/ec2/responses/elastic_block_store.py @@ -39,7 +39,7 @@ class ElasticBlockStore(object): success = ec2_backend.delete_snapshot(snapshot_id) if not success: # Snapshot doesn't exist - return "Snapshot with id {} does not exist".format(snapshot_id), dict(status=404) + return "Snapshot with id {0} does not exist".format(snapshot_id), dict(status=404) return DELETE_SNAPSHOT_RESPONSE def delete_volume(self): @@ -47,7 +47,7 @@ class ElasticBlockStore(object): success = ec2_backend.delete_volume(volume_id) if not success: # Volume doesn't exist - return "Volume with id {} does not exist".format(volume_id), dict(status=404) + return "Volume with id {0} does not exist".format(volume_id), dict(status=404) return DELETE_VOLUME_RESPONSE def describe_snapshot_attribute(self): @@ -77,7 +77,7 @@ class ElasticBlockStore(object): attachment = ec2_backend.detach_volume(volume_id, instance_id, device_path) if not attachment: # Volume wasn't attached - return "Volume {} can not be detached from {} because it is not attached".format(volume_id, instance_id), dict(status=404) + return "Volume {0} can not be detached from {1} because it is not attached".format(volume_id, instance_id), dict(status=404) template = Template(DETATCH_VOLUME_RESPONSE) return template.render(attachment=attachment) diff --git a/moto/ec2/responses/elastic_ip_addresses.py b/moto/ec2/responses/elastic_ip_addresses.py index 368517d7d..60cdbcf5e 100644 --- a/moto/ec2/responses/elastic_ip_addresses.py +++ b/moto/ec2/responses/elastic_ip_addresses.py @@ -1,21 +1,132 @@ from jinja2 import Template from moto.ec2.models import ec2_backend -from moto.ec2.utils import resource_ids_from_querystring +from moto.ec2.utils import sequence_from_querystring + class ElasticIPAddresses(object): def allocate_address(self): - raise NotImplementedError('ElasticIPAddresses.allocate_address is not yet implemented') + if "Domain" in self.querystring: + domain = self.querystring.get('Domain')[0] + if domain != "vpc": + return "Invalid domain:{0}.".format(domain), dict(status=400) + else: + domain = "standard" + address = ec2_backend.allocate_address(domain) + template = Template(ALLOCATE_ADDRESS_RESPONSE) + return template.render(address=address) def associate_address(self): - raise NotImplementedError('ElasticIPAddresses.associate_address is not yet implemented') + if "InstanceId" in self.querystring: + instance = ec2_backend.get_instance(self.querystring['InstanceId'][0]) + elif "NetworkInterfaceId" in self.querystring: + raise NotImplementedError("Lookup by allocation id not implemented") + else: + return "Invalid request, expect InstanceId/NetworkId parameter.", dict(status=400) + + reassociate = False + if "AllowReassociation" in self.querystring: + reassociate = self.querystring['AllowReassociation'][0] == "true" + + if "PublicIp" in self.querystring: + eip = ec2_backend.associate_address(instance, address=self.querystring['PublicIp'][0], reassociate=reassociate) + elif "AllocationId" in self.querystring: + eip = ec2_backend.associate_address(instance, allocation_id=self.querystring['AllocationId'][0], reassociate=reassociate) + else: + return "Invalid request, expect PublicIp/AllocationId parameter.", dict(status=400) + + if eip: + template = Template(ASSOCIATE_ADDRESS_RESPONSE) + return template.render(address=eip) + else: + return "Failed to associate address.", dict(status=400) def describe_addresses(self): - raise NotImplementedError('ElasticIPAddresses.describe_addresses is not yet implemented') + template = Template(DESCRIBE_ADDRESS_RESPONSE) + + if "Filter.1.Name" in self.querystring: + raise NotImplementedError("Filtering not supported in describe_address.") + elif "PublicIp.1" in self.querystring: + public_ips = sequence_from_querystring("PublicIp", self.querystring) + addresses = ec2_backend.address_by_ip(public_ips) + elif "AllocationId.1" in self.querystring: + allocation_ids = sequence_from_querystring("AllocationId", self.querystring) + addresses = ec2_backend.address_by_allocation(allocation_ids) + else: + addresses = ec2_backend.describe_addresses() + return template.render(addresses=addresses) def disassociate_address(self): - raise NotImplementedError('ElasticIPAddresses.disassociate_address is not yet implemented') + if "PublicIp" in self.querystring: + disassociated = ec2_backend.disassociate_address(address=self.querystring['PublicIp'][0]) + elif "AssociationId" in self.querystring: + disassociated = ec2_backend.disassociate_address(association_id=self.querystring['AssociationId'][0]) + else: + return "Invalid request, expect PublicIp/AssociationId parameter.", dict(status=400) + + if disassociated: + return Template(DISASSOCIATE_ADDRESS_RESPONSE).render() + else: + return "Address conresponding to PublicIp/AssociationIP not found.", dict(status=400) def release_address(self): - raise NotImplementedError('ElasticIPAddresses.release_address is not yet implemented') + if "PublicIp" in self.querystring: + released = ec2_backend.release_address(address=self.querystring['PublicIp'][0]) + elif "AllocationId" in self.querystring: + released = ec2_backend.release_address(allocation_id=self.querystring['AllocationId'][0]) + else: + return "Invalid request, expect PublicIp/AllocationId parameter.", dict(status=400) + + if released: + return Template(RELEASE_ADDRESS_RESPONSE).render() + else: + return "Address conresponding to PublicIp/AssociationIP not found.", dict(status=400) + + +ALLOCATE_ADDRESS_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ address.public_ip }} + {{ address.domain }} + {% if address.allocation_id %} + {{ address.allocation_id }} + {% endif %} +""" + +ASSOCIATE_ADDRESS_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + {% if address.association_id %} + {{ address.association_id }} + {% endif %} +""" + +DESCRIBE_ADDRESS_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + {% for address in addresses %} + + {{ address.public_ip }} + {{ address.domain }} + {% if address.instance %} + {{ address.instance.id }} + {% else %} + + {% endif %} + {% if address.association_id %} + {{ address.association_id }} + {% endif %} + + {% endfor %} + +""" + +DISASSOCIATE_ADDRESS_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true +""" + +RELEASE_ADDRESS_RESPONSE = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true +""" diff --git a/moto/ec2/responses/instances.py b/moto/ec2/responses/instances.py index 68be9dafd..f230dcf49 100644 --- a/moto/ec2/responses/instances.py +++ b/moto/ec2/responses/instances.py @@ -95,8 +95,8 @@ EC2_RUN_INSTANCES = """ diff --git a/moto/s3/urls.py b/moto/s3/urls.py index 21370c15a..5f9bc0cf1 100644 --- a/moto/s3/urls.py +++ b/moto/s3/urls.py @@ -1,10 +1,10 @@ -from .responses import bucket_response, key_response +from .responses import S3ResponseInstance url_bases = [ "https?://(?P[a-zA-Z0-9\-_.]*)\.?s3.amazonaws.com" ] url_paths = { - '{0}/$': bucket_response, - '{0}/(?P[a-zA-Z0-9\-_.]+)': key_response, + '{0}/$': S3ResponseInstance.bucket_response, + '{0}/(?P[a-zA-Z0-9\-_.]+)': S3ResponseInstance.key_response, } diff --git a/moto/s3bucket_path/__init__.py b/moto/s3bucket_path/__init__.py new file mode 100644 index 000000000..6dd680bed --- /dev/null +++ b/moto/s3bucket_path/__init__.py @@ -0,0 +1,2 @@ +from .models import s3bucket_path_backend +mock_s3bucket_path = s3bucket_path_backend.decorator diff --git a/moto/s3bucket_path/models.py b/moto/s3bucket_path/models.py new file mode 100644 index 000000000..2b7e99539 --- /dev/null +++ b/moto/s3bucket_path/models.py @@ -0,0 +1,7 @@ +from moto.s3.models import S3Backend + + +class S3BucketPathBackend(S3Backend): + True + +s3bucket_path_backend = S3BucketPathBackend() diff --git a/moto/s3bucket_path/responses.py b/moto/s3bucket_path/responses.py new file mode 100644 index 000000000..0f54a1a1d --- /dev/null +++ b/moto/s3bucket_path/responses.py @@ -0,0 +1,15 @@ +from .models import s3bucket_path_backend + +from .utils import bucket_name_from_url + +from moto.s3.responses import ResponseObject + + +def parse_key_name(pth): + return "/".join(pth.rstrip("/").split("/")[2:]) + +S3BucketPathResponseInstance = ResponseObject( + s3bucket_path_backend, + bucket_name_from_url, + parse_key_name, +) diff --git a/moto/s3bucket_path/urls.py b/moto/s3bucket_path/urls.py new file mode 100644 index 000000000..28f1debc8 --- /dev/null +++ b/moto/s3bucket_path/urls.py @@ -0,0 +1,20 @@ +from .responses import S3BucketPathResponseInstance as ro + +url_bases = [ + "https?://s3.amazonaws.com" +] + + +def bucket_response2(*args): + return ro.bucket_response(*args) + + +def bucket_response3(*args): + return ro.bucket_response(*args) + +url_paths = { + '{0}/$': bucket_response3, + '{0}/(?P[a-zA-Z0-9\-_.]+)$': ro.bucket_response, + '{0}/(?P[a-zA-Z0-9\-_.]+)/$': bucket_response2, + '{0}/(?P[a-zA-Z0-9\-_./]+)/(?P[a-zA-Z0-9\-_.?]+)': ro.key_response +} diff --git a/moto/s3bucket_path/utils.py b/moto/s3bucket_path/utils.py new file mode 100644 index 000000000..97f1d40f1 --- /dev/null +++ b/moto/s3bucket_path/utils.py @@ -0,0 +1,10 @@ +import urlparse + + +def bucket_name_from_url(url): + pth = urlparse.urlparse(url).path.lstrip("/") + + l = pth.lstrip("/").split("/") + if len(l) == 0 or l[0] == "": + return None + return l[0] diff --git a/moto/ses/responses.py b/moto/ses/responses.py index 6640d76be..a6c2d1790 100644 --- a/moto/ses/responses.py +++ b/moto/ses/responses.py @@ -45,7 +45,7 @@ class EmailResponse(BaseResponse): destination = self.querystring.get('Destination.ToAddresses.member.1')[0] message = ses_backend.send_email(source, subject, body, destination) if not message: - return "Did not have authority to send from email {}".format(source), dict(status=400) + return "Did not have authority to send from email {0}".format(source), dict(status=400) template = Template(SEND_EMAIL_RESPONSE) return template.render(message=message) @@ -56,7 +56,7 @@ class EmailResponse(BaseResponse): message = ses_backend.send_raw_email(source, destination, raw_data) if not message: - return "Did not have authority to send from email {}".format(source), dict(status=400) + return "Did not have authority to send from email {0}".format(source), dict(status=400) template = Template(SEND_RAW_EMAIL_RESPONSE) return template.render(message=message) diff --git a/moto/ses/utils.py b/moto/ses/utils.py index ad6c13dcc..6501290a4 100644 --- a/moto/ses/utils.py +++ b/moto/ses/utils.py @@ -7,7 +7,7 @@ def random_hex(length): def get_random_message_id(): - return "{}-{}-{}-{}-{}-{}-{}".format( + return "{0}-{1}-{2}-{3}-{4}-{5}-{6}".format( random_hex(16), random_hex(8), random_hex(4), diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py index 0f582cf85..b49796545 100644 --- a/moto/sqs/responses.py +++ b/moto/sqs/responses.py @@ -26,7 +26,6 @@ class QueuesResponse(BaseResponse): else: return "", dict(status=404) - def list_queues(self): queues = sqs_backend.list_queues() template = Template(LIST_QUEUES_RESPONSE) @@ -51,7 +50,7 @@ class QueueResponse(BaseResponse): queue_name = self.path.split("/")[-1] queue = sqs_backend.delete_queue(queue_name) if not queue: - return "A queue with name {} does not exist".format(queue_name), dict(status=404) + return "A queue with name {0} does not exist".format(queue_name), dict(status=404) template = Template(DELETE_QUEUE_RESPONSE) return template.render(queue=queue) @@ -79,15 +78,15 @@ class QueueResponse(BaseResponse): messages = [] for index in range(1, 11): # Loop through looking for messages - message_key = 'SendMessageBatchRequestEntry.{}.MessageBody'.format(index) + message_key = 'SendMessageBatchRequestEntry.{0}.MessageBody'.format(index) message_body = self.querystring.get(message_key) if not message_body: # Found all messages break - message_user_id_key = 'SendMessageBatchRequestEntry.{}.Id'.format(index) + message_user_id_key = 'SendMessageBatchRequestEntry.{0}.Id'.format(index) message_user_id = self.querystring.get(message_user_id_key)[0] - delay_key = 'SendMessageBatchRequestEntry.{}.DelaySeconds'.format(index) + delay_key = 'SendMessageBatchRequestEntry.{0}.DelaySeconds'.format(index) delay_seconds = self.querystring.get(delay_key, [None])[0] message = sqs_backend.send_message(queue_name, message_body[0], delay_seconds=delay_seconds) message.user_id = message_user_id @@ -118,7 +117,7 @@ class QueueResponse(BaseResponse): message_ids = [] for index in range(1, 11): # Loop through looking for messages - receipt_key = 'DeleteMessageBatchRequestEntry.{}.ReceiptHandle'.format(index) + receipt_key = 'DeleteMessageBatchRequestEntry.{0}.ReceiptHandle'.format(index) receipt_handle = self.querystring.get(receipt_key) if not receipt_handle: # Found all messages @@ -126,7 +125,7 @@ class QueueResponse(BaseResponse): sqs_backend.delete_message(queue_name, receipt_handle[0]) - message_user_id_key = 'DeleteMessageBatchRequestEntry.{}.Id'.format(index) + message_user_id_key = 'DeleteMessageBatchRequestEntry.{0}.Id'.format(index) message_user_id = self.querystring.get(message_user_id_key)[0] message_ids.append(message_user_id) diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 000000000..96e1d08d9 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,4 @@ +-r requirements.txt +flask +boto +httpretty diff --git a/setup.py b/setup.py index d5c232b40..05f185a7f 100644 --- a/setup.py +++ b/setup.py @@ -2,9 +2,22 @@ from setuptools import setup, find_packages +install_requires = [ + "boto", + "flask", + "httpretty>=0.6.1", + "Jinja2", +] + +import sys + +if sys.version_info < (2, 7): + # No buildint OrderedDict before 2.7 + install_requires.append('ordereddict') + setup( name='moto', - version='0.2.9', + version='0.2.10', description='A library that allows your python tests to easily' ' mock out the boto library', author='Steve Pulec', @@ -16,10 +29,5 @@ setup( ], }, packages=find_packages(), - install_requires=[ - "boto", - "flask", - "httpretty>=0.6.1", - "Jinja2", - ], + install_requires=install_requires, ) diff --git a/tests/test_dynamodb/test_dynamodb_table_with_range_key.py b/tests/test_dynamodb/test_dynamodb_table_with_range_key.py index 12700707c..38be4e493 100644 --- a/tests/test_dynamodb/test_dynamodb_table_with_range_key.py +++ b/tests/test_dynamodb/test_dynamodb_table_with_range_key.py @@ -365,7 +365,7 @@ def test_scan(): 'Body': 'http://url_to_lolcat.gif', 'SentBy': 'User B', 'ReceivedTime': '12/9/2011 11:36:03 PM', - 'Ids': {1, 2, 3}, + 'Ids': set([1, 2, 3]), 'PK': 7, } item = table.new_item( @@ -442,7 +442,7 @@ def test_write_batch(): 'Body': 'http://url_to_lolcat.gif', 'SentBy': 'User B', 'ReceivedTime': '12/9/2011 11:36:03 PM', - 'Ids': {1, 2, 3}, + 'Ids': set([1, 2, 3]), 'PK': 7, }, )) @@ -489,7 +489,7 @@ def test_batch_read(): 'Body': 'http://url_to_lolcat.gif', 'SentBy': 'User B', 'ReceivedTime': '12/9/2011 11:36:03 PM', - 'Ids': {1, 2, 3}, + 'Ids': set([1, 2, 3]), 'PK': 7, } item = table.new_item( diff --git a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py index 81e76f7f8..3e00fb979 100644 --- a/tests/test_dynamodb/test_dynamodb_table_without_range_key.py +++ b/tests/test_dynamodb/test_dynamodb_table_without_range_key.py @@ -282,7 +282,7 @@ def test_scan(): 'Body': 'http://url_to_lolcat.gif', 'SentBy': 'User B', 'ReceivedTime': '12/9/2011 11:36:03 PM', - 'Ids': {1, 2, 3}, + 'Ids': set([1, 2, 3]), 'PK': 7, } item = table.new_item( @@ -356,7 +356,7 @@ def test_write_batch(): 'Body': 'http://url_to_lolcat.gif', 'SentBy': 'User B', 'ReceivedTime': '12/9/2011 11:36:03 PM', - 'Ids': {1, 2, 3}, + 'Ids': set([1, 2, 3]), 'PK': 7, }, )) @@ -401,7 +401,7 @@ def test_batch_read(): 'Body': 'http://url_to_lolcat.gif', 'SentBy': 'User B', 'ReceivedTime': '12/9/2011 11:36:03 PM', - 'Ids': {1, 2, 3}, + 'Ids': set([1, 2, 3]), 'PK': 7, } item = table.new_item( diff --git a/tests/test_ec2/test_elastic_ip_addresses.py b/tests/test_ec2/test_elastic_ip_addresses.py index 5aba36b92..69647a162 100644 --- a/tests/test_ec2/test_elastic_ip_addresses.py +++ b/tests/test_ec2/test_elastic_ip_addresses.py @@ -1,9 +1,194 @@ +"""Test mocking of Elatic IP Address""" import boto +from boto.exception import EC2ResponseError + import sure # noqa from moto import mock_ec2 +import logging +import types + @mock_ec2 -def test_elastic_ip_addresses(): - pass +def test_eip_allocate_classic(): + """Allocate/release Classic EIP""" + conn = boto.connect_ec2('the_key', 'the_secret') + + standard = conn.allocate_address() + standard.should.be.a(boto.ec2.address.Address) + standard.public_ip.should.be.a(types.UnicodeType) + standard.instance_id.should.be.none + standard.domain.should.be.equal("standard") + standard.release() + standard.should_not.be.within(conn.get_all_addresses()) + + +@mock_ec2 +def test_eip_allocate_vpc(): + """Allocate/release VPC EIP""" + conn = boto.connect_ec2('the_key', 'the_secret') + + vpc = conn.allocate_address(domain="vpc") + vpc.should.be.a(boto.ec2.address.Address) + vpc.domain.should.be.equal("vpc") + logging.debug("vpc alloc_id:".format(vpc.allocation_id)) + vpc.release() + + +@mock_ec2 +def test_eip_allocate_invalid_domain(): + """Allocate EIP invalid domain""" + conn = boto.connect_ec2('the_key', 'the_secret') + + conn.allocate_address.when.called_with(domain="bogus").should.throw(EC2ResponseError) + + +@mock_ec2 +def test_eip_associate_classic(): + """Associate/Disassociate EIP to classic instance""" + conn = boto.connect_ec2('the_key', 'the_secret') + + reservation = conn.run_instances('ami-1234abcd') + instance = reservation.instances[0] + + eip = conn.allocate_address() + eip.instance_id.should.be.none + conn.associate_address.when.called_with(public_ip=eip.public_ip).should.throw(EC2ResponseError) + conn.associate_address(instance_id=instance.id, public_ip=eip.public_ip) + eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): + eip.instance_id.should.be.equal(instance.id) + conn.disassociate_address(public_ip=eip.public_ip) + eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): + eip.instance_id.should.be.equal(u'') + eip.release() + eip.should_not.be.within(conn.get_all_addresses()) + eip = None + + instance.terminate() + +@mock_ec2 +def test_eip_associate_vpc(): + """Associate/Disassociate EIP to VPC instance""" + conn = boto.connect_ec2('the_key', 'the_secret') + + reservation = conn.run_instances('ami-1234abcd') + instance = reservation.instances[0] + + eip = conn.allocate_address(domain='vpc') + eip.instance_id.should.be.none + conn.associate_address.when.called_with(allocation_id=eip.allocation_id).should.throw(EC2ResponseError) + conn.associate_address(instance_id=instance.id, allocation_id=eip.allocation_id) + eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): + eip.instance_id.should.be.equal(instance.id) + conn.disassociate_address(association_id=eip.association_id) + eip = conn.get_all_addresses(addresses=[eip.public_ip])[0] # no .update() on address ): + eip.instance_id.should.be.equal(u'') + eip.association_id.should.be.none + eip.release() + eip = None + + instance.terminate() + +@mock_ec2 +def test_eip_reassociate(): + """reassociate EIP""" + conn = boto.connect_ec2('the_key', 'the_secret') + + reservation = conn.run_instances('ami-1234abcd') + instance = reservation.instances[0] + + eip = conn.allocate_address() + conn.associate_address(instance_id=instance.id, public_ip=eip.public_ip) + conn.associate_address.when.called_with(instance_id=instance.id, public_ip=eip.public_ip, allow_reassociation=False).should.throw(EC2ResponseError) + conn.associate_address.when.called_with(instance_id=instance.id, public_ip=eip.public_ip, allow_reassociation=True).should_not.throw(EC2ResponseError) + eip.release() + eip = None + + instance.terminate() + +@mock_ec2 +def test_eip_associate_invalid_args(): + """Associate EIP, invalid args """ + conn = boto.connect_ec2('the_key', 'the_secret') + + reservation = conn.run_instances('ami-1234abcd') + instance = reservation.instances[0] + + eip = conn.allocate_address() + conn.associate_address.when.called_with(instance_id=instance.id).should.throw(EC2ResponseError) + + instance.terminate() + + +@mock_ec2 +def test_eip_disassociate_bogus_association(): + """Disassociate bogus EIP""" + conn = boto.connect_ec2('the_key', 'the_secret') + conn.disassociate_address.when.called_with(association_id="bogus").should.throw(EC2ResponseError) + +@mock_ec2 +def test_eip_release_bogus_eip(): + """Release bogus EIP""" + conn = boto.connect_ec2('the_key', 'the_secret') + conn.release_address.when.called_with(allocation_id="bogus").should.throw(EC2ResponseError) + + +@mock_ec2 +def test_eip_disassociate_arg_error(): + """Invalid arguments disassociate address""" + conn = boto.connect_ec2('the_key', 'the_secret') + conn.disassociate_address.when.called_with().should.throw(EC2ResponseError) + + +@mock_ec2 +def test_eip_release_arg_error(): + """Invalid arguments release address""" + conn = boto.connect_ec2('the_key', 'the_secret') + conn.release_address.when.called_with().should.throw(EC2ResponseError) + + +@mock_ec2 +def test_eip_describe(): + """Listing of allocated Elastic IP Addresses.""" + conn = boto.connect_ec2('the_key', 'the_secret') + eips = [] + number_of_classic_ips = 2 + number_of_vpc_ips = 2 + + #allocate some IPs + for _ in range(number_of_classic_ips): + eips.append(conn.allocate_address()) + for _ in range(number_of_vpc_ips): + eips.append(conn.allocate_address(domain='vpc')) + len(eips).should.be.equal(number_of_classic_ips + number_of_vpc_ips) + + # Can we find each one individually? + for eip in eips: + if eip.allocation_id: + lookup_addresses = conn.get_all_addresses(allocation_ids=[eip.allocation_id]) + else: + lookup_addresses = conn.get_all_addresses(addresses=[eip.public_ip]) + len(lookup_addresses).should.be.equal(1) + lookup_addresses[0].public_ip.should.be.equal(eip.public_ip) + + # Can we find first two when we search for them? + lookup_addresses = conn.get_all_addresses(addresses=[eips[0].public_ip, eips[1].public_ip]) + len(lookup_addresses).should.be.equal(2) + lookup_addresses[0].public_ip.should.be.equal(eips[0].public_ip) + lookup_addresses[1].public_ip.should.be.equal(eips[1].public_ip) + + #Release all IPs + for eip in eips: + eip.release() + len(conn.get_all_addresses()).should.be.equal(0) + + +@mock_ec2 +def test_eip_describe_none(): + """Find nothing when seach for bogus IP""" + conn = boto.connect_ec2('the_key', 'the_secret') + lookup_addresses = conn.get_all_addresses(addresses=["256.256.256.256"]) + len(lookup_addresses).should.be.equal(0) + + diff --git a/tests/test_ec2/test_instances.py b/tests/test_ec2/test_instances.py index 073ad7e4b..d2c386555 100644 --- a/tests/test_ec2/test_instances.py +++ b/tests/test_ec2/test_instances.py @@ -35,6 +35,7 @@ def test_instance_launch_and_terminate(): reservation.should.be.a(Reservation) reservation.instances.should.have.length_of(1) instance = reservation.instances[0] + instance.state.should.equal('pending') reservations = conn.get_all_instances() reservations.should.have.length_of(1) @@ -42,13 +43,13 @@ def test_instance_launch_and_terminate(): instances = reservations[0].instances instances.should.have.length_of(1) instances[0].id.should.equal(instance.id) - instances[0].state.should.equal('pending') + instances[0].state.should.equal('running') conn.terminate_instances([instances[0].id]) reservations = conn.get_all_instances() instance = reservations[0].instances[0] - instance.state.should.equal('shutting-down') + instance.state.should.equal('terminated') @mock_ec2 @@ -85,18 +86,18 @@ def test_get_instances_filtering_by_state(): conn.terminate_instances([instance1.id]) - reservations = conn.get_all_instances(filters={'instance-state-name': 'pending'}) + reservations = conn.get_all_instances(filters={'instance-state-name': 'running'}) reservations.should.have.length_of(1) # Since we terminated instance1, only instance2 and instance3 should be returned instance_ids = [instance.id for instance in reservations[0].instances] set(instance_ids).should.equal(set([instance2.id, instance3.id])) - reservations = conn.get_all_instances([instance2.id], filters={'instance-state-name': 'pending'}) + reservations = conn.get_all_instances([instance2.id], filters={'instance-state-name': 'running'}) reservations.should.have.length_of(1) instance_ids = [instance.id for instance in reservations[0].instances] instance_ids.should.equal([instance2.id]) - reservations = conn.get_all_instances([instance2.id], filters={'instance-state-name': 'terminating'}) + reservations = conn.get_all_instances([instance2.id], filters={'instance-state-name': 'terminated'}) list(reservations).should.equal([]) # get_all_instances should still return all 3 diff --git a/tests/test_s3bucket_path/test_bucket_path_server.py b/tests/test_s3bucket_path/test_bucket_path_server.py new file mode 100644 index 000000000..943615767 --- /dev/null +++ b/tests/test_s3bucket_path/test_bucket_path_server.py @@ -0,0 +1,50 @@ +import sure # noqa + +import moto.server as server + +''' +Test the different server responses +''' +server.configure_urls("s3bucket_path") + + +def test_s3_server_get(): + test_client = server.app.test_client() + res = test_client.get('/') + + res.data.should.contain('ListAllMyBucketsResult') + + +def test_s3_server_bucket_create(): + test_client = server.app.test_client() + res = test_client.put('/foobar', 'http://localhost:5000') + res.status_code.should.equal(200) + + res = test_client.get('/') + res.data.should.contain('foobar') + + res = test_client.get('/foobar', 'http://localhost:5000') + res.status_code.should.equal(200) + res.data.should.contain("ListBucketResult") + + res = test_client.put('/foobar/bar', 'http://localhost:5000', data='test value') + res.status_code.should.equal(200) + + res = test_client.get('/foobar/bar', 'http://localhost:5000') + res.status_code.should.equal(200) + res.data.should.equal("test value") + + +def test_s3_server_post_to_bucket(): + test_client = server.app.test_client() + res = test_client.put('/foobar', 'http://localhost:5000/') + res.status_code.should.equal(200) + + test_client.post('/foobar', "https://localhost:5000/", data={ + 'key': 'the-key', + 'file': 'nothing' + }) + + res = test_client.get('/foobar/the-key', 'http://localhost:5000/') + res.status_code.should.equal(200) + res.data.should.equal("nothing") diff --git a/tests/test_s3bucket_path/test_s3bucket_path.py b/tests/test_s3bucket_path/test_s3bucket_path.py new file mode 100644 index 000000000..1f62f23eb --- /dev/null +++ b/tests/test_s3bucket_path/test_s3bucket_path.py @@ -0,0 +1,281 @@ +import urllib2 + +import boto +from boto.exception import S3ResponseError +from boto.s3.key import Key +from boto.s3.connection import OrdinaryCallingFormat + +from freezegun import freeze_time +import requests + +import sure # noqa + +from moto import mock_s3bucket_path + + +def create_connection(key=None, secret=None): + return boto.connect_s3(key, secret, calling_format=OrdinaryCallingFormat()) + + +class MyModel(object): + def __init__(self, name, value): + self.name = name + self.value = value + + def save(self): + conn = create_connection('the_key', 'the_secret') + bucket = conn.get_bucket('mybucket') + k = Key(bucket) + k.key = self.name + k.set_contents_from_string(self.value) + + +@mock_s3bucket_path +def test_my_model_save(): + # Create Bucket so that test can run + conn = create_connection('the_key', 'the_secret') + conn.create_bucket('mybucket') + #################################### + + model_instance = MyModel('steve', 'is awesome') + model_instance.save() + + conn.get_bucket('mybucket').get_key('steve').get_contents_as_string().should.equal('is awesome') + + +@mock_s3bucket_path +def test_missing_key(): + conn = create_connection('the_key', 'the_secret') + bucket = conn.create_bucket("foobar") + bucket.get_key("the-key").should.equal(None) + + +@mock_s3bucket_path +def test_missing_key_urllib2(): + conn = create_connection('the_key', 'the_secret') + conn.create_bucket("foobar") + + urllib2.urlopen.when.called_with("http://s3.amazonaws.com/foobar/the-key").should.throw(urllib2.HTTPError) + + +@mock_s3bucket_path +def test_empty_key(): + conn = create_connection('the_key', 'the_secret') + bucket = conn.create_bucket("foobar") + key = Key(bucket) + key.key = "the-key" + key.set_contents_from_string("") + + bucket.get_key("the-key").get_contents_as_string().should.equal('') + + +@mock_s3bucket_path +def test_empty_key_set_on_existing_key(): + conn = create_connection('the_key', 'the_secret') + bucket = conn.create_bucket("foobar") + key = Key(bucket) + key.key = "the-key" + key.set_contents_from_string("foobar") + + bucket.get_key("the-key").get_contents_as_string().should.equal('foobar') + + key.set_contents_from_string("") + bucket.get_key("the-key").get_contents_as_string().should.equal('') + + +@mock_s3bucket_path +def test_large_key_save(): + conn = create_connection('the_key', 'the_secret') + bucket = conn.create_bucket("foobar") + key = Key(bucket) + key.key = "the-key" + key.set_contents_from_string("foobar" * 100000) + + bucket.get_key("the-key").get_contents_as_string().should.equal('foobar' * 100000) + + +@mock_s3bucket_path +def test_copy_key(): + conn = create_connection('the_key', 'the_secret') + bucket = conn.create_bucket("foobar") + key = Key(bucket) + key.key = "the-key" + key.set_contents_from_string("some value") + + bucket.copy_key('new-key', 'foobar', 'the-key') + + bucket.get_key("the-key").get_contents_as_string().should.equal("some value") + bucket.get_key("new-key").get_contents_as_string().should.equal("some value") + + +@mock_s3bucket_path +def test_set_metadata(): + conn = create_connection('the_key', 'the_secret') + bucket = conn.create_bucket("foobar") + key = Key(bucket) + key.key = 'the-key' + key.set_metadata('md', 'Metadatastring') + key.set_contents_from_string("Testval") + + bucket.get_key('the-key').get_metadata('md').should.equal('Metadatastring') + + +@freeze_time("2012-01-01 12:00:00") +@mock_s3bucket_path +def test_last_modified(): + # See https://github.com/boto/boto/issues/466 + conn = create_connection() + bucket = conn.create_bucket("foobar") + key = Key(bucket) + key.key = "the-key" + key.set_contents_from_string("some value") + + rs = bucket.get_all_keys() + rs[0].last_modified.should.equal('2012-01-01T12:00:00Z') + + bucket.get_key("the-key").last_modified.should.equal('Sun, 01 Jan 2012 12:00:00 GMT') + + +@mock_s3bucket_path +def test_missing_bucket(): + conn = create_connection('the_key', 'the_secret') + conn.get_bucket.when.called_with('mybucket').should.throw(S3ResponseError) + + +@mock_s3bucket_path +def test_bucket_with_dash(): + conn = create_connection('the_key', 'the_secret') + conn.get_bucket.when.called_with('mybucket-test').should.throw(S3ResponseError) + + +@mock_s3bucket_path +def test_bucket_deletion(): + conn = create_connection('the_key', 'the_secret') + bucket = conn.create_bucket("foobar") + + key = Key(bucket) + key.key = "the-key" + key.set_contents_from_string("some value") + + # Try to delete a bucket that still has keys + conn.delete_bucket.when.called_with("foobar").should.throw(S3ResponseError) + + bucket.delete_key("the-key") + conn.delete_bucket("foobar") + + # Get non-existing bucket + conn.get_bucket.when.called_with("foobar").should.throw(S3ResponseError) + + # Delete non-existant bucket + conn.delete_bucket.when.called_with("foobar").should.throw(S3ResponseError) + + +@mock_s3bucket_path +def test_get_all_buckets(): + conn = create_connection('the_key', 'the_secret') + conn.create_bucket("foobar") + conn.create_bucket("foobar2") + buckets = conn.get_all_buckets() + + buckets.should.have.length_of(2) + + +@mock_s3bucket_path +def test_post_to_bucket(): + conn = create_connection('the_key', 'the_secret') + bucket = conn.create_bucket("foobar") + + requests.post("https://s3.amazonaws.com/foobar", { + 'key': 'the-key', + 'file': 'nothing' + }) + + bucket.get_key('the-key').get_contents_as_string().should.equal('nothing') + + +@mock_s3bucket_path +def test_post_with_metadata_to_bucket(): + conn = create_connection('the_key', 'the_secret') + bucket = conn.create_bucket("foobar") + + requests.post("https://s3.amazonaws.com/foobar", { + 'key': 'the-key', + 'file': 'nothing', + 'x-amz-meta-test': 'metadata' + }) + + bucket.get_key('the-key').get_metadata('test').should.equal('metadata') + + +@mock_s3bucket_path +def test_bucket_method_not_implemented(): + requests.patch.when.called_with("https://s3.amazonaws.com/foobar").should.throw(NotImplementedError) + + +@mock_s3bucket_path +def test_key_method_not_implemented(): + requests.post.when.called_with("https://s3.amazonaws.com/foobar/foo").should.throw(NotImplementedError) + + +@mock_s3bucket_path +def test_bucket_name_with_dot(): + conn = create_connection() + bucket = conn.create_bucket('firstname.lastname') + + k = Key(bucket, 'somekey') + k.set_contents_from_string('somedata') + + +@mock_s3bucket_path +def test_key_with_special_characters(): + conn = create_connection() + bucket = conn.create_bucket('test_bucket_name') + + key = Key(bucket, 'test_list_keys_2/x?y') + key.set_contents_from_string('value1') + + key_list = bucket.list('test_list_keys_2/', '/') + keys = [x for x in key_list] + keys[0].name.should.equal("test_list_keys_2/x?y") + + +@mock_s3bucket_path +def test_bucket_key_listing_order(): + conn = create_connection() + bucket = conn.create_bucket('test_bucket') + prefix = 'toplevel/' + + def store(name): + k = Key(bucket, prefix + name) + k.set_contents_from_string('somedata') + + names = ['x/key', 'y.key1', 'y.key2', 'y.key3', 'x/y/key', 'x/y/z/key'] + + for name in names: + store(name) + + delimiter = None + keys = [x.name for x in bucket.list(prefix, delimiter)] + keys.should.equal([ + 'toplevel/x/key', 'toplevel/x/y/key', 'toplevel/x/y/z/key', + 'toplevel/y.key1', 'toplevel/y.key2', 'toplevel/y.key3' + ]) + + delimiter = '/' + keys = [x.name for x in bucket.list(prefix, delimiter)] + keys.should.equal([ + 'toplevel/y.key1', 'toplevel/y.key2', 'toplevel/y.key3', 'toplevel/x/' + ]) + + # Test delimiter with no prefix + delimiter = '/' + keys = [x.name for x in bucket.list(prefix=None, delimiter=delimiter)] + keys.should.equal(['toplevel']) + + delimiter = None + keys = [x.name for x in bucket.list(prefix + 'x', delimiter)] + keys.should.equal([u'toplevel/x/key', u'toplevel/x/y/key', u'toplevel/x/y/z/key']) + + delimiter = '/' + keys = [x.name for x in bucket.list(prefix + 'x', delimiter)] + keys.should.equal([u'toplevel/x/']) diff --git a/tests/test_s3bucket_path/test_s3bucket_path_utils.py b/tests/test_s3bucket_path/test_s3bucket_path_utils.py new file mode 100644 index 000000000..4b9ff30b1 --- /dev/null +++ b/tests/test_s3bucket_path/test_s3bucket_path_utils.py @@ -0,0 +1,14 @@ +from sure import expect +from moto.s3bucket_path.utils import bucket_name_from_url + + +def test_base_url(): + expect(bucket_name_from_url('https://s3.amazonaws.com/')).should.equal(None) + + +def test_localhost_bucket(): + expect(bucket_name_from_url('https://localhost:5000/wfoobar/abc')).should.equal("wfoobar") + + +def test_localhost_without_bucket(): + expect(bucket_name_from_url('https://www.localhost:5000')).should.equal(None) diff --git a/tox.ini b/tox.ini new file mode 100644 index 000000000..5f5b681e3 --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py26, py27 + +[testenv] +deps = -r{toxinidir}/requirements.txt +commands = + {envpython} setup.py test + nosetests