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