diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md index f889b54f4..1ad96aeb4 100644 --- a/IMPLEMENTATION_COVERAGE.md +++ b/IMPLEMENTATION_COVERAGE.md @@ -6050,24 +6050,24 @@ ## stepfunctions 0% implemented - [ ] create_activity -- [ ] create_state_machine +- [X] create_state_machine - [ ] delete_activity -- [ ] delete_state_machine +- [X] delete_state_machine - [ ] describe_activity -- [ ] describe_execution -- [ ] describe_state_machine -- [ ] describe_state_machine_for_execution +- [X] describe_execution +- [X] describe_state_machine +- [x] describe_state_machine_for_execution - [ ] get_activity_task - [ ] get_execution_history - [ ] list_activities -- [ ] list_executions -- [ ] list_state_machines -- [ ] list_tags_for_resource +- [X] list_executions +- [X] list_state_machines +- [X] list_tags_for_resource - [ ] send_task_failure - [ ] send_task_heartbeat - [ ] send_task_success -- [ ] start_execution -- [ ] stop_execution +- [X] start_execution +- [X] stop_execution - [ ] tag_resource - [ ] untag_resource - [ ] update_state_machine diff --git a/docs/index.rst b/docs/index.rst index 4811fb797..6311597fe 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -94,6 +94,8 @@ Currently implemented Services: +---------------------------+-----------------------+------------------------------------+ | SES | @mock_ses | all endpoints done | +---------------------------+-----------------------+------------------------------------+ +| SFN | @mock_stepfunctions | basic endpoints done | ++---------------------------+-----------------------+------------------------------------+ | SNS | @mock_sns | all endpoints done | +---------------------------+-----------------------+------------------------------------+ | SQS | @mock_sqs | core endpoints done | diff --git a/moto/__init__.py b/moto/__init__.py index 8594cedd2..f82a411cf 100644 --- a/moto/__init__.py +++ b/moto/__init__.py @@ -42,6 +42,7 @@ from .ses import mock_ses, mock_ses_deprecated # flake8: noqa from .secretsmanager import mock_secretsmanager # flake8: noqa from .sns import mock_sns, mock_sns_deprecated # flake8: noqa from .sqs import mock_sqs, mock_sqs_deprecated # flake8: noqa +from .stepfunctions import mock_stepfunctions # flake8: noqa from .sts import mock_sts, mock_sts_deprecated # flake8: noqa from .ssm import mock_ssm # flake8: noqa from .route53 import mock_route53, mock_route53_deprecated # flake8: noqa diff --git a/moto/backends.py b/moto/backends.py index 6ea85093d..8a20697c2 100644 --- a/moto/backends.py +++ b/moto/backends.py @@ -40,6 +40,7 @@ from moto.secretsmanager import secretsmanager_backends from moto.sns import sns_backends from moto.sqs import sqs_backends from moto.ssm import ssm_backends +from moto.stepfunctions import stepfunction_backends from moto.sts import sts_backends from moto.swf import swf_backends from moto.xray import xray_backends @@ -91,6 +92,7 @@ BACKENDS = { 'sns': sns_backends, 'sqs': sqs_backends, 'ssm': ssm_backends, + 'stepfunctions': stepfunction_backends, 'sts': sts_backends, 'swf': swf_backends, 'route53': route53_backends, diff --git a/moto/stepfunctions/__init__.py b/moto/stepfunctions/__init__.py new file mode 100644 index 000000000..dc2b0ba13 --- /dev/null +++ b/moto/stepfunctions/__init__.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +from .models import stepfunction_backends +from ..core.models import base_decorator + +stepfunction_backend = stepfunction_backends['us-east-1'] +mock_stepfunctions = base_decorator(stepfunction_backends) diff --git a/moto/stepfunctions/exceptions.py b/moto/stepfunctions/exceptions.py new file mode 100644 index 000000000..8af4686c7 --- /dev/null +++ b/moto/stepfunctions/exceptions.py @@ -0,0 +1,35 @@ +from __future__ import unicode_literals +import json + + +class AWSError(Exception): + TYPE = None + STATUS = 400 + + def __init__(self, message, type=None, status=None): + self.message = message + self.type = type if type is not None else self.TYPE + self.status = status if status is not None else self.STATUS + + def response(self): + return json.dumps({'__type': self.type, 'message': self.message}), dict(status=self.status) + + +class ExecutionDoesNotExist(AWSError): + TYPE = 'ExecutionDoesNotExist' + STATUS = 400 + + +class InvalidArn(AWSError): + TYPE = 'InvalidArn' + STATUS = 400 + + +class InvalidName(AWSError): + TYPE = 'InvalidName' + STATUS = 400 + + +class StateMachineDoesNotExist(AWSError): + TYPE = 'StateMachineDoesNotExist' + STATUS = 400 diff --git a/moto/stepfunctions/models.py b/moto/stepfunctions/models.py new file mode 100644 index 000000000..7784919b0 --- /dev/null +++ b/moto/stepfunctions/models.py @@ -0,0 +1,162 @@ +import boto +import re +from datetime import datetime +from moto.core import BaseBackend +from moto.core.utils import iso_8601_datetime_without_milliseconds +from moto.sts.models import ACCOUNT_ID +from uuid import uuid4 +from .exceptions import ExecutionDoesNotExist, InvalidArn, InvalidName, StateMachineDoesNotExist + + +class StateMachine(): + def __init__(self, arn, name, definition, roleArn, tags=None): + self.creation_date = iso_8601_datetime_without_milliseconds(datetime.now()) + self.arn = arn + self.name = name + self.definition = definition + self.roleArn = roleArn + self.tags = tags + + +class Execution(): + def __init__(self, region_name, account_id, state_machine_name, execution_name, state_machine_arn): + execution_arn = 'arn:aws:states:{}:{}:execution:{}:{}' + execution_arn = execution_arn.format(region_name, account_id, state_machine_name, execution_name) + self.execution_arn = execution_arn + self.name = execution_name + self.start_date = iso_8601_datetime_without_milliseconds(datetime.now()) + self.state_machine_arn = state_machine_arn + self.status = 'RUNNING' + self.stop_date = None + + def stop(self): + self.status = 'SUCCEEDED' + self.stop_date = iso_8601_datetime_without_milliseconds(datetime.now()) + + +class StepFunctionBackend(BaseBackend): + + # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/stepfunctions.html#SFN.Client.create_state_machine + # A name must not contain: + # whitespace + # brackets < > { } [ ] + # wildcard characters ? * + # special characters " # % \ ^ | ~ ` $ & , ; : / + invalid_chars_for_name = [' ', '{', '}', '[', ']', '<', '>', + '?', '*', + '"', '#', '%', '\\', '^', '|', '~', '`', '$', '&', ',', ';', ':', '/'] + # control characters (U+0000-001F , U+007F-009F ) + invalid_unicodes_for_name = [u'\u0000', u'\u0001', u'\u0002', u'\u0003', u'\u0004', + u'\u0005', u'\u0006', u'\u0007', u'\u0008', u'\u0009', + u'\u000A', u'\u000B', u'\u000C', u'\u000D', u'\u000E', u'\u000F', + u'\u0010', u'\u0011', u'\u0012', u'\u0013', u'\u0014', + u'\u0015', u'\u0016', u'\u0017', u'\u0018', u'\u0019', + u'\u001A', u'\u001B', u'\u001C', u'\u001D', u'\u001E', u'\u001F', + u'\u007F', + u'\u0080', u'\u0081', u'\u0082', u'\u0083', u'\u0084', u'\u0085', + u'\u0086', u'\u0087', u'\u0088', u'\u0089', + u'\u008A', u'\u008B', u'\u008C', u'\u008D', u'\u008E', u'\u008F', + u'\u0090', u'\u0091', u'\u0092', u'\u0093', u'\u0094', u'\u0095', + u'\u0096', u'\u0097', u'\u0098', u'\u0099', + u'\u009A', u'\u009B', u'\u009C', u'\u009D', u'\u009E', u'\u009F'] + accepted_role_arn_format = re.compile('arn:aws:iam:(?P[0-9]{12}):role/.+') + accepted_mchn_arn_format = re.compile('arn:aws:states:[-0-9a-zA-Z]+:(?P[0-9]{12}):stateMachine:.+') + accepted_exec_arn_format = re.compile('arn:aws:states:[-0-9a-zA-Z]+:(?P[0-9]{12}):execution:.+') + + def __init__(self, region_name): + self.state_machines = [] + self.executions = [] + self.region_name = region_name + self._account_id = None + + def create_state_machine(self, name, definition, roleArn, tags=None): + self._validate_name(name) + self._validate_role_arn(roleArn) + arn = 'arn:aws:states:' + self.region_name + ':' + str(self._get_account_id()) + ':stateMachine:' + name + try: + return self.describe_state_machine(arn) + except StateMachineDoesNotExist: + state_machine = StateMachine(arn, name, definition, roleArn, tags) + self.state_machines.append(state_machine) + return state_machine + + def list_state_machines(self): + return self.state_machines + + def describe_state_machine(self, arn): + self._validate_machine_arn(arn) + sm = next((x for x in self.state_machines if x.arn == arn), None) + if not sm: + raise StateMachineDoesNotExist("State Machine Does Not Exist: '" + arn + "'") + return sm + + def delete_state_machine(self, arn): + self._validate_machine_arn(arn) + sm = next((x for x in self.state_machines if x.arn == arn), None) + if sm: + self.state_machines.remove(sm) + + def start_execution(self, state_machine_arn): + state_machine_name = self.describe_state_machine(state_machine_arn).name + execution = Execution(region_name=self.region_name, + account_id=self._get_account_id(), + state_machine_name=state_machine_name, + execution_name=str(uuid4()), + state_machine_arn=state_machine_arn) + self.executions.append(execution) + return execution + + def stop_execution(self, execution_arn): + execution = next((x for x in self.executions if x.execution_arn == execution_arn), None) + if not execution: + raise ExecutionDoesNotExist("Execution Does Not Exist: '" + execution_arn + "'") + execution.stop() + return execution + + def list_executions(self, state_machine_arn): + return [execution for execution in self.executions if execution.state_machine_arn == state_machine_arn] + + def describe_execution(self, arn): + self._validate_execution_arn(arn) + exctn = next((x for x in self.executions if x.execution_arn == arn), None) + if not exctn: + raise ExecutionDoesNotExist("Execution Does Not Exist: '" + arn + "'") + return exctn + + def reset(self): + region_name = self.region_name + self.__dict__ = {} + self.__init__(region_name) + + def _validate_name(self, name): + if any(invalid_char in name for invalid_char in self.invalid_chars_for_name): + raise InvalidName("Invalid Name: '" + name + "'") + + if any(name.find(char) >= 0 for char in self.invalid_unicodes_for_name): + raise InvalidName("Invalid Name: '" + name + "'") + + def _validate_role_arn(self, role_arn): + self._validate_arn(arn=role_arn, + regex=self.accepted_role_arn_format, + invalid_msg="Invalid Role Arn: '" + role_arn + "'") + + def _validate_machine_arn(self, machine_arn): + self._validate_arn(arn=machine_arn, + regex=self.accepted_mchn_arn_format, + invalid_msg="Invalid Role Arn: '" + machine_arn + "'") + + def _validate_execution_arn(self, execution_arn): + self._validate_arn(arn=execution_arn, + regex=self.accepted_exec_arn_format, + invalid_msg="Execution Does Not Exist: '" + execution_arn + "'") + + def _validate_arn(self, arn, regex, invalid_msg): + match = regex.match(arn) + if not arn or not match: + raise InvalidArn(invalid_msg) + + def _get_account_id(self): + return ACCOUNT_ID + + +stepfunction_backends = {_region.name: StepFunctionBackend(_region.name) for _region in boto.awslambda.regions()} diff --git a/moto/stepfunctions/responses.py b/moto/stepfunctions/responses.py new file mode 100644 index 000000000..0a170aa57 --- /dev/null +++ b/moto/stepfunctions/responses.py @@ -0,0 +1,138 @@ +from __future__ import unicode_literals + +import json + +from moto.core.responses import BaseResponse +from moto.core.utils import amzn_request_id +from .exceptions import AWSError +from .models import stepfunction_backends + + +class StepFunctionResponse(BaseResponse): + + @property + def stepfunction_backend(self): + return stepfunction_backends[self.region] + + @amzn_request_id + def create_state_machine(self): + name = self._get_param('name') + definition = self._get_param('definition') + roleArn = self._get_param('roleArn') + tags = self._get_param('tags') + try: + state_machine = self.stepfunction_backend.create_state_machine(name=name, definition=definition, + roleArn=roleArn, + tags=tags) + response = { + 'creationDate': state_machine.creation_date, + 'stateMachineArn': state_machine.arn + } + return 200, {}, json.dumps(response) + except AWSError as err: + return err.response() + + @amzn_request_id + def list_state_machines(self): + list_all = self.stepfunction_backend.list_state_machines() + list_all = sorted([{'creationDate': sm.creation_date, + 'name': sm.name, + 'stateMachineArn': sm.arn} for sm in list_all], + key=lambda x: x['name']) + response = {'stateMachines': list_all} + return 200, {}, json.dumps(response) + + @amzn_request_id + def describe_state_machine(self): + arn = self._get_param('stateMachineArn') + return self._describe_state_machine(arn) + + @amzn_request_id + def _describe_state_machine(self, state_machine_arn): + try: + state_machine = self.stepfunction_backend.describe_state_machine(state_machine_arn) + response = { + 'creationDate': state_machine.creation_date, + 'stateMachineArn': state_machine.arn, + 'definition': state_machine.definition, + 'name': state_machine.name, + 'roleArn': state_machine.roleArn, + 'status': 'ACTIVE' + } + return 200, {}, json.dumps(response) + except AWSError as err: + return err.response() + + @amzn_request_id + def delete_state_machine(self): + arn = self._get_param('stateMachineArn') + try: + self.stepfunction_backend.delete_state_machine(arn) + return 200, {}, json.dumps('{}') + except AWSError as err: + return err.response() + + @amzn_request_id + def list_tags_for_resource(self): + arn = self._get_param('resourceArn') + try: + state_machine = self.stepfunction_backend.describe_state_machine(arn) + tags = state_machine.tags or [] + except AWSError: + tags = [] + response = {'tags': tags} + return 200, {}, json.dumps(response) + + @amzn_request_id + def start_execution(self): + arn = self._get_param('stateMachineArn') + execution = self.stepfunction_backend.start_execution(arn) + response = {'executionArn': execution.execution_arn, + 'startDate': execution.start_date} + return 200, {}, json.dumps(response) + + @amzn_request_id + def list_executions(self): + arn = self._get_param('stateMachineArn') + state_machine = self.stepfunction_backend.describe_state_machine(arn) + executions = self.stepfunction_backend.list_executions(arn) + executions = [{'executionArn': execution.execution_arn, + 'name': execution.name, + 'startDate': execution.start_date, + 'stateMachineArn': state_machine.arn, + 'status': execution.status} for execution in executions] + return 200, {}, json.dumps({'executions': executions}) + + @amzn_request_id + def describe_execution(self): + arn = self._get_param('executionArn') + try: + execution = self.stepfunction_backend.describe_execution(arn) + response = { + 'executionArn': arn, + 'input': '{}', + 'name': execution.name, + 'startDate': execution.start_date, + 'stateMachineArn': execution.state_machine_arn, + 'status': execution.status, + 'stopDate': execution.stop_date + } + return 200, {}, json.dumps(response) + except AWSError as err: + return err.response() + + @amzn_request_id + def describe_state_machine_for_execution(self): + arn = self._get_param('executionArn') + try: + execution = self.stepfunction_backend.describe_execution(arn) + return self._describe_state_machine(execution.state_machine_arn) + except AWSError as err: + return err.response() + + @amzn_request_id + def stop_execution(self): + arn = self._get_param('executionArn') + execution = self.stepfunction_backend.stop_execution(arn) + response = {'stopDate': execution.stop_date} + return 200, {}, json.dumps(response) diff --git a/moto/stepfunctions/urls.py b/moto/stepfunctions/urls.py new file mode 100644 index 000000000..f8d5fb1e8 --- /dev/null +++ b/moto/stepfunctions/urls.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from .responses import StepFunctionResponse + +url_bases = [ + "https?://states.(.+).amazonaws.com", +] + +url_paths = { + '{0}/$': StepFunctionResponse.dispatch, +} diff --git a/tests/test_stepfunctions/test_stepfunctions.py b/tests/test_stepfunctions/test_stepfunctions.py new file mode 100644 index 000000000..10953ce2d --- /dev/null +++ b/tests/test_stepfunctions/test_stepfunctions.py @@ -0,0 +1,378 @@ +from __future__ import unicode_literals + +import boto3 +import sure # noqa +import datetime + +from datetime import datetime +from botocore.exceptions import ClientError +from nose.tools import assert_raises + +from moto import mock_sts, mock_stepfunctions + + +region = 'us-east-1' +simple_definition = '{"Comment": "An example of the Amazon States Language using a choice state.",' \ + '"StartAt": "DefaultState",' \ + '"States": ' \ + '{"DefaultState": {"Type": "Fail","Error": "DefaultStateError","Cause": "No Matches!"}}}' +account_id = None + + +@mock_stepfunctions +@mock_sts +def test_state_machine_creation_succeeds(): + client = boto3.client('stepfunctions', region_name=region) + name = 'example_step_function' + # + response = client.create_state_machine(name=name, + definition=str(simple_definition), + roleArn=_get_default_role()) + # + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + response['creationDate'].should.be.a(datetime) + response['stateMachineArn'].should.equal('arn:aws:states:' + region + ':123456789012:stateMachine:' + name) + + +@mock_stepfunctions +def test_state_machine_creation_fails_with_invalid_names(): + client = boto3.client('stepfunctions', region_name=region) + invalid_names = [ + 'with space', + 'withbracket', 'with{bracket', 'with}bracket', 'with[bracket', 'with]bracket', + 'with?wildcard', 'with*wildcard', + 'special"char', 'special#char', 'special%char', 'special\\char', 'special^char', 'special|char', + 'special~char', 'special`char', 'special$char', 'special&char', 'special,char', 'special;char', + 'special:char', 'special/char', + u'uni\u0000code', u'uni\u0001code', u'uni\u0002code', u'uni\u0003code', u'uni\u0004code', + u'uni\u0005code', u'uni\u0006code', u'uni\u0007code', u'uni\u0008code', u'uni\u0009code', + u'uni\u000Acode', u'uni\u000Bcode', u'uni\u000Ccode', + u'uni\u000Dcode', u'uni\u000Ecode', u'uni\u000Fcode', + u'uni\u0010code', u'uni\u0011code', u'uni\u0012code', u'uni\u0013code', u'uni\u0014code', + u'uni\u0015code', u'uni\u0016code', u'uni\u0017code', u'uni\u0018code', u'uni\u0019code', + u'uni\u001Acode', u'uni\u001Bcode', u'uni\u001Ccode', + u'uni\u001Dcode', u'uni\u001Ecode', u'uni\u001Fcode', + u'uni\u007Fcode', + u'uni\u0080code', u'uni\u0081code', u'uni\u0082code', u'uni\u0083code', u'uni\u0084code', + u'uni\u0085code', u'uni\u0086code', u'uni\u0087code', u'uni\u0088code', u'uni\u0089code', + u'uni\u008Acode', u'uni\u008Bcode', u'uni\u008Ccode', + u'uni\u008Dcode', u'uni\u008Ecode', u'uni\u008Fcode', + u'uni\u0090code', u'uni\u0091code', u'uni\u0092code', u'uni\u0093code', u'uni\u0094code', + u'uni\u0095code', u'uni\u0096code', u'uni\u0097code', u'uni\u0098code', u'uni\u0099code', + u'uni\u009Acode', u'uni\u009Bcode', u'uni\u009Ccode', + u'uni\u009Dcode', u'uni\u009Ecode', u'uni\u009Fcode'] + # + + for invalid_name in invalid_names: + with assert_raises(ClientError) as exc: + client.create_state_machine(name=invalid_name, + definition=str(simple_definition), + roleArn=_get_default_role()) + + +@mock_stepfunctions +def test_state_machine_creation_requires_valid_role_arn(): + client = boto3.client('stepfunctions', region_name=region) + name = 'example_step_function' + # + with assert_raises(ClientError) as exc: + client.create_state_machine(name=name, + definition=str(simple_definition), + roleArn='arn:aws:iam:1234:role/unknown_role') + + +@mock_stepfunctions +def test_state_machine_list_returns_empty_list_by_default(): + client = boto3.client('stepfunctions', region_name=region) + # + list = client.list_state_machines() + list['stateMachines'].should.be.empty + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_returns_created_state_machines(): + client = boto3.client('stepfunctions', region_name=region) + # + machine2 = client.create_state_machine(name='name2', + definition=str(simple_definition), + roleArn=_get_default_role()) + machine1 = client.create_state_machine(name='name1', + definition=str(simple_definition), + roleArn=_get_default_role(), + tags=[{'key': 'tag_key', 'value': 'tag_value'}]) + list = client.list_state_machines() + # + list['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + list['stateMachines'].should.have.length_of(2) + list['stateMachines'][0]['creationDate'].should.be.a(datetime) + list['stateMachines'][0]['creationDate'].should.equal(machine1['creationDate']) + list['stateMachines'][0]['name'].should.equal('name1') + list['stateMachines'][0]['stateMachineArn'].should.equal(machine1['stateMachineArn']) + list['stateMachines'][1]['creationDate'].should.be.a(datetime) + list['stateMachines'][1]['creationDate'].should.equal(machine2['creationDate']) + list['stateMachines'][1]['name'].should.equal('name2') + list['stateMachines'][1]['stateMachineArn'].should.equal(machine2['stateMachineArn']) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_creation_is_idempotent_by_name(): + client = boto3.client('stepfunctions', region_name=region) + # + client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + sm_list = client.list_state_machines() + sm_list['stateMachines'].should.have.length_of(1) + # + client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + sm_list = client.list_state_machines() + sm_list['stateMachines'].should.have.length_of(1) + # + client.create_state_machine(name='diff_name', definition=str(simple_definition), roleArn=_get_default_role()) + sm_list = client.list_state_machines() + sm_list['stateMachines'].should.have.length_of(2) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_creation_can_be_described(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + desc = client.describe_state_machine(stateMachineArn=sm['stateMachineArn']) + desc['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + desc['creationDate'].should.equal(sm['creationDate']) + desc['definition'].should.equal(str(simple_definition)) + desc['name'].should.equal('name') + desc['roleArn'].should.equal(_get_default_role()) + desc['stateMachineArn'].should.equal(sm['stateMachineArn']) + desc['status'].should.equal('ACTIVE') + + +@mock_stepfunctions +@mock_sts +def test_state_machine_throws_error_when_describing_unknown_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_state_machine = 'arn:aws:states:' + region + ':' + _get_account_id() + ':stateMachine:unknown' + client.describe_state_machine(stateMachineArn=unknown_state_machine) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_throws_error_when_describing_machine_in_different_account(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_state_machine = 'arn:aws:states:' + region + ':000000000000:stateMachine:unknown' + client.describe_state_machine(stateMachineArn=unknown_state_machine) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_can_be_deleted(): + client = boto3.client('stepfunctions', region_name=region) + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + # + response = client.delete_state_machine(stateMachineArn=sm['stateMachineArn']) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + # + sm_list = client.list_state_machines() + sm_list['stateMachines'].should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_can_deleted_nonexisting_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + unknown_state_machine = 'arn:aws:states:' + region + ':123456789012:stateMachine:unknown' + response = client.delete_state_machine(stateMachineArn=unknown_state_machine) + response['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + # + sm_list = client.list_state_machines() + sm_list['stateMachines'].should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_tags_for_created_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + machine = client.create_state_machine(name='name1', + definition=str(simple_definition), + roleArn=_get_default_role(), + tags=[{'key': 'tag_key', 'value': 'tag_value'}]) + response = client.list_tags_for_resource(resourceArn=machine['stateMachineArn']) + tags = response['tags'] + tags.should.have.length_of(1) + tags[0].should.equal({'key': 'tag_key', 'value': 'tag_value'}) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_tags_for_machine_without_tags(): + client = boto3.client('stepfunctions', region_name=region) + # + machine = client.create_state_machine(name='name1', + definition=str(simple_definition), + roleArn=_get_default_role()) + response = client.list_tags_for_resource(resourceArn=machine['stateMachineArn']) + tags = response['tags'] + tags.should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_tags_for_nonexisting_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + non_existing_state_machine = 'arn:aws:states:' + region + ':' + _get_account_id() + ':stateMachine:unknown' + response = client.list_tags_for_resource(resourceArn=non_existing_state_machine) + tags = response['tags'] + tags.should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_start_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + # + execution['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + expected_exec_name = 'arn:aws:states:' + region + ':' + _get_account_id() + ':execution:name:[a-zA-Z0-9-]+' + execution['executionArn'].should.match(expected_exec_name) + execution['startDate'].should.be.a(datetime) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_executions(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + execution_arn = execution['executionArn'] + execution_name = execution_arn[execution_arn.rindex(':')+1:] + executions = client.list_executions(stateMachineArn=sm['stateMachineArn']) + # + executions['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + executions['executions'].should.have.length_of(1) + executions['executions'][0]['executionArn'].should.equal(execution_arn) + executions['executions'][0]['name'].should.equal(execution_name) + executions['executions'][0]['startDate'].should.equal(execution['startDate']) + executions['executions'][0]['stateMachineArn'].should.equal(sm['stateMachineArn']) + executions['executions'][0]['status'].should.equal('RUNNING') + executions['executions'][0].shouldnt.have('stopDate') + + +@mock_stepfunctions +@mock_sts +def test_state_machine_list_executions_when_none_exist(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + executions = client.list_executions(stateMachineArn=sm['stateMachineArn']) + # + executions['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + executions['executions'].should.have.length_of(0) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_describe_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + description = client.describe_execution(executionArn=execution['executionArn']) + # + description['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + description['executionArn'].should.equal(execution['executionArn']) + description['input'].should.equal("{}") + description['name'].shouldnt.be.empty + description['startDate'].should.equal(execution['startDate']) + description['stateMachineArn'].should.equal(sm['stateMachineArn']) + description['status'].should.equal('RUNNING') + description.shouldnt.have('stopDate') + + +@mock_stepfunctions +@mock_sts +def test_state_machine_throws_error_when_describing_unknown_machine(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_execution = 'arn:aws:states:' + region + ':' + _get_account_id() + ':execution:unknown' + client.describe_execution(executionArn=unknown_execution) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_can_be_described_by_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + desc = client.describe_state_machine_for_execution(executionArn=execution['executionArn']) + desc['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + desc['definition'].should.equal(str(simple_definition)) + desc['name'].should.equal('name') + desc['roleArn'].should.equal(_get_default_role()) + desc['stateMachineArn'].should.equal(sm['stateMachineArn']) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_throws_error_when_describing_unknown_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + with assert_raises(ClientError) as exc: + unknown_execution = 'arn:aws:states:' + region + ':' + _get_account_id() + ':execution:unknown' + client.describe_state_machine_for_execution(executionArn=unknown_execution) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_stop_execution(): + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + start = client.start_execution(stateMachineArn=sm['stateMachineArn']) + stop = client.stop_execution(executionArn=start['executionArn']) + # + stop['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + stop['stopDate'].should.be.a(datetime) + + +@mock_stepfunctions +@mock_sts +def test_state_machine_describe_execution_after_stoppage(): + account_id + client = boto3.client('stepfunctions', region_name=region) + # + sm = client.create_state_machine(name='name', definition=str(simple_definition), roleArn=_get_default_role()) + execution = client.start_execution(stateMachineArn=sm['stateMachineArn']) + client.stop_execution(executionArn=execution['executionArn']) + description = client.describe_execution(executionArn=execution['executionArn']) + # + description['ResponseMetadata']['HTTPStatusCode'].should.equal(200) + description['status'].should.equal('SUCCEEDED') + description['stopDate'].should.be.a(datetime) + + +def _get_account_id(): + global account_id + if account_id: + return account_id + sts = boto3.client("sts") + identity = sts.get_caller_identity() + account_id = identity['Account'] + return account_id + + +def _get_default_role(): + return 'arn:aws:iam:' + _get_account_id() + ':role/unknown_sf_role'