diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..94deffdd --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +SHELL := /bin/bash + +init: + python setup.py develop + pip install -r requirements.txt + +test: + nosetests ./tests/ + +travis: + nosetests ./tests/ diff --git a/README.md b/README.md index 962983d9..204f20bc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ Imagine you have the following code that you want to test: ```python import boto from boto.s3.key import Key -conn = boto.connect_s3() class MyModel(object): def __init__(self, name, value): @@ -17,6 +16,7 @@ class MyModel(object): self.value = value def save(self): + conn = boto.connect_s3() bucket = conn.get_bucket('mybucket') k = Key(bucket) k.key = self.name diff --git a/moto/__init__.py b/moto/__init__.py new file mode 100644 index 00000000..8da1c85e --- /dev/null +++ b/moto/__init__.py @@ -0,0 +1,2 @@ +from .ec2 import mock_ec2 +from .s3 import mock_s3 diff --git a/moto/__init__.pyc b/moto/__init__.pyc new file mode 100644 index 00000000..0969fbd6 Binary files /dev/null and b/moto/__init__.pyc differ diff --git a/moto/core/__init__.py b/moto/core/__init__.py new file mode 100644 index 00000000..a4f19aed --- /dev/null +++ b/moto/core/__init__.py @@ -0,0 +1 @@ +from .models import BaseBackend \ No newline at end of file diff --git a/moto/core/__init__.pyc b/moto/core/__init__.pyc new file mode 100644 index 00000000..3679b007 Binary files /dev/null and b/moto/core/__init__.pyc differ diff --git a/moto/core/models.py b/moto/core/models.py new file mode 100644 index 00000000..f24974d5 --- /dev/null +++ b/moto/core/models.py @@ -0,0 +1,40 @@ +import functools +import re + +from httpretty import HTTPretty + + +class BaseBackend(object): + base_url = None + + def reset(self): + self = self.__class__() + + @property + def urls(self): + backend_module = self.__class__.__module__ + backend_urls_module_name = backend_module.replace("models", "urls") + backend_urls_module = __import__(backend_urls_module_name, fromlist=['urls']) + urls = backend_urls_module.urls + return urls + + def decorator(self, func): + @functools.wraps(func) + def wrapper(*args, **kw): + self.reset() + + HTTPretty.reset() + HTTPretty.enable() + + for method in HTTPretty.METHODS: + for key, value in self.urls.iteritems(): + HTTPretty.register_uri( + method=method, + uri=re.compile(self.base_url + key), + body=value, + ) + try: + return func(*args, **kw) + finally: + HTTPretty.disable() + return wrapper diff --git a/moto/core/models.pyc b/moto/core/models.pyc new file mode 100644 index 00000000..0eaf077a Binary files /dev/null and b/moto/core/models.pyc differ diff --git a/moto/ec2/__init__.py b/moto/ec2/__init__.py new file mode 100644 index 00000000..f64708fe --- /dev/null +++ b/moto/ec2/__init__.py @@ -0,0 +1,2 @@ +from .models import ec2_backend +mock_ec2 = ec2_backend.decorator \ No newline at end of file diff --git a/moto/ec2/__init__.pyc b/moto/ec2/__init__.pyc new file mode 100644 index 00000000..aad71507 Binary files /dev/null and b/moto/ec2/__init__.pyc differ diff --git a/moto/ec2/models.py b/moto/ec2/models.py new file mode 100644 index 00000000..6d6013d4 --- /dev/null +++ b/moto/ec2/models.py @@ -0,0 +1,44 @@ +from boto.ec2.instance import Instance, InstanceState, Reservation + +from moto.core import BaseBackend +from .utils import random_instance_id, random_reservation_id + + +class MockEC2(BaseBackend): + base_url = "https://ec2.us-east-1.amazonaws.com" + + def __init__(self): + self.reservations = {} + + def add_instance(self): + new_instance = Instance() + new_instance.id = random_instance_id() + new_instance._state = InstanceState(0, "pending") + + new_reservation = Reservation() + new_reservation.id = random_reservation_id() + new_reservation.instances = [new_instance] + self.reservations[new_reservation.id] = new_reservation + return new_reservation + + def terminate_instances(self, instance_ids): + terminated_instances = [] + for instance in self.all_instances(): + if instance.id in instance_ids: + instance._state = InstanceState(32, 'shutting-down') + terminated_instances.append(instance) + + return terminated_instances + + def all_instances(self): + instances = [] + for reservation in self.all_reservations(): + for instance in reservation.instances: + instances.append(instance) + return instances + + def all_reservations(self): + return self.reservations.values() + + +ec2_backend = MockEC2() \ No newline at end of file diff --git a/moto/ec2/models.pyc b/moto/ec2/models.pyc new file mode 100644 index 00000000..f7d51b6a Binary files /dev/null and b/moto/ec2/models.pyc differ diff --git a/moto/ec2/responses.py b/moto/ec2/responses.py new file mode 100644 index 00000000..d77c828c --- /dev/null +++ b/moto/ec2/responses.py @@ -0,0 +1,224 @@ +from urlparse import parse_qs + +from jinja2 import Template + +from .models import ec2_backend + + +def instances(uri, body, headers): + querystring = parse_qs(body) + action = querystring['Action'][0] + + if action == 'DescribeInstances': + template = Template(EC2_DESCRIBE_INSTANCES) + return template.render(reservations=ec2_backend.all_reservations()) + elif action == 'RunInstances': + new_reservation = ec2_backend.add_instance() + template = Template(EC2_RUN_INSTANCES) + return template.render(reservation=new_reservation) + elif action == 'TerminateInstances': + instance_ids = querystring.get('InstanceId.1')[0] + instances = ec2_backend.terminate_instances(instance_ids) + template = Template(EC2_TERMINATE_INSTANCES) + return template.render(instances=instances) + else: + raise ValueError("Not implemented", action) + + +EC2_RUN_INSTANCES = """ + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + {{ reservation.id }} + 111122223333 + + + sg-245f6a01 + default + + + + {% for instance in reservation.instances %} + + {{ instance.id }} + ami-60a54009 + + 0 + pending + + + + + 0 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + default + + + enabled + + true + + + sg-245f6a01 + default + + + paravirtual + + xen + false + + {% endfor %} + + """ + +EC2_DESCRIBE_INSTANCES = """ + fdcdcab1-ae5c-489e-9c33-4637c5dda355 + + {% for reservation in reservations %} + + {{ reservation.id }} + 111122223333 + + + sg-1a2b3c4d + my-security-group + + + + {% for instance in reservation.instances %} + + {{ instance.id }} + ami-1a2b3c4d + + 16 + {{ instance.state }} + + + + + gsg-keypair + 0 + + c1.medium + YYYY-MM-DDTHH:MM:SS+0000 + + us-west-2a + + default + + windows + + disabled + + subnet-1a2b3c4d + vpc-1a2b3c4d + 10.0.0.12 + 46.51.219.63 + true + + + sg-1a2b3c4d + my-security-group + + + x86_64 + ebs + /dev/sda1 + + + /dev/sda1 + + vol-1a2b3c4d + attached + YYYY-MM-DDTHH:MM:SS.SSSZ + true + + + + hvm + ABCDE1234567890123 + + + Name + Windows Instance + + + xen + + + eni-1a2b3c4d + subnet-1a2b3c4d + vpc-1a2b3c4d + Primary network interface + 111122223333 + in-use + 10.0.0.12 + 1b:2b:3c:4d:5e:6f + true + + + sg-1a2b3c4d + my-security-group + + + + eni-attach-1a2b3c4d + 0 + attached + YYYY-MM-DDTHH:MM:SS+0000 + true + + + 46.51.219.63 + 111122223333 + + + + 10.0.0.12 + true + + 46.51.219.63 + 111122223333 + + + + 10.0.0.14 + false + + 46.51.221.177 + 111122223333 + + + + + + + {% endfor %} + + + {% endfor %} + +""" + +EC2_TERMINATE_INSTANCES = """ + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + {% for instance in instances %} + + {{ instance.id }} + + 32 + shutting-down + + + 16 + running + + + {% endfor %} + +""" \ No newline at end of file diff --git a/moto/ec2/responses.pyc b/moto/ec2/responses.pyc new file mode 100644 index 00000000..83c92271 Binary files /dev/null and b/moto/ec2/responses.pyc differ diff --git a/moto/ec2/urls.py b/moto/ec2/urls.py new file mode 100644 index 00000000..0d12059e --- /dev/null +++ b/moto/ec2/urls.py @@ -0,0 +1,5 @@ +from .responses import instances + +urls = { + '/': instances, +} diff --git a/moto/ec2/urls.pyc b/moto/ec2/urls.pyc new file mode 100644 index 00000000..5a13c4fe Binary files /dev/null and b/moto/ec2/urls.pyc differ diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py new file mode 100644 index 00000000..190ff4cd --- /dev/null +++ b/moto/ec2/utils.py @@ -0,0 +1,15 @@ +import random + + +def random_id(prefix=''): + size = 8 + chars = range(10) + ['a', 'b', 'c', 'd', 'e', 'f'] + + instance_tag = ''.join(unicode(random.choice(chars)) for x in range(size)) + return '{}-{}'.format(prefix, instance_tag) + +def random_instance_id(): + return random_id(prefix='i') + +def random_reservation_id(): + return random_id(prefix='r') diff --git a/moto/ec2/utils.pyc b/moto/ec2/utils.pyc new file mode 100644 index 00000000..c5395cf9 Binary files /dev/null and b/moto/ec2/utils.pyc differ diff --git a/moto/s3/__init__.py b/moto/s3/__init__.py new file mode 100644 index 00000000..441b5996 --- /dev/null +++ b/moto/s3/__init__.py @@ -0,0 +1,2 @@ +from .models import s3_backend +mock_s3 = s3_backend.decorator \ No newline at end of file diff --git a/moto/s3/__init__.pyc b/moto/s3/__init__.pyc new file mode 100644 index 00000000..d172de9a Binary files /dev/null and b/moto/s3/__init__.pyc differ diff --git a/moto/s3/models.py b/moto/s3/models.py new file mode 100644 index 00000000..c24fb89c --- /dev/null +++ b/moto/s3/models.py @@ -0,0 +1,57 @@ +# from boto.s3.bucket import Bucket +# from boto.s3.key import Key +import md5 + +from moto.core import BaseBackend + + +class FakeKey(object): + def __init__(self, name, value): + self.name = name + self.value = value + + @property + def etag(self): + value_md5 = md5.new() + value_md5.update(self.value) + return '"{0}"'.format(value_md5.hexdigest()) + +class FakeBucket(object): + def __init__(self, name): + self.name = name + self.keys = [] + + +class MockS3(BaseBackend): + base_url = "https://(.+).s3.amazonaws.com" + + def __init__(self): + self.buckets = {} + + def create_bucket(self, bucket_name): + new_bucket = FakeBucket(name=bucket_name) + self.buckets[bucket_name] = new_bucket + return new_bucket + + def get_bucket(self, bucket_name): + return self.buckets.get(bucket_name) + + def set_key(self, bucket_name, key_name, value): + bucket = self.buckets[bucket_name] + new_key = FakeKey(name=key_name, value=value) + bucket.keys.append(new_key) + + return new_key + + def get_key(self, bucket_name, key_name): + bucket = self.buckets[bucket_name] + found_key = None + for key in bucket.keys: + if key.name == key_name: + found_key = key + break + + return found_key + + +s3_backend = MockS3() diff --git a/moto/s3/models.pyc b/moto/s3/models.pyc new file mode 100644 index 00000000..d515c95f Binary files /dev/null and b/moto/s3/models.pyc differ diff --git a/moto/s3/responses.py b/moto/s3/responses.py new file mode 100644 index 00000000..57a44dd0 --- /dev/null +++ b/moto/s3/responses.py @@ -0,0 +1,72 @@ +from jinja2 import Template + +from .models import s3_backend + +def bucket_response(uri, body, headers): + hostname = uri.hostname + bucket_name = hostname.replace(".s3.amazonaws.com", "") + + if uri.method == 'GET': + bucket = s3_backend.get_bucket(bucket_name) + if bucket: + template = Template(S3_BUCKET_GET_RESPONSE) + return template.render(bucket=bucket) + else: + return "", dict(status=404) + else: + new_bucket = s3_backend.create_bucket(bucket_name) + template = Template(S3_BUCKET_CREATE_RESPONSE) + return template.render(bucket=new_bucket) + + +def key_response(uri_info, body, headers): + + key_name = uri_info.path.lstrip('/') + hostname = uri_info.hostname + bucket_name = hostname.replace(".s3.amazonaws.com", "") + + if uri_info.method == 'GET': + key = s3_backend.get_key(bucket_name, key_name) + if key: + return key.value + else: + return "", dict(status=404) + + if uri_info.method == 'PUT': + if body: + new_key = s3_backend.set_key(bucket_name, key_name, body) + return S3_OBJECT_RESPONSE, dict(etag=new_key.etag) + key = s3_backend.get_key(bucket_name, key_name) + if key: + return "", dict(etag=key.etag) + else: + return "" + elif uri_info.method == 'HEAD': + key = s3_backend.get_key(bucket_name, key_name) + return S3_OBJECT_RESPONSE, dict(etag=key.etag) + else: + import pdb;pdb.set_trace() + + +S3_BUCKET_GET_RESPONSE = """\ + {{ bucket.name }}\ + notes/\ + /\ + 1000\ + AKIAIOSFODNN7EXAMPLE\ + 2006-03-01T12:00:00.183Z\ + Iuyz3d3P0aTou39dzbqaEXAMPLE=\ + """ + +S3_BUCKET_CREATE_RESPONSE = """ + + {{ bucket.name }} + +""" + +S3_OBJECT_RESPONSE = """ + + "asdlfkdalsjfsalfkjsadlfjsdjkk" + 2006-03-01T12:00:00.183Z + + """ \ No newline at end of file diff --git a/moto/s3/responses.pyc b/moto/s3/responses.pyc new file mode 100644 index 00000000..b1f545e1 Binary files /dev/null and b/moto/s3/responses.pyc differ diff --git a/moto/s3/urls.py b/moto/s3/urls.py new file mode 100644 index 00000000..b7b20d82 --- /dev/null +++ b/moto/s3/urls.py @@ -0,0 +1,6 @@ +from .responses import bucket_response, key_response + +urls = { + '/$': bucket_response, + '/(.+)': key_response, +} diff --git a/moto/s3/urls.pyc b/moto/s3/urls.pyc new file mode 100644 index 00000000..4ab59047 Binary files /dev/null and b/moto/s3/urls.pyc differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..430bd583 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +boto +httpretty +Jinja2 +mock +nose +sure \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..4bfde779 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import sys +from setuptools import setup, find_packages + +setup( + name='moto', + version='0.0.1', + description='Moto is a library that allows your python tests to easily mock out the boto library', + author='Steve Pulec', + author_email='spulec@gmail', + url='https://github.com/spulec/moto', + packages=find_packages() +) \ No newline at end of file diff --git a/tests/__init__.pyc b/tests/__init__.pyc new file mode 100644 index 00000000..a328373b Binary files /dev/null and b/tests/__init__.pyc differ diff --git a/tests/test_ec2/test_ec2.py b/tests/test_ec2/test_ec2.py new file mode 100644 index 00000000..90e7e156 --- /dev/null +++ b/tests/test_ec2/test_ec2.py @@ -0,0 +1,29 @@ +import boto +from boto.ec2.instance import Reservation +from sure import expect + +from moto import mock_ec2 + + + +@mock_ec2 +def test_instance_launch_and_terminate(): + conn = boto.connect_ec2('the_key', 'the_secret') + reservation = conn.run_instances('') + reservation.should.be.a(Reservation) + reservation.instances.should.have.length_of(1) + instance = reservation.instances[0] + + reservations = conn.get_all_instances() + reservations.should.have.length_of(1) + reservations[0].id.should.equal(reservation.id) + instances = reservations[0].instances + instances.should.have.length_of(1) + instances[0].id.should.equal(instance.id) + instances[0].state.should.equal('pending') + + conn.terminate_instances(instances[0].id) + + reservations = conn.get_all_instances() + instance = reservations[0].instances[0] + instance.state.should.equal('shutting-down') diff --git a/tests/test_ec2/test_ec2.pyc b/tests/test_ec2/test_ec2.pyc new file mode 100644 index 00000000..50053ae6 Binary files /dev/null and b/tests/test_ec2/test_ec2.pyc differ diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py new file mode 100644 index 00000000..0f844476 --- /dev/null +++ b/tests/test_s3/test_s3.py @@ -0,0 +1,30 @@ +import boto +from boto.s3.key import Key + +from moto import mock_s3 + + +class MyModel(object): + def __init__(self, name, value): + self.name = name + self.value = value + + def save(self): + conn = boto.connect_s3('the_key', 'the_secret') + bucket = conn.get_bucket('mybucket') + k = Key(bucket) + k.key = self.name + k.set_contents_from_string(self.value) + + +@mock_s3 +def test_my_model_save(): + # Create Bucket so that test can run + conn = boto.connect_s3('the_key', 'the_secret') + conn.create_bucket('mybucket') + #################################### + + model_instance = MyModel('steve', 'is awesome') + model_instance.save() + + assert conn.get_bucket('mybucket').get_key('steve').get_contents_as_string() == 'is awesome' diff --git a/tests/test_s3/test_s3.pyc b/tests/test_s3/test_s3.pyc new file mode 100644 index 00000000..8fe1e7de Binary files /dev/null and b/tests/test_s3/test_s3.pyc differ