diff --git a/moto/__init__.py b/moto/__init__.py index be7cfdda3..70ba9ca20 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -28,3 +28,4 @@ from .sns import mock_sns # flake8: noqa from .sqs import mock_sqs # flake8: noqa from .sts import mock_sts # flake8: noqa from .route53 import mock_route53 # flake8: noqa +from .swf import mock_swf # flake8: noqa diff --git a/moto/swf/__init__.py b/moto/swf/__init__.py new file mode 100644 index 000000000..7e43ca392 --- /dev/null +++ b/moto/swf/__init__.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from .models import swf_backends +from ..core.models import MockAWS + +swf_backend = swf_backends['us-east-1'] + + +def mock_swf(func=None): + if func: + return MockAWS(swf_backends)(func) + else: + return MockAWS(swf_backends) diff --git a/moto/swf/exceptions.py b/moto/swf/exceptions.py new file mode 100644 index 000000000..bfbb96020 --- /dev/null +++ b/moto/swf/exceptions.py @@ -0,0 +1,32 @@ +from __future__ import unicode_literals + +from boto.exception import JSONResponseError + + +class SWFClientError(JSONResponseError): + def __init__(self, message, __type): + super(SWFClientError, self).__init__( + 400, "Bad Request", + body={"message": message, "__type": __type} + ) + + +class SWFUnknownResourceFault(SWFClientError): + def __init__(self, resource_type, resource_name): + super(SWFUnknownResourceFault, self).__init__( + "Unknown {}: {}".format(resource_type, resource_name), + "com.amazonaws.swf.base.model#UnknownResourceFault") + + +class SWFDomainAlreadyExistsFault(SWFClientError): + def __init__(self, domain_name): + super(SWFDomainAlreadyExistsFault, self).__init__( + domain_name, + "com.amazonaws.swf.base.model#DomainAlreadyExistsFault") + + +class SWFDomainDeprecatedFault(SWFClientError): + def __init__(self, domain_name): + super(SWFDomainDeprecatedFault, self).__init__( + domain_name, + "com.amazonaws.swf.base.model#DomainDeprecatedFault") diff --git a/moto/swf/models.py b/moto/swf/models.py new file mode 100644 index 000000000..61ed508aa --- /dev/null +++ b/moto/swf/models.py @@ -0,0 +1,67 @@ +from __future__ import unicode_literals + +import boto.swf + +from moto.core import BaseBackend +from .exceptions import ( + SWFUnknownResourceFault, + SWFDomainAlreadyExistsFault, + SWFDomainDeprecatedFault, +) + + +class Domain(object): + def __init__(self, name, retention, description=None): + self.name = name + self.retention = retention + self.description = description + self.status = "REGISTERED" + + def __repr__(self): + return "Domain(name: %s, retention: %s, description: %s)" % (self.name, self.retention, self.description) + + +class SWFBackend(BaseBackend): + def __init__(self, region_name): + self.region_name = region_name + self.domains = [] + super(SWFBackend, self).__init__() + + def reset(self): + region_name = self.region_name + self.__dict__ = {} + self.__init__(region_name) + + def _get_domain(self, name, ignore_empty=False): + matching = [domain for domain in self.domains if domain.name == name] + if not matching and not ignore_empty: + raise SWFUnknownResourceFault("domain", name) + if matching: + return matching[0] + return None + + def list_domains(self, status): + return [domain for domain in self.domains + if domain.status == status] + + def register_domain(self, name, workflow_execution_retention_period_in_days, + description=None): + if self._get_domain(name, ignore_empty=True): + raise SWFDomainAlreadyExistsFault(name) + domain = Domain(name, workflow_execution_retention_period_in_days, + description) + self.domains.append(domain) + + def deprecate_domain(self, name): + domain = self._get_domain(name) + if domain.status == "DEPRECATED": + raise SWFDomainDeprecatedFault(name) + domain.status = "DEPRECATED" + + def describe_domain(self, name): + return self._get_domain(name) + + +swf_backends = {} +for region in boto.swf.regions(): + swf_backends[region.name] = SWFBackend(region.name) diff --git a/moto/swf/responses.py b/moto/swf/responses.py new file mode 100644 index 000000000..ec23a9536 --- /dev/null +++ b/moto/swf/responses.py @@ -0,0 +1,103 @@ +import json +import logging +import six + +from moto.core.responses import BaseResponse +from werkzeug.exceptions import HTTPException +from moto.core.utils import camelcase_to_underscores, method_names_from_class + +from .models import swf_backends + + +class SWFResponse(BaseResponse): + + @property + def swf_backend(self): + return swf_backends[self.region] + + # SWF actions are not dispatched via URLs but via a specific header called + # "x-amz-target", in the form of com.amazonaws.swf.service.model.SimpleWorkflowService. + # This is not supported directly in BaseResponse sor for now we override + # the call_action() method + # See: http://docs.aws.amazon.com/amazonswf/latest/developerguide/UsingJSON-swf.html + def call_action(self): + headers = self.response_headers + # Headers are case-insensitive. Probably a better way to do this. + match = self.headers.get('x-amz-target') or self.headers.get('X-Amz-Target') + if match: + # TODO: see if we can call "[-1]" in BaseResponse, which would + # allow to remove that + action = match.split(".")[-1] + + action = camelcase_to_underscores(action) + method_names = method_names_from_class(self.__class__) + if action in method_names: + method = getattr(self, action) + try: + response = method() + except HTTPException as http_error: + response = http_error.description, dict(status=http_error.code) + if isinstance(response, six.string_types): + return 200, headers, response + else: + body, new_headers = response + status = new_headers.get('status', 200) + headers.update(new_headers) + return status, headers, body + raise NotImplementedError("The {0} action has not been implemented".format(action)) + + # SWF parameters are passed through a JSON body, so let's ease retrieval + @property + def _params(self): + return json.loads(self.body) + + def list_domains(self): + status = self._params.get("registrationStatus") + domains = self.swf_backend.list_domains(status) + template = self.response_template(LIST_DOMAINS_TEMPLATE) + return template.render(domains=domains) + + def register_domain(self): + name = self._params.get("name") + description = self._params.get("description") + retention = self._params.get("workflowExecutionRetentionPeriodInDays") + domain = self.swf_backend.register_domain(name, retention, + description=description) + template = self.response_template("") + return template.render() + + def deprecate_domain(self): + name = self._params.get("name") + domain = self.swf_backend.deprecate_domain(name) + template = self.response_template("") + return template.render() + + def describe_domain(self): + name = self._params.get("name") + domain = self.swf_backend.describe_domain(name) + template = self.response_template(DESCRIBE_DOMAIN_TEMPLATE) + return template.render(domain=domain) + + +LIST_DOMAINS_TEMPLATE = """{ + "domainInfos": [ + {% for domain in domains %} + { + "description": "{{ domain.description }}", + "name": "{{ domain.name }}", + "status": "{{ domain.status }}" + } + {% endfor %} + ] +}""" + +DESCRIBE_DOMAIN_TEMPLATE = """{ + "configuration": { + "workflowExecutionRetentionPeriodInDays": "{{ domain.retention }}" + }, + "domainInfo": { + "description": "{{ domain.description }}", + "name": "{{ domain.name }}", + "status": "{{ domain.status }}" + } +}""" diff --git a/moto/swf/urls.py b/moto/swf/urls.py new file mode 100644 index 000000000..582c874fc --- /dev/null +++ b/moto/swf/urls.py @@ -0,0 +1,9 @@ +from .responses import SWFResponse + +url_bases = [ + "https?://swf.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': SWFResponse.dispatch, +} diff --git a/tests/test_swf/test_swf.py b/tests/test_swf/test_swf.py new file mode 100644 index 000000000..4f0617fc1 --- /dev/null +++ b/tests/test_swf/test_swf.py @@ -0,0 +1,113 @@ +import boto +from nose.tools import assert_raises +from sure import expect + +from moto import mock_swf +from moto.swf.exceptions import ( + SWFUnknownResourceFault, + SWFDomainAlreadyExistsFault, + SWFDomainDeprecatedFault, +) + + +# RegisterDomain endpoint +# ListDomain endpoint +@mock_swf +def test_register_domain(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", 60, description="A test domain") + + all_domains = conn.list_domains("REGISTERED") + domain = all_domains["domainInfos"][0] + + domain["name"].should.equal("test-domain") + domain["status"].should.equal("REGISTERED") + domain["description"].should.equal("A test domain") + +@mock_swf +def test_register_already_existing_domain(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", 60, description="A test domain") + + with assert_raises(SWFDomainAlreadyExistsFault) as err: + conn.register_domain("test-domain", 60, description="A test domain") + + ex = err.exception + ex.status.should.equal(400) + ex.error_code.should.equal("DomainAlreadyExistsFault") + ex.body.should.equal({ + "__type": "com.amazonaws.swf.base.model#DomainAlreadyExistsFault", + "message": "test-domain" + }) + + +# DeprecateDomain endpoint +@mock_swf +def test_deprecate_domain(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", 60, description="A test domain") + conn.deprecate_domain("test-domain") + + all_domains = conn.list_domains("DEPRECATED") + domain = all_domains["domainInfos"][0] + + domain["name"].should.equal("test-domain") + +@mock_swf +def test_deprecate_already_deprecated_domain(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", 60, description="A test domain") + conn.deprecate_domain("test-domain") + + with assert_raises(SWFDomainDeprecatedFault) as err: + conn.deprecate_domain("test-domain") + + ex = err.exception + ex.status.should.equal(400) + ex.error_code.should.equal("DomainDeprecatedFault") + ex.body.should.equal({ + "__type": "com.amazonaws.swf.base.model#DomainDeprecatedFault", + "message": "test-domain" + }) + +@mock_swf +def test_deprecate_non_existent_domain(): + conn = boto.connect_swf("the_key", "the_secret") + + with assert_raises(SWFUnknownResourceFault) as err: + conn.deprecate_domain("non-existent") + + ex = err.exception + ex.status.should.equal(400) + ex.error_code.should.equal("UnknownResourceFault") + ex.body.should.equal({ + "__type": "com.amazonaws.swf.base.model#UnknownResourceFault", + "message": "Unknown domain: non-existent" + }) + +# DescribeDomain endpoint +@mock_swf +def test_describe_domain(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", 60, description="A test domain") + + domain = conn.describe_domain("test-domain") + domain["configuration"]["workflowExecutionRetentionPeriodInDays"].should.equal("60") + domain["domainInfo"]["description"].should.equal("A test domain") + domain["domainInfo"]["name"].should.equal("test-domain") + domain["domainInfo"]["status"].should.equal("REGISTERED") + +@mock_swf +def test_describe_non_existent_domain(): + conn = boto.connect_swf("the_key", "the_secret") + + with assert_raises(SWFUnknownResourceFault) as err: + conn.describe_domain("non-existent") + + ex = err.exception + ex.status.should.equal(400) + ex.error_code.should.equal("UnknownResourceFault") + ex.body.should.equal({ + "__type": "com.amazonaws.swf.base.model#UnknownResourceFault", + "message": "Unknown domain: non-existent" + })