Example #1
0
class TestStack(object):

    @patch("sceptre.stack.Stack.config")
    def setup_method(self, test_method, mock_config):
        self.mock_environment_config = MagicMock(spec=Config)
        self.mock_environment_config.environment_path = sentinel.path
        # environment config is an object which inherits from dict. Its
        # attributes are accessable via dot and square bracket notation.
        # In order to mimic the behaviour of the square bracket notation,
        # a side effect is used to return the expected value from the call to
        # __getitem__ that the square bracket notation makes.
        self.mock_environment_config.__getitem__.side_effect = [
            sentinel.project_code,
            sentinel.region
        ]
        self.mock_connection_manager = Mock()

        self.stack = Stack(
            name="stack_name",
            environment_config=self.mock_environment_config,
            connection_manager=self.mock_connection_manager
        )

        # Set default value for stack properties
        self.stack._external_name = sentinel.external_name

    def test_initiate_stack(self):
        assert self.stack.name == "stack_name"
        assert self.stack.environment_config == self.mock_environment_config
        assert self.stack.project == sentinel.project_code
        assert self.stack._environment_path == sentinel.path
        assert self.stack._config is None
        assert self.stack._template is None
        assert self.stack.region == sentinel.region
        assert self.stack.connection_manager == self.mock_connection_manager
        assert self.stack._hooks is None
        assert self.stack._dependencies is None

    @patch("sceptre.stack.Stack.config")
    def test_initialiser_calls_correct_methods(self, mock_config):
        mock_config.get.return_value = sentinel.hooks
        self.stack._config = {
            "parameters": sentinel.parameters,
            "hooks": sentinel.hooks
        }
        self.mock_environment_config = MagicMock(spec=Config)
        self.mock_environment_config.environment_path = sentinel.path
        # environment config is an object which inherits from dict. Its
        # attributes are accessable via dot and square bracket notation.
        # In order to mimic the behaviour of the square bracket notation,
        # a side effect is used to return the expected value from the call to
        # __getitem__ that the square bracket notation makes.
        self.mock_environment_config.__getitem__.side_effect = [
            sentinel.project_code,
            sentinel.template_bucket_name,
            sentinel.region
        ]

        Stack(
            name=sentinel.name,
            environment_config=self.mock_environment_config,
            connection_manager=sentinel.connection_manager
        )

    def test_repr(self):
        self.stack.name = "stack_name"
        self.stack.environment_config = {"key": "val"}
        self.stack.connection_manager = "connection_manager"
        assert self.stack.__repr__() == \
            "sceptre.stack.Stack(stack_name='stack_name', \
environment_config={'key': 'val'}, connection_manager=connection_manager)"

    @patch("sceptre.stack.Config")
    def test_config_loads_config(self, mock_Config):
        self.stack._config = None
        self.stack.name = "stack"
        # self.stack.environment_config = MagicMock(spec=Config)
        self.stack.environment_config.sceptre_dir = sentinel.sceptre_dir
        self.stack.environment_config.environment_path = \
            sentinel.environment_path
        self.stack.environment_config.get.return_value = \
            sentinel.user_variables
        mock_config = Mock()
        mock_Config.with_yaml_constructors.return_value = mock_config

        response = self.stack.config
        mock_Config.with_yaml_constructors.assert_called_once_with(
            sceptre_dir=sentinel.sceptre_dir,
            environment_path=sentinel.environment_path,
            base_file_name="stack",
            environment_config=self.stack.environment_config,
            connection_manager=self.stack.connection_manager
        )
        mock_config.read.assert_called_once_with(sentinel.user_variables,
                                                 self.stack.environment_config)
        assert response == mock_config

    def test_config_returns_config_if_it_exists(self):
        self.stack._config = sentinel.config
        response = self.stack.config
        assert response == sentinel.config

    def test_dependencies_loads_dependencies(self):
        self.stack.name = "dev/security-group"
        self.stack._config = {
            "dependencies": ["dev/vpc", "dev/vpc", "dev/subnets"]
        }
        dependencies = self.stack.dependencies
        assert dependencies == set(["dev/vpc", "dev/subnets"])

    def test_dependencies_returns_dependencies_if_it_exists(self):
        self.stack._dependencies = sentinel.dependencies
        response = self.stack.dependencies
        assert response == sentinel.dependencies

    def test_hooks_with_no_cache(self):
        self.stack._hooks = None
        self.stack._config = {}
        self.stack._config["hooks"] = sentinel.hooks

        assert self.stack.hooks == sentinel.hooks

    def test_hooks_with_cache(self):
        self.stack._hooks = sentinel.hooks
        assert self.stack.hooks == sentinel.hooks

    @patch("sceptre.stack.Template")
    def test_template_loads_template(self, mock_Template):
        self.stack._template = None
        self.stack.environment_config.sceptre_dir = "sceptre_dir"
        self.stack._config = {
            "template_path": "template_path",
            "sceptre_user_data": sentinel.sceptre_user_data
        }
        mock_Template.return_value = sentinel.template

        response = self.stack.template

        mock_Template.assert_called_once_with(
            path="sceptre_dir/template_path",
            sceptre_user_data=sentinel.sceptre_user_data
        )
        assert response == sentinel.template

    def test_template_returns_template_if_it_exists(self):
        self.stack._template = sentinel.template
        response = self.stack.template
        assert response == sentinel.template

    @patch("sceptre.stack.get_external_stack_name")
    def test_external_name_with_custom_stack_name(
            self, mock_get_external_stack_name
    ):
        self.stack._external_name = None

        self.stack._config = {"stack_name": "custom_stack_name"}
        external_name = self.stack.external_name
        assert external_name == "custom_stack_name"

    def test_external_name_without_custom_name(self):
        self.stack._external_name = None
        self.stack.project = "project"
        self.stack.name = "stack-name"
        self.stack._config = {}

        external_name = self.stack.external_name
        assert external_name == "project-stack-name"

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_create_sends_correct_request(
        self, mock_get_template_details,
        mock_wait_for_completion, mock_format_params
    ):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {"stack_tags": {
            "tag1": "val1"
        }}
        self.stack._hooks = {}
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.config["notifications"] = [sentinel.notification]
        self.stack.create()

        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [sentinel.notification],
                "Tags": [
                    {"Key": "tag1", "Value": "val1"}
                ]
            }
        )
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_create_sends_correct_request_no_notifications(
            self, mock_get_template_details,
            mock_wait_for_completion, mock_format_params
    ):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {"stack_tags": {
            "tag1": "val1"
        }}
        self.stack._hooks = {}
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.create()

        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [],
                "Tags": [
                    {"Key": "tag1", "Value": "val1"}
                ]
            }
        )
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_create_sends_correct_request_with_failure(
        self, mock_get_template_details,
        mock_wait_for_completion, mock_format_params
    ):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {"stack_tags": {
            "tag1": "val1"
        }}
        self.stack._hooks = {}
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.config["notifications"] = [sentinel.notification]
        self.stack.config["on_failure"] = 'DO_NOTHING'
        self.stack.create()

        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [sentinel.notification],
                "Tags": [
                    {"Key": "tag1", "Value": "val1"}
                ],
                "OnFailure": 'DO_NOTHING'
            }
        )
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_update_sends_correct_request(
        self, mock_get_template_details,
        mock_wait_for_completion, mock_format_params
    ):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {"stack_tags": {
            "tag1": "val1"
        }}
        self.stack._hooks = {}
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.config["notifications"] = [sentinel.notification]

        self.stack.update()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="update_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [sentinel.notification],
                "Tags": [
                    {"Key": "tag1", "Value": "val1"}
                ]
            }
        )
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_update_sends_correct_request_no_notification(
            self, mock_get_template_details,
            mock_wait_for_completion, mock_format_params
    ):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {"stack_tags": {
            "tag1": "val1"
        }}
        self.stack._hooks = {}
        self.stack.config["role_arn"] = sentinel.role_arn

        self.stack.update()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="update_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [],
                "Tags": [
                    {"Key": "tag1", "Value": "val1"}
                ]
            }
        )
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.create")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_stack_that_does_not_exist(
            self, mock_get_status, mock_create, mock_hooks
    ):
        self.stack._config = {"protect": False}
        mock_get_status.side_effect = StackDoesNotExistError()
        mock_create.return_value = sentinel.launch_response
        response = self.stack.launch()
        mock_create.assert_called_once_with()
        assert response == sentinel.launch_response

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.create")
    @patch("sceptre.stack.Stack.delete")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_stack_that_failed_to_create(
            self, mock_get_status, mock_delete, mock_create, mock_hooks
    ):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_FAILED"
        mock_create.return_value = sentinel.launch_response
        response = self.stack.launch()
        mock_delete.assert_called_once_with()
        mock_create.assert_called_once_with()
        assert response == sentinel.launch_response

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.update")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_complete_stack_with_updates_to_perform(
            self, mock_get_status, mock_update, mock_hooks
    ):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_update.return_value = sentinel.launch_response
        response = self.stack.launch()
        mock_update.assert_called_once_with()
        assert response == sentinel.launch_response

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.update")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_complete_stack_with_no_updates_to_perform(
            self, mock_get_status, mock_update, mock_hooks
    ):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_update.side_effect = ClientError(
            {
                "Error": {
                    "Code": "NoUpdateToPerformError",
                    "Message": "No updates are to be performed."
                }
            },
            sentinel.operation
        )
        response = self.stack.launch()
        mock_update.assert_called_once_with()
        assert response == StackStatus.COMPLETE

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.update")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_complete_stack_with_unknown_client_error(
            self, mock_get_status, mock_update, mock_hooks
    ):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_update.side_effect = ClientError(
            {
                "Error": {
                    "Code": "Boom!",
                    "Message": "Boom!"
                }
            },
            sentinel.operation
        )
        with pytest.raises(ClientError):
            self.stack.launch()

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_in_progress_stack(self, mock_get_status, mock_hooks):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_IN_PROGRESS"
        response = self.stack.launch()
        assert response == StackStatus.IN_PROGRESS

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_failed_stack(self, mock_get_status, mock_hooks):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "UPDATE_FAILED"
        with pytest.raises(CannotUpdateFailedStackError):
            response = self.stack.launch()
            assert response == StackStatus.FAILED

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_unknown_stack_status(
            self, mock_get_status, mock_hooks
    ):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "UNKNOWN_STATUS"
        with pytest.raises(UnknownStackStatusError):
            self.stack.launch()

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_with_created_stack(
            self, mock_get_status, mock_hooks, mock_wait_for_completion
    ):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.delete()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="delete_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "RoleARN": sentinel.role_arn
            }
        )

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_when_wait_for_completion_raises_stack_does_not_exist_error(
            self, mock_get_status, mock_hooks, mock_wait_for_completion
    ):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        self.stack.config["role_arn"] = sentinel.role_arn
        mock_wait_for_completion.side_effect = StackDoesNotExistError()
        status = self.stack.delete()
        assert status == StackStatus.COMPLETE

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_when_wait_for_completion_raises_non_existent_client_error(
            self, mock_get_status, mock_hooks, mock_wait_for_completion
    ):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        self.stack.config["role_arn"] = sentinel.role_arn
        mock_wait_for_completion.side_effect = ClientError(
            {
                "Error": {
                    "Code": "DoesNotExistException",
                    "Message": "Stack does not exist"
                }
            },
            sentinel.operation
        )
        status = self.stack.delete()
        assert status == StackStatus.COMPLETE

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_when_wait_for_completion_raises_unexpected_client_error(
            self, mock_get_status, mock_hooks, mock_wait_for_completion
    ):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        self.stack.config["role_arn"] = sentinel.role_arn
        mock_wait_for_completion.side_effect = ClientError(
            {
                "Error": {
                    "Code": "DoesNotExistException",
                    "Message": "Boom"
                }
            },
            sentinel.operation
        )
        with pytest.raises(ClientError):
            self.stack.delete()

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_with_non_existent_stack(
            self, mock_get_status, mock_hooks, mock_wait_for_completion
    ):
        self.stack._config = {"protect": False}
        mock_get_status.side_effect = StackDoesNotExistError()
        status = self.stack.delete()
        assert status == StackStatus.COMPLETE

    def test_describe_stack_sends_correct_request(self):
        self.stack.describe()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_stacks",
            kwargs={"StackName": sentinel.external_name}
        )

    def test_describe_events_sends_correct_request(self):
        self.stack.describe_events()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_stack_events",
            kwargs={"StackName": sentinel.external_name}
        )

    def test_describe_resources_sends_correct_request(self):
        self.stack.connection_manager.call.return_value = {
            "StackResources": [
                {
                    "LogicalResourceId": sentinel.logical_resource_id,
                    "PhysicalResourceId": sentinel.physical_resource_id,
                    "OtherParam": sentinel.other_param
                }
            ]
        }
        response = self.stack.describe_resources()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_stack_resources",
            kwargs={"StackName": sentinel.external_name}
        )
        assert response == [
            {
                "LogicalResourceId": sentinel.logical_resource_id,
                "PhysicalResourceId": sentinel.physical_resource_id
            }
        ]

    @patch("sceptre.stack.Stack.describe")
    def test_describe_outputs_sends_correct_request(self, mock_describe):
        mock_describe.return_value = {
            "Stacks": [{
                "Outputs": sentinel.outputs
            }]
        }
        response = self.stack.describe_outputs()
        mock_describe.assert_called_once_with()
        assert response == sentinel.outputs

    @patch("sceptre.stack.Stack.describe")
    def test_describe_outputs_handles_stack_with_no_outputs(
            self, mock_describe
    ):
        mock_describe.return_value = {
            "Stacks": [{}]
        }
        response = self.stack.describe_outputs()
        assert response == []

    def test_continue_update_rollback_sends_correct_request(self):
        self.stack._config = {
            "template_path": sentinel.template_path,
        }
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.continue_update_rollback()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="continue_update_rollback",
            kwargs={
                "StackName": sentinel.external_name,
                "RoleARN": sentinel.role_arn
            }
        )

    def test_set_stack_policy_sends_correct_request(self):
        self.stack.set_policy("tests/fixtures/stack_policies/unlock.json")
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="set_stack_policy",
            kwargs={
                "StackName": sentinel.external_name,
                "StackPolicyBody": """{
  "Statement" : [
    {
      "Effect" : "Allow",
      "Action" : "Update:*",
      "Principal": "*",
      "Resource" : "*"
    }
  ]
}
"""
            }
        )

    def test_get_stack_policy_sends_correct_request(self):
        self.stack.get_policy()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="get_stack_policy",
            kwargs={
                "StackName": sentinel.external_name
            }
        )

    @patch("sceptre.stack.Stack._get_template_details")
    def test_validate_template_sends_correct_request(
        self, mock_get_template_details
    ):
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name
        }
        self.stack.validate_template()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="validate_template",
            kwargs={"Template": sentinel.template}
        )

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_create_change_set_sends_correct_request(
        self, mock_get_template_details, mock_format_params
    ):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {
            "stack_tags": {"tag1": "val1"}
        }
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.config["notifications"] = [sentinel.notification]

        self.stack.create_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_change_set",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "ChangeSetName": sentinel.change_set_name,
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [sentinel.notification],
                "Tags": [
                    {"Key": "tag1", "Value": "val1"}
                ]
            }
        )

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_create_change_set_sends_correct_request_no_notifications(
            self, mock_get_template_details, mock_format_params
    ):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {
            "stack_tags": {"tag1": "val1"}
        }
        self.stack.config["role_arn"] = sentinel.role_arn

        self.stack.create_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_change_set",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "ChangeSetName": sentinel.change_set_name,
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [],
                "Tags": [
                    {"Key": "tag1", "Value": "val1"}
                ]
            }
        )

    def test_delete_change_set_sends_correct_request(self):
        self.stack.delete_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="delete_change_set",
            kwargs={
                "ChangeSetName": sentinel.change_set_name,
                "StackName": sentinel.external_name
            }
        )

    def test_describe_change_set_sends_correct_request(self):
        self.stack.describe_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_change_set",
            kwargs={
                "ChangeSetName": sentinel.change_set_name,
                "StackName": sentinel.external_name
            }
        )

    @patch("sceptre.stack.Stack._wait_for_completion")
    def test_execute_change_set_sends_correct_request(
        self, mock_wait_for_completion
    ):
        self.stack._config = {"protect": False}
        self.stack.execute_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="execute_change_set",
            kwargs={
                "ChangeSetName": sentinel.change_set_name,
                "StackName": sentinel.external_name
            }
        )
        mock_wait_for_completion.assert_called_once_with()

    def test_list_change_sets_sends_correct_request(self):
        self.stack.list_change_sets()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="list_change_sets",
            kwargs={"StackName": sentinel.external_name}
        )

    @patch("sceptre.stack.Stack.set_policy")
    @patch("os.path.join")
    def test_lock_calls_set_stack_policy_with_policy(
            self, mock_join, mock_set_policy
    ):
        mock_join.return_value = "tests/fixtures/stack_policies/lock.json"
        self.stack.lock()
        mock_set_policy.assert_called_once_with(
            "tests/fixtures/stack_policies/lock.json"
        )

    @patch("sceptre.stack.Stack.set_policy")
    @patch("os.path.join")
    def test_unlock_calls_set_stack_policy_with_policy(
            self, mock_join, mock_set_policy
    ):
        mock_join.return_value = "tests/fixtures/stack_policies/unlock.json"
        self.stack.unlock()
        mock_set_policy.assert_called_once_with(
            "tests/fixtures/stack_policies/unlock.json"
        )

    def test_format_parameters_with_sting_values(self):
        parameters = {
            "key1": "value1",
            "key2": "value2",
            "key3": "value3"
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(
            formatted_parameters,
            key=lambda x: x["ParameterKey"]
        )
        assert sorted_formatted_parameters == [
            {"ParameterKey": "key1", "ParameterValue": "value1"},
            {"ParameterKey": "key2", "ParameterValue": "value2"},
            {"ParameterKey": "key3", "ParameterValue": "value3"}
        ]

    def test_format_parameters_with_none_values(self):
        parameters = {
            "key1": None,
            "key2": None,
            "key3": None
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(
            formatted_parameters,
            key=lambda x: x["ParameterKey"]
        )
        assert sorted_formatted_parameters == []

    def test_format_parameters_with_none_and_string_values(self):
        parameters = {
            "key1": "value1",
            "key2": None,
            "key3": "value3"
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(
            formatted_parameters,
            key=lambda x: x["ParameterKey"]
        )
        assert sorted_formatted_parameters == [
            {"ParameterKey": "key1", "ParameterValue": "value1"},
            {"ParameterKey": "key3", "ParameterValue": "value3"}
        ]

    def test_format_parameters_with_list_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": ["value4", "value5", "value6"],
            "key3": ["value7", "value8", "value9"]
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(
            formatted_parameters,
            key=lambda x: x["ParameterKey"]
        )
        assert sorted_formatted_parameters == [
            {"ParameterKey": "key1", "ParameterValue": "value1,value2,value3"},
            {"ParameterKey": "key2", "ParameterValue": "value4,value5,value6"},
            {"ParameterKey": "key3", "ParameterValue": "value7,value8,value9"}
        ]

    def test_format_parameters_with_none_and_list_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": None,
            "key3": ["value7", "value8", "value9"]
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(
            formatted_parameters,
            key=lambda x: x["ParameterKey"]
        )
        assert sorted_formatted_parameters == [
            {"ParameterKey": "key1", "ParameterValue": "value1,value2,value3"},
            {"ParameterKey": "key3", "ParameterValue": "value7,value8,value9"}
        ]

    def test_format_parameters_with_list_and_string_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": "value4",
            "key3": ["value5", "value6", "value7"]
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(
            formatted_parameters,
            key=lambda x: x["ParameterKey"]
        )
        assert sorted_formatted_parameters == [
            {"ParameterKey": "key1", "ParameterValue": "value1,value2,value3"},
            {"ParameterKey": "key2", "ParameterValue": "value4"},
            {"ParameterKey": "key3", "ParameterValue": "value5,value6,value7"}
        ]

    def test_format_parameters_with_none_list_and_string_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": "value4",
            "key3": None
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(
            formatted_parameters,
            key=lambda x: x["ParameterKey"]
        )
        assert sorted_formatted_parameters == [
            {"ParameterKey": "key1", "ParameterValue": "value1,value2,value3"},
            {"ParameterKey": "key2", "ParameterValue": "value4"},
        ]

    @patch("sceptre.stack.Stack.describe")
    def test_get_status_with_created_stack(self, mock_describe):
        mock_describe.return_value = {
            "Stacks": [{"StackStatus": "CREATE_COMPLETE"}]
        }
        status = self.stack.get_status()
        assert status == "CREATE_COMPLETE"

    @patch("sceptre.stack.Stack.describe")
    def test_get_status_with_non_existent_stack(self, mock_describe):
        mock_describe.side_effect = ClientError(
            {
                "Error": {
                    "Code": "DoesNotExistException",
                    "Message": "Stack does not exist"
                }
            },
            sentinel.operation
        )
        with pytest.raises(StackDoesNotExistError):
            self.stack.get_status()

    @patch("sceptre.stack.Stack.describe")
    def test_get_status_with_unknown_clinet_error(self, mock_describe):
        mock_describe.side_effect = ClientError(
            {
                "Error": {
                    "Code": "DoesNotExistException",
                    "Message": "Boom!"
                }
            },
            sentinel.operation
        )
        with pytest.raises(ClientError):
            self.stack.get_status()

    def test_get_template_details_with_upload(self):
        self.stack._template = Mock(spec=Template)
        self.stack._template.upload_to_s3.return_value = sentinel.template_url
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }

        template_details = self.stack._get_template_details()

        self.stack._template.upload_to_s3.assert_called_once_with(
            self.stack.region,
            sentinel.template_bucket_name,
            sentinel.template_key_prefix,
            self.stack._environment_path,
            sentinel.external_name,
            self.stack.connection_manager
        )

        assert template_details == {"TemplateURL": sentinel.template_url}

    def test_get_template_details_without_upload(self):
        self.stack._template = Mock(spec=Template)
        self.stack._template.body = sentinel.body
        self.stack.environment_config = {
            "template_key_prefix": sentinel.template_key_prefix
        }

        template_details = self.stack._get_template_details()

        assert template_details == {"TemplateBody": sentinel.body}

    def test_get_role_arn_without_role(self):
        self.stack._template = Mock(spec=Template)
        self.stack._config = {
            "template_path": sentinel.template_path,
        }
        assert self.stack._get_role_arn() == {}

    def test_get_role_arn_with_role(self):
        self.stack._template = Mock(spec=Template)
        self.stack._config = {
            "template_path": sentinel.template_path,
        }
        self.stack.config["role_arn"] = sentinel.role_arn
        assert self.stack._get_role_arn() == {"RoleARN": sentinel.role_arn}

    def test_protect_execution_without_protection(self):
        self.stack._config = {"protect": False}
        # Function should do nothing if protect == False
        self.stack._protect_execution()

    def test_protect_execution_without_explicit_protection(self):
        self.stack._config = {}
        # Function should do nothing if protect isn't explicitly set
        self.stack._protect_execution()

    def test_protect_execution_with_protection(self):
        self.stack._config = {"protect": True}
        with pytest.raises(ProtectedStackError):
            self.stack._protect_execution()

    @patch("sceptre.stack.time")
    @patch("sceptre.stack.Stack._log_new_events")
    @patch("sceptre.stack.Stack.get_status")
    @patch("sceptre.stack.Stack._get_simplified_status")
    def test_wait_for_completion_calls_log_new_events(
            self, mock_get_simplified_status, mock_get_status,
            mock_log_new_events, mock_time
    ):
        mock_get_simplified_status.return_value = StackStatus.COMPLETE

        self.stack._wait_for_completion()
        mock_log_new_events.assert_called_once_with()

    @pytest.mark.parametrize("test_input,expected", [
        ("ROLLBACK_COMPLETE", StackStatus.FAILED),
        ("STACK_COMPLETE", StackStatus.COMPLETE),
        ("STACK_IN_PROGRESS", StackStatus.IN_PROGRESS),
        ("STACK_FAILED", StackStatus.FAILED)
    ])
    def test_get_simplified_status_with_known_stack_statuses(
            self, test_input, expected
    ):
        response = self.stack._get_simplified_status(test_input)
        assert response == expected

    def test_get_simplified_status_with_stack_in_unknown_state(self):
        with pytest.raises(UnknownStackStatusError):
            self.stack._get_simplified_status("UNKOWN_STATUS")

    @patch("sceptre.stack.Stack.describe_events")
    def test_log_new_events_calls_describe_events(self, mock_describe_events):
        mock_describe_events.return_value = {
            "StackEvents": []
        }
        self.stack._log_new_events()
        self.stack.describe_events.assert_called_once_with()

    @patch("sceptre.stack.Stack.describe_events")
    def test_log_new_events_prints_correct_event(self, mock_describe_events):
        mock_describe_events.return_value = {
            "StackEvents": [
                {
                    "Timestamp": datetime.datetime(
                        2016, 3, 15, 14, 2, 0, 0, tzinfo=tzutc()
                    ),
                    "LogicalResourceId": "id-2",
                    "ResourceType": "type-2",
                    "ResourceStatus": "resource-status"
                },
                {
                    "Timestamp": datetime.datetime(
                        2016, 3, 15, 14, 1, 0, 0, tzinfo=tzutc()
                    ),
                    "LogicalResourceId": "id-1",
                    "ResourceType": "type-1",
                    "ResourceStatus": "resource",
                    "ResourceStatusReason": "User Initiated"
                }
            ]
        }
        self.stack.most_recent_event_datetime = (
            datetime.datetime(2016, 3, 15, 14, 0, 0, 0, tzinfo=tzutc())
        )
        self.stack._log_new_events()

    @patch("sceptre.stack.time")
    @patch("sceptre.stack.Stack._get_cs_status")
    def test_wait_for_cs_completion_calls_get_cs_status(
        self, mock_get_cs_status, mock_time
    ):
        mock_get_cs_status.side_effect = [
            StackChangeSetStatus.PENDING, StackChangeSetStatus.READY
        ]

        self.stack.wait_for_cs_completion(sentinel.change_set_name)
        mock_get_cs_status.assert_called_with(sentinel.change_set_name)

    @patch("sceptre.stack.Stack.describe_change_set")
    def test_get_cs_status_handles_all_statuses(
        self, mock_describe_change_set
    ):
        scss = StackChangeSetStatus
        return_values = {                                                                                                     # NOQA
                 "Status":    ('CREATE_PENDING', 'CREATE_IN_PROGRESS', 'CREATE_COMPLETE', 'DELETE_COMPLETE', 'FAILED'),       # NOQA
        "ExecutionStatus": {                                                                                                  # NOQA
        'UNAVAILABLE':         (scss.PENDING,     scss.PENDING,         scss.PENDING,      scss.DEFUNCT,      scss.DEFUNCT),  # NOQA
        'AVAILABLE':           (scss.PENDING,     scss.PENDING,         scss.READY,        scss.DEFUNCT,      scss.DEFUNCT),  # NOQA
        'EXECUTE_IN_PROGRESS': (scss.DEFUNCT,     scss.DEFUNCT,         scss.DEFUNCT,      scss.DEFUNCT,      scss.DEFUNCT),  # NOQA
        'EXECUTE_COMPLETE':    (scss.DEFUNCT,     scss.DEFUNCT,         scss.DEFUNCT,      scss.DEFUNCT,      scss.DEFUNCT),  # NOQA
        'EXECUTE_FAILED':      (scss.DEFUNCT,     scss.DEFUNCT,         scss.DEFUNCT,      scss.DEFUNCT,      scss.DEFUNCT),  # NOQA
        'OBSOLETE':            (scss.DEFUNCT,     scss.DEFUNCT,         scss.DEFUNCT,      scss.DEFUNCT,      scss.DEFUNCT),  # NOQA
        }                                                                                                                     # NOQA
        }                                                                                                                     # NOQA

        for i, status in enumerate(return_values['Status']):
            for exec_status, returns in \
                    return_values['ExecutionStatus'].items():
                mock_describe_change_set.return_value = {
                    "Status": status,
                    "ExecutionStatus": exec_status
                }
                response = self.stack._get_cs_status(sentinel.change_set_name)
                assert response == returns[i]

        for status in return_values['Status']:
            mock_describe_change_set.return_value = {
                "Status": status,
                "ExecutionStatus": 'UNKOWN_STATUS'
            }
            with pytest.raises(UnknownStackChangeSetStatusError):
                self.stack._get_cs_status(sentinel.change_set_name)

        for exec_status in return_values['ExecutionStatus'].keys():
            mock_describe_change_set.return_value = {
                "Status": 'UNKOWN_STATUS',
                "ExecutionStatus": exec_status
            }
            with pytest.raises(UnknownStackChangeSetStatusError):
                self.stack._get_cs_status(sentinel.change_set_name)

        mock_describe_change_set.return_value = {
            "Status": 'UNKOWN_STATUS',
            "ExecutionStatus": 'UNKOWN_STATUS',
        }
        with pytest.raises(UnknownStackChangeSetStatusError):
            self.stack._get_cs_status(sentinel.change_set_name)

    @patch("sceptre.stack.Stack.describe_change_set")
    def test_get_cs_status_raises_unexpected_exceptions(
        self, mock_describe_change_set
    ):
        mock_describe_change_set.side_effect = ClientError(
            {
                "Error": {
                    "Code": "ChangeSetNotFound",
                    "Message": "ChangeSet [*] does not exist"
                }
            },
            sentinel.operation
        )
        with pytest.raises(ClientError):
            self.stack._get_cs_status(sentinel.change_set_name)
Example #2
0
class TestStack(object):
    @patch("sceptre.stack.Stack.config")
    def setup_method(self, test_method, mock_config):
        self.mock_environment_config = MagicMock(spec=Config)
        self.mock_environment_config.environment_path = sentinel.path
        # environment config is an object which inherits from dict. Its
        # attributes are accessable via dot and square bracket notation.
        # In order to mimic the behaviour of the square bracket notation,
        # a side effect is used to return the expected value from the call to
        # __getitem__ that the square bracket notation makes.
        self.mock_environment_config.__getitem__.side_effect = [
            sentinel.project_code, sentinel.region
        ]
        self.mock_connection_manager = Mock()

        self.stack = Stack(name="stack_name",
                           environment_config=self.mock_environment_config,
                           connection_manager=self.mock_connection_manager)

        # Set default value for stack properties
        self.stack._external_name = sentinel.external_name

    def test_initiate_stack(self):
        assert self.stack.name == "stack_name"
        assert self.stack.environment_config == self.mock_environment_config
        assert self.stack.project == sentinel.project_code
        assert self.stack._environment_path == sentinel.path
        assert self.stack._config is None
        assert self.stack._template is None
        assert self.stack.region == sentinel.region
        assert self.stack.connection_manager == self.mock_connection_manager
        assert self.stack._hooks is None
        assert self.stack._dependencies is None

    @patch("sceptre.stack.Stack.config")
    def test_initialiser_calls_correct_methods(self, mock_config):
        mock_config.get.return_value = sentinel.hooks
        self.stack._config = {
            "parameters": sentinel.parameters,
            "hooks": sentinel.hooks
        }
        self.mock_environment_config = MagicMock(spec=Config)
        self.mock_environment_config.environment_path = sentinel.path
        # environment config is an object which inherits from dict. Its
        # attributes are accessable via dot and square bracket notation.
        # In order to mimic the behaviour of the square bracket notation,
        # a side effect is used to return the expected value from the call to
        # __getitem__ that the square bracket notation makes.
        self.mock_environment_config.__getitem__.side_effect = [
            sentinel.project_code, sentinel.template_bucket_name,
            sentinel.region
        ]

        Stack(name=sentinel.name,
              environment_config=self.mock_environment_config,
              connection_manager=sentinel.connection_manager)

    def test_repr(self):
        self.stack.name = "stack_name"
        self.stack.environment_config = {"key": "val"}
        self.stack.connection_manager = "connection_manager"
        assert self.stack.__repr__() == \
            "sceptre.stack.Stack(stack_name='stack_name', \
environment_config={'key': 'val'}, connection_manager=connection_manager)"

    @patch("sceptre.stack.Config")
    def test_config_loads_config(self, mock_Config):
        self.stack._config = None
        self.stack.name = "stack"
        # self.stack.environment_config = MagicMock(spec=Config)
        self.stack.environment_config.sceptre_dir = sentinel.sceptre_dir
        self.stack.environment_config.environment_path = \
            sentinel.environment_path
        self.stack.environment_config.get.return_value = \
            sentinel.user_variables
        mock_config = Mock()
        mock_Config.with_yaml_constructors.return_value = mock_config

        response = self.stack.config
        mock_Config.with_yaml_constructors.assert_called_once_with(
            sceptre_dir=sentinel.sceptre_dir,
            environment_path=sentinel.environment_path,
            base_file_name="stack",
            environment_config=self.stack.environment_config,
            connection_manager=self.stack.connection_manager)
        mock_config.read.assert_called_once_with(sentinel.user_variables,
                                                 self.stack.environment_config)
        assert response == mock_config

    def test_config_returns_config_if_it_exists(self):
        self.stack._config = sentinel.config
        response = self.stack.config
        assert response == sentinel.config

    def test_dependencies_loads_dependencies(self):
        self.stack.name = "dev/security-group"
        self.stack._config = {
            "dependencies": ["dev/vpc", "dev/vpc", "dev/subnets"]
        }
        dependencies = self.stack.dependencies
        assert dependencies == set(["dev/vpc", "dev/subnets"])

    def test_dependencies_returns_dependencies_if_it_exists(self):
        self.stack._dependencies = sentinel.dependencies
        response = self.stack.dependencies
        assert response == sentinel.dependencies

    def test_hooks_with_no_cache(self):
        self.stack._hooks = None
        self.stack._config = {}
        self.stack._config["hooks"] = sentinel.hooks

        assert self.stack.hooks == sentinel.hooks

    def test_hooks_with_cache(self):
        self.stack._hooks = sentinel.hooks
        assert self.stack.hooks == sentinel.hooks

    @patch("sceptre.stack.Template")
    def test_template_loads_template(self, mock_Template):
        self.stack._template = None
        self.stack.environment_config.sceptre_dir = "sceptre_dir"
        self.stack._config = {
            "template_path": "template_path",
            "sceptre_user_data": sentinel.sceptre_user_data
        }
        mock_Template.return_value = sentinel.template

        response = self.stack.template

        mock_Template.assert_called_once_with(
            path="sceptre_dir/template_path",
            sceptre_user_data=sentinel.sceptre_user_data)
        assert response == sentinel.template

    def test_template_returns_template_if_it_exists(self):
        self.stack._template = sentinel.template
        response = self.stack.template
        assert response == sentinel.template

    @patch("sceptre.stack.get_external_stack_name")
    def test_external_name_with_custom_stack_name(
            self, mock_get_external_stack_name):
        self.stack._external_name = None

        self.stack._config = {"stack_name": "custom_stack_name"}
        external_name = self.stack.external_name
        assert external_name == "custom_stack_name"

    def test_external_name_without_custom_name(self):
        self.stack._external_name = None
        self.stack.project = "project"
        self.stack.name = "stack-name"
        self.stack._config = {}

        external_name = self.stack.external_name
        assert external_name == "project-stack-name"

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_create_sends_correct_request(self, mock_get_template_details,
                                          mock_wait_for_completion,
                                          mock_format_params):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {"stack_tags": {"tag1": "val1"}}
        self.stack._hooks = {}
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.create()

        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }]
            })
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_create_sends_correct_request_with_failure(
            self, mock_get_template_details, mock_wait_for_completion,
            mock_format_params):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {"stack_tags": {"tag1": "val1"}}
        self.stack._hooks = {}
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.config["on_failure"] = 'DO_NOTHING'
        self.stack.create()

        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }],
                "OnFailure": 'DO_NOTHING'
            })
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_update_sends_correct_request(self, mock_get_template_details,
                                          mock_wait_for_completion,
                                          mock_format_params):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {"stack_tags": {"tag1": "val1"}}
        self.stack._hooks = {}
        self.stack.config["role_arn"] = sentinel.role_arn

        self.stack.update()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="update_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }]
            })
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.create")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_stack_that_does_not_exist(self, mock_get_status,
                                                   mock_create, mock_hooks):
        self.stack._config = {"protect": False}
        mock_get_status.side_effect = StackDoesNotExistError()
        mock_create.return_value = sentinel.launch_response
        response = self.stack.launch()
        mock_create.assert_called_once_with()
        assert response == sentinel.launch_response

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.create")
    @patch("sceptre.stack.Stack.delete")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_stack_that_failed_to_create(self, mock_get_status,
                                                     mock_delete, mock_create,
                                                     mock_hooks):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_FAILED"
        mock_create.return_value = sentinel.launch_response
        response = self.stack.launch()
        mock_delete.assert_called_once_with()
        mock_create.assert_called_once_with()
        assert response == sentinel.launch_response

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.update")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_complete_stack_with_updates_to_perform(
            self, mock_get_status, mock_update, mock_hooks):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_update.return_value = sentinel.launch_response
        response = self.stack.launch()
        mock_update.assert_called_once_with()
        assert response == sentinel.launch_response

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.update")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_complete_stack_with_no_updates_to_perform(
            self, mock_get_status, mock_update, mock_hooks):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_update.side_effect = ClientError(
            {
                "Error": {
                    "Code": "NoUpdateToPerformError",
                    "Message": "No updates are to be performed."
                }
            }, sentinel.operation)
        response = self.stack.launch()
        mock_update.assert_called_once_with()
        assert response == StackStatus.COMPLETE

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.update")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_complete_stack_with_unknown_client_error(
            self, mock_get_status, mock_update, mock_hooks):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_update.side_effect = ClientError(
            {"Error": {
                "Code": "Boom!",
                "Message": "Boom!"
            }}, sentinel.operation)
        with pytest.raises(ClientError):
            self.stack.launch()

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_in_progress_stack(self, mock_get_status, mock_hooks):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_IN_PROGRESS"
        response = self.stack.launch()
        assert response == StackStatus.IN_PROGRESS

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_failed_stack(self, mock_get_status, mock_hooks):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "UPDATE_FAILED"
        with pytest.raises(CannotUpdateFailedStackError):
            response = self.stack.launch()
            assert response == StackStatus.FAILED

    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_unknown_stack_status(self, mock_get_status,
                                              mock_hooks):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "UNKNOWN_STATUS"
        with pytest.raises(UnknownStackStatusError):
            self.stack.launch()

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_with_created_stack(self, mock_get_status, mock_hooks,
                                       mock_wait_for_completion):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.delete()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="delete_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "RoleARN": sentinel.role_arn
            })

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_when_wait_for_completion_raises_stack_does_not_exist_error(
            self, mock_get_status, mock_hooks, mock_wait_for_completion):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        self.stack.config["role_arn"] = sentinel.role_arn
        mock_wait_for_completion.side_effect = StackDoesNotExistError()
        status = self.stack.delete()
        assert status == StackStatus.COMPLETE

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_when_wait_for_completion_raises_non_existent_client_error(
            self, mock_get_status, mock_hooks, mock_wait_for_completion):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        self.stack.config["role_arn"] = sentinel.role_arn
        mock_wait_for_completion.side_effect = ClientError(
            {
                "Error": {
                    "Code": "DoesNotExistException",
                    "Message": "Stack does not exist"
                }
            }, sentinel.operation)
        status = self.stack.delete()
        assert status == StackStatus.COMPLETE

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_when_wait_for_completion_raises_unexpected_client_error(
            self, mock_get_status, mock_hooks, mock_wait_for_completion):
        self.stack._config = {"protect": False}
        mock_get_status.return_value = "CREATE_COMPLETE"
        self.stack.config["role_arn"] = sentinel.role_arn
        mock_wait_for_completion.side_effect = ClientError(
            {"Error": {
                "Code": "DoesNotExistException",
                "Message": "Boom"
            }}, sentinel.operation)
        with pytest.raises(ClientError):
            self.stack.delete()

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.hooks")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_with_non_existent_stack(self, mock_get_status, mock_hooks,
                                            mock_wait_for_completion):
        self.stack._config = {"protect": False}
        mock_get_status.side_effect = StackDoesNotExistError()
        status = self.stack.delete()
        assert status == StackStatus.COMPLETE

    def test_describe_stack_sends_correct_request(self):
        self.stack.describe()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_stacks",
            kwargs={"StackName": sentinel.external_name})

    def test_describe_events_sends_correct_request(self):
        self.stack.describe_events()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_stack_events",
            kwargs={"StackName": sentinel.external_name})

    def test_describe_resources_sends_correct_request(self):
        self.stack.connection_manager.call.return_value = {
            "StackResources": [{
                "LogicalResourceId": sentinel.logical_resource_id,
                "PhysicalResourceId": sentinel.physical_resource_id,
                "OtherParam": sentinel.other_param
            }]
        }
        response = self.stack.describe_resources()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_stack_resources",
            kwargs={"StackName": sentinel.external_name})
        assert response == [{
            "LogicalResourceId": sentinel.logical_resource_id,
            "PhysicalResourceId": sentinel.physical_resource_id
        }]

    @patch("sceptre.stack.Stack.describe")
    def test_describe_outputs_sends_correct_request(self, mock_describe):
        mock_describe.return_value = {
            "Stacks": [{
                "Outputs": sentinel.outputs
            }]
        }
        response = self.stack.describe_outputs()
        mock_describe.assert_called_once_with()
        assert response == sentinel.outputs

    @patch("sceptre.stack.Stack.describe")
    def test_describe_outputs_handles_stack_with_no_outputs(
            self, mock_describe):
        mock_describe.return_value = {"Stacks": [{}]}
        response = self.stack.describe_outputs()
        assert response == []

    @pytest.mark.parametrize(
        "local_template,remote_template,diff_remote_local,diff_local_remote",
        [
            (
                "local_template_content",
                "remote_template_content",
                "--- remote_template\n+++ local_template\n@@ -1 +1 @@\n-remote_template_content\n+local_template_content\n",  # NOQA
                "--- remote_template\n+++ local_template\n@@ -1 +1 @@\n-local_template_content\n+remote_template_content\n"  # NOQA
            ),
            (
                "template_content\nonlylocal_content",
                "template_content",
                "--- remote_template\n+++ local_template\n@@ -1 +1,2 @@\n template_content\n+onlylocal_content\n",  # NOQA
                "--- remote_template\n+++ local_template\n@@ -1,2 +1 @@\n template_content\n-onlylocal_content\n"  # NOQA
            ),
            (
                "onlylocal_content\ntemplate_content",
                "template_content",
                "--- remote_template\n+++ local_template\n@@ -1 +1,2 @@\n+onlylocal_content\n template_content\n",  # NOQA
                "--- remote_template\n+++ local_template\n@@ -1,2 +1 @@\n-onlylocal_content\n template_content\n"  # NOQA
            ),
            (
                "template_content1\nonlylocal_content\ntemplate_content2",
                "template_content1\ntemplate_content2",
                "--- remote_template\n+++ local_template\n@@ -1,2 +1,3 @@\n template_content1\n+onlylocal_content\n template_content2\n",  # NOQA
                "--- remote_template\n+++ local_template\n@@ -1,3 +1,2 @@\n template_content1\n-onlylocal_content\n template_content2\n"  # NOQA
            ),
            (
                "template_content1\nonlylocal_content\ntemplate_content2",
                "template_content1\nonlyremote_content\ntemplate_content2",
                "--- remote_template\n+++ local_template\n@@ -1,3 +1,3 @@\n template_content1\n-onlyremote_content\n+onlylocal_content\n template_content2\n",  # NOQA
                "--- remote_template\n+++ local_template\n@@ -1,3 +1,3 @@\n template_content1\n-onlylocal_content\n+onlyremote_content\n template_content2\n"  # NOQA
            ),
        ])
    def test_diff_stack_cases(self, local_template, remote_template,
                              diff_remote_local, diff_local_remote):
        self.stack._template = Mock(spec=Template)
        self.stack._template.body = local_template
        self.stack.connection_manager.call.return_value = {
            "TemplateBody": remote_template
        }
        response = self.stack.diff()
        assert response == diff_remote_local

        self.stack._template.body = remote_template
        self.stack.connection_manager.call.return_value = {
            "TemplateBody": local_template
        }
        response = self.stack.diff()
        assert response == diff_local_remote

    def test_continue_update_rollback_sends_correct_request(self):
        self.stack._config = {
            "template_path": sentinel.template_path,
        }
        self.stack.config["role_arn"] = sentinel.role_arn
        self.stack.continue_update_rollback()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="continue_update_rollback",
            kwargs={
                "StackName": sentinel.external_name,
                "RoleARN": sentinel.role_arn
            })

    def test_set_stack_policy_sends_correct_request(self):
        self.stack.set_policy("tests/fixtures/stack_policies/unlock.json")
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="set_stack_policy",
            kwargs={
                "StackName":
                sentinel.external_name,
                "StackPolicyBody":
                """{
  "Statement" : [
    {
      "Effect" : "Allow",
      "Action" : "Update:*",
      "Principal": "*",
      "Resource" : "*"
    }
  ]
}
"""
            })

    def test_get_stack_policy_sends_correct_request(self):
        self.stack.get_policy()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="get_stack_policy",
            kwargs={"StackName": sentinel.external_name})

    @patch("sceptre.stack.Stack._get_template_details")
    def test_validate_template_sends_correct_request(
            self, mock_get_template_details):
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name
        }
        self.stack.validate_template()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="validate_template",
            kwargs={"Template": sentinel.template})

    @patch("sceptre.stack.Stack._format_parameters")
    @patch("sceptre.stack.Stack._get_template_details")
    def test_create_change_set_sends_correct_request(self,
                                                     mock_get_template_details,
                                                     mock_format_params):
        mock_format_params.return_value = sentinel.parameters
        mock_get_template_details.return_value = {
            "Template": sentinel.template
        }
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }
        self.stack._config = {"stack_tags": {"tag1": "val1"}}
        self.stack.config["role_arn"] = sentinel.role_arn

        self.stack.create_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_change_set",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": sentinel.parameters,
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "ChangeSetName": sentinel.change_set_name,
                "RoleARN": sentinel.role_arn,
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }]
            })

    def test_delete_change_set_sends_correct_request(self):
        self.stack.delete_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="delete_change_set",
            kwargs={
                "ChangeSetName": sentinel.change_set_name,
                "StackName": sentinel.external_name
            })

    def test_describe_change_set_sends_correct_request(self):
        self.stack.describe_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_change_set",
            kwargs={
                "ChangeSetName": sentinel.change_set_name,
                "StackName": sentinel.external_name
            })

    @patch("sceptre.stack.Stack._wait_for_completion")
    def test_execute_change_set_sends_correct_request(
            self, mock_wait_for_completion):
        self.stack._config = {"protect": False}
        self.stack.execute_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="execute_change_set",
            kwargs={
                "ChangeSetName": sentinel.change_set_name,
                "StackName": sentinel.external_name
            })
        mock_wait_for_completion.assert_called_once_with()

    def test_list_change_sets_sends_correct_request(self):
        self.stack.list_change_sets()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="list_change_sets",
            kwargs={"StackName": sentinel.external_name})

    def test_diff_stack_sends_correct_request(self):
        self.stack._template = Mock(spec=Template)
        self.stack._template.body = "local_template_content"

        self.stack.connection_manager.call.return_value = \
            {"TemplateBody": "remote_template_content"}

        self.stack.diff()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="get_template",
            kwargs={"StackName": sentinel.external_name})

    @patch("sceptre.stack.Stack.set_policy")
    @patch("os.path.join")
    def test_lock_calls_set_stack_policy_with_policy(self, mock_join,
                                                     mock_set_policy):
        mock_join.return_value = "tests/fixtures/stack_policies/lock.json"
        self.stack.lock()
        mock_set_policy.assert_called_once_with(
            "tests/fixtures/stack_policies/lock.json")

    @patch("sceptre.stack.Stack.set_policy")
    @patch("os.path.join")
    def test_unlock_calls_set_stack_policy_with_policy(self, mock_join,
                                                       mock_set_policy):
        mock_join.return_value = "tests/fixtures/stack_policies/unlock.json"
        self.stack.unlock()
        mock_set_policy.assert_called_once_with(
            "tests/fixtures/stack_policies/unlock.json")

    def test_format_parameters_with_sting_values(self):
        parameters = {"key1": "value1", "key2": "value2", "key3": "value3"}
        formatted_parameters = self.stack._format_parameters(parameters)
        assert sorted(formatted_parameters) == sorted([{
            "ParameterKey":
            "key1",
            "ParameterValue":
            "value1"
        }, {
            "ParameterKey":
            "key2",
            "ParameterValue":
            "value2"
        }, {
            "ParameterKey":
            "key3",
            "ParameterValue":
            "value3"
        }])

    def test_format_parameters_with_none_values(self):
        parameters = {"key1": None, "key2": None, "key3": None}
        formatted_parameters = self.stack._format_parameters(parameters)
        assert sorted(formatted_parameters) == []

    def test_format_parameters_with_none_and_string_values(self):
        parameters = {"key1": "value1", "key2": None, "key3": "value3"}
        formatted_parameters = self.stack._format_parameters(parameters)
        assert sorted(formatted_parameters) == sorted([{
            "ParameterKey":
            "key1",
            "ParameterValue":
            "value1"
        }, {
            "ParameterKey":
            "key3",
            "ParameterValue":
            "value3"
        }])

    def test_format_parameters_with_list_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": ["value4", "value5", "value6"],
            "key3": ["value7", "value8", "value9"]
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        assert sorted(formatted_parameters) == sorted([{
            "ParameterKey":
            "key1",
            "ParameterValue":
            "value1,value2,value3"
        }, {
            "ParameterKey":
            "key2",
            "ParameterValue":
            "value4,value5,value6"
        }, {
            "ParameterKey":
            "key3",
            "ParameterValue":
            "value7,value8,value9"
        }])

    def test_format_parameters_with_none_and_list_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": None,
            "key3": ["value7", "value8", "value9"]
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        assert sorted(formatted_parameters) == sorted([{
            "ParameterKey":
            "key1",
            "ParameterValue":
            "value1,value2,value3"
        }, {
            "ParameterKey":
            "key3",
            "ParameterValue":
            "value7,value8,value9"
        }])

    def test_format_parameters_with_list_and_string_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": "value4",
            "key3": ["value5", "value6", "value7"]
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        assert sorted(formatted_parameters) == sorted([{
            "ParameterKey":
            "key1",
            "ParameterValue":
            "value1,value2,value3"
        }, {
            "ParameterKey":
            "key2",
            "ParameterValue":
            "value4"
        }, {
            "ParameterKey":
            "key3",
            "ParameterValue":
            "value5,value6,value7"
        }])

    def test_format_parameters_with_none_list_and_string_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": "value4",
            "key3": None
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        assert sorted(formatted_parameters) == sorted([
            {
                "ParameterKey": "key1",
                "ParameterValue": "value1,value2,value3"
            },
            {
                "ParameterKey": "key2",
                "ParameterValue": "value4"
            },
        ])

    @patch("sceptre.stack.Stack.describe")
    def test_get_status_with_created_stack(self, mock_describe):
        mock_describe.return_value = {
            "Stacks": [{
                "StackStatus": "CREATE_COMPLETE"
            }]
        }
        status = self.stack.get_status()
        assert status == "CREATE_COMPLETE"

    @patch("sceptre.stack.Stack.describe")
    def test_get_status_with_non_existent_stack(self, mock_describe):
        mock_describe.side_effect = ClientError(
            {
                "Error": {
                    "Code": "DoesNotExistException",
                    "Message": "Stack does not exist"
                }
            }, sentinel.operation)
        with pytest.raises(StackDoesNotExistError):
            self.stack.get_status()

    @patch("sceptre.stack.Stack.describe")
    def test_get_status_with_unknown_clinet_error(self, mock_describe):
        mock_describe.side_effect = ClientError(
            {"Error": {
                "Code": "DoesNotExistException",
                "Message": "Boom!"
            }}, sentinel.operation)
        with pytest.raises(ClientError):
            self.stack.get_status()

    def test_get_template_details_with_upload(self):
        self.stack._template = Mock(spec=Template)
        self.stack._template.upload_to_s3.return_value = sentinel.template_url
        self.stack.environment_config = {
            "template_bucket_name": sentinel.template_bucket_name,
            "template_key_prefix": sentinel.template_key_prefix
        }

        template_details = self.stack._get_template_details()

        self.stack._template.upload_to_s3.assert_called_once_with(
            self.stack.region, sentinel.template_bucket_name,
            sentinel.template_key_prefix, self.stack._environment_path,
            sentinel.external_name, self.stack.connection_manager)

        assert template_details == {"TemplateURL": sentinel.template_url}

    def test_get_template_details_without_upload(self):
        self.stack._template = Mock(spec=Template)
        self.stack._template.body = sentinel.body
        self.stack.environment_config = {
            "template_key_prefix": sentinel.template_key_prefix
        }

        template_details = self.stack._get_template_details()

        assert template_details == {"TemplateBody": sentinel.body}

    def test_get_role_arn_without_role(self):
        self.stack._template = Mock(spec=Template)
        self.stack._config = {
            "template_path": sentinel.template_path,
        }
        assert self.stack._get_role_arn() == {}

    def test_get_role_arn_with_role(self):
        self.stack._template = Mock(spec=Template)
        self.stack._config = {
            "template_path": sentinel.template_path,
        }
        self.stack.config["role_arn"] = sentinel.role_arn
        assert self.stack._get_role_arn() == {"RoleARN": sentinel.role_arn}

    def test_protect_execution_without_protection(self):
        self.stack._config = {"protect": False}
        # Function should do nothing if protect == False
        self.stack._protect_execution()

    def test_protect_execution_without_explicit_protection(self):
        self.stack._config = {}
        # Function should do nothing if protect isn't explicitly set
        self.stack._protect_execution()

    def test_protect_execution_with_protection(self):
        self.stack._config = {"protect": True}
        with pytest.raises(ProtectedStackError):
            self.stack._protect_execution()

    @patch("sceptre.stack.time")
    @patch("sceptre.stack.Stack._log_new_events")
    @patch("sceptre.stack.Stack.get_status")
    @patch("sceptre.stack.Stack._get_simplified_status")
    def test_wait_for_completion_calls_log_new_events(
            self, mock_get_simplified_status, mock_get_status,
            mock_log_new_events, mock_time):
        mock_get_simplified_status.return_value = StackStatus.COMPLETE

        self.stack._wait_for_completion()
        mock_log_new_events.assert_called_once_with()

    @pytest.mark.parametrize("test_input,expected",
                             [("ROLLBACK_COMPLETE", StackStatus.FAILED),
                              ("STACK_COMPLETE", StackStatus.COMPLETE),
                              ("STACK_IN_PROGRESS", StackStatus.IN_PROGRESS),
                              ("STACK_FAILED", StackStatus.FAILED)])
    def test_get_simplified_status_with_known_stack_statuses(
            self, test_input, expected):
        response = self.stack._get_simplified_status(test_input)
        assert response == expected

    def test_get_simplified_status_with_stack_in_unknown_state(self):
        with pytest.raises(UnknownStackStatusError):
            self.stack._get_simplified_status("UNKOWN_STATUS")

    @patch("sceptre.stack.Stack.describe_events")
    def test_log_new_events_calls_describe_events(self, mock_describe_events):
        mock_describe_events.return_value = {"StackEvents": []}
        self.stack._log_new_events()
        self.stack.describe_events.assert_called_once_with()

    @patch("sceptre.stack.Stack.describe_events")
    def test_log_new_events_prints_correct_event(self, mock_describe_events):
        mock_describe_events.return_value = {
            "StackEvents": [{
                "Timestamp":
                datetime.datetime(2016, 3, 15, 14, 2, 0, 0, tzinfo=tzutc()),
                "LogicalResourceId":
                "id-2",
                "ResourceType":
                "type-2",
                "ResourceStatus":
                "resource-status"
            }, {
                "Timestamp":
                datetime.datetime(2016, 3, 15, 14, 1, 0, 0, tzinfo=tzutc()),
                "LogicalResourceId":
                "id-1",
                "ResourceType":
                "type-1",
                "ResourceStatus":
                "resource",
                "ResourceStatusReason":
                "User Initiated"
            }]
        }
        self.stack.most_recent_event_datetime = (datetime.datetime(
            2016, 3, 15, 14, 0, 0, 0, tzinfo=tzutc()))
        self.stack._log_new_events()

    @patch("sceptre.stack.time")
    @patch("sceptre.stack.Stack._get_cs_status")
    def test_wait_for_cs_completion_calls_get_cs_status(
            self, mock_get_cs_status, mock_time):
        mock_get_cs_status.side_effect = [
            StackChangeSetStatus.PENDING, StackChangeSetStatus.READY
        ]

        self.stack.wait_for_cs_completion(sentinel.change_set_name)
        mock_get_cs_status.assert_called_with(sentinel.change_set_name)

    @patch("sceptre.stack.Stack.describe_change_set")
    def test_get_cs_status_handles_all_statuses(self,
                                                mock_describe_change_set):
        scss = StackChangeSetStatus
        return_values = {  # NOQA
            "Status": ('CREATE_PENDING', 'CREATE_IN_PROGRESS',
                       'CREATE_COMPLETE', 'DELETE_COMPLETE', 'FAILED'),  # NOQA
            "ExecutionStatus": {  # NOQA
                'UNAVAILABLE': (scss.PENDING, scss.PENDING, scss.PENDING,
                                scss.DEFUNCT, scss.DEFUNCT),  # NOQA
                'AVAILABLE': (scss.PENDING, scss.PENDING, scss.READY,
                              scss.DEFUNCT, scss.DEFUNCT),  # NOQA
                'EXECUTE_IN_PROGRESS':
                (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT,
                 scss.DEFUNCT),  # NOQA
                'EXECUTE_COMPLETE': (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT,
                                     scss.DEFUNCT, scss.DEFUNCT),  # NOQA
                'EXECUTE_FAILED': (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT,
                                   scss.DEFUNCT, scss.DEFUNCT),  # NOQA
                'OBSOLETE': (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT,
                             scss.DEFUNCT, scss.DEFUNCT),  # NOQA
            }  # NOQA
        }  # NOQA

        for i, status in enumerate(return_values['Status']):
            for exec_status, returns in \
                    return_values['ExecutionStatus'].items():
                mock_describe_change_set.return_value = {
                    "Status": status,
                    "ExecutionStatus": exec_status
                }
                response = self.stack._get_cs_status(sentinel.change_set_name)
                assert response == returns[i]

        for status in return_values['Status']:
            mock_describe_change_set.return_value = {
                "Status": status,
                "ExecutionStatus": 'UNKOWN_STATUS'
            }
            with pytest.raises(UnknownStackChangeSetStatusError):
                self.stack._get_cs_status(sentinel.change_set_name)

        for exec_status in return_values['ExecutionStatus'].keys():
            mock_describe_change_set.return_value = {
                "Status": 'UNKOWN_STATUS',
                "ExecutionStatus": exec_status
            }
            with pytest.raises(UnknownStackChangeSetStatusError):
                self.stack._get_cs_status(sentinel.change_set_name)

        mock_describe_change_set.return_value = {
            "Status": 'UNKOWN_STATUS',
            "ExecutionStatus": 'UNKOWN_STATUS',
        }
        with pytest.raises(UnknownStackChangeSetStatusError):
            self.stack._get_cs_status(sentinel.change_set_name)

    @patch("sceptre.stack.Stack.describe_change_set")
    def test_get_cs_status_raises_unexpected_exceptions(
            self, mock_describe_change_set):
        mock_describe_change_set.side_effect = ClientError(
            {
                "Error": {
                    "Code": "ChangeSetNotFound",
                    "Message": "ChangeSet [*] does not exist"
                }
            }, sentinel.operation)
        with pytest.raises(ClientError):
            self.stack._get_cs_status(sentinel.change_set_name)
Example #3
0
class TestStack(object):
    def setup_method(self, test_method):
        self.patcher_connection_manager = patch(
            "sceptre.stack.ConnectionManager")
        self.mock_ConnectionManager = self.patcher_connection_manager.start()
        self.stack = Stack(name=sentinel.stack_name,
                           project_code=sentinel.project_code,
                           template_path=sentinel.template_path,
                           region=sentinel.region,
                           iam_role=sentinel.iam_role,
                           parameters={"key1": "val1"},
                           sceptre_user_data=sentinel.sceptre_user_data,
                           hooks={},
                           s3_details=None,
                           dependencies=sentinel.dependencies,
                           role_arn=sentinel.role_arn,
                           protected=False,
                           tags={"tag1": "val1"},
                           external_name=sentinel.external_name,
                           notifications=[sentinel.notification],
                           on_failure=sentinel.on_failure,
                           stack_timeout=sentinel.stack_timeout)
        self.stack._template = MagicMock(spec=Template)

    def teardown_method(self, test_method):
        self.patcher_connection_manager.stop()

    def test_initiate_stack(self):
        stack = Stack(name=sentinel.stack_name,
                      project_code=sentinel.project_code,
                      template_path=sentinel.template_path,
                      region=sentinel.region,
                      external_name=sentinel.external_name)
        self.mock_ConnectionManager.assert_called_with(sentinel.region, None)
        assert stack.name == sentinel.stack_name
        assert stack.project_code == sentinel.project_code
        assert stack.external_name == sentinel.external_name
        assert stack.hooks == {}
        assert stack.parameters == {}
        assert stack.sceptre_user_data == {}
        assert stack.template_path == sentinel.template_path
        assert stack.s3_details is None
        assert stack._template is None
        assert stack.protected is False
        assert stack.role_arn is None
        assert stack.dependencies == []
        assert stack.tags == {}
        assert stack.notifications == []
        assert stack.on_failure is None

    def test_repr(self):
        self.stack.connection_manager.region = sentinel.region
        self.stack.connection_manager.iam_role = None

        assert self.stack.__repr__() == \
            "sceptre.stack.Stack(" \
            "name='sentinel.stack_name', " \
            "project_code='sentinel.project_code', " \
            "template_path='sentinel.template_path', " \
            "region='sentinel.region', " \
            "iam_role='None', parameters='{'key1': 'val1'}', " \
            "sceptre_user_data='sentinel.sceptre_user_data', " \
            "hooks='{}', s3_details='None', " \
            "dependencies='sentinel.dependencies', "\
            "role_arn='sentinel.role_arn', " \
            "protected='False', tags='{'tag1': 'val1'}', " \
            "external_name='sentinel.external_name', " \
            "notifications='[sentinel.notification]', " \
            "on_failure='sentinel.on_failure', " \
            "stack_timeout='sentinel.stack_timeout'" \
            ")"

    @patch("sceptre.stack.Template")
    def test_template_loads_template(self, mock_Template):
        self.stack._template = None
        mock_Template.return_value = sentinel.template
        response = self.stack.template

        mock_Template.assert_called_once_with(
            path=sentinel.template_path,
            sceptre_user_data=sentinel.sceptre_user_data,
            connection_manager=self.stack.connection_manager,
            s3_details=None)
        assert response == sentinel.template

    def test_template_returns_template_if_it_exists(self):
        self.stack._template = sentinel.template
        response = self.stack.template
        assert response == sentinel.template

    def test_external_name_with_custom_stack_name(self):
        stack = Stack(name="stack_name",
                      project_code="project_code",
                      template_path="template_path",
                      region="region",
                      external_name="external_name")

        assert stack.external_name == "external_name"

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack._get_stack_timeout")
    def test_create_sends_correct_request(self, mock_get_stack_timeout,
                                          mock_wait_for_completion):
        self.stack._template.get_boto_call_parameter.return_value = {
            "Template": sentinel.template
        }
        mock_get_stack_timeout.return_value = {
            "TimeoutInMinutes": sentinel.timeout
        }

        self.stack.create()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": [{
                    "ParameterKey": "key1",
                    "ParameterValue": "val1"
                }],
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [sentinel.notification],
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }],
                "OnFailure": sentinel.on_failure,
                "TimeoutInMinutes": sentinel.timeout
            })
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack._wait_for_completion")
    def test_create_sends_correct_request_no_notifications(
            self, mock_wait_for_completion):
        self.stack._template = Mock(spec=Template)
        self.stack._template.get_boto_call_parameter.return_value = {
            "Template": sentinel.template
        }
        self.stack.notifications = []

        self.stack.create()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": [{
                    "ParameterKey": "key1",
                    "ParameterValue": "val1"
                }],
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [],
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }],
                "OnFailure": sentinel.on_failure,
                "TimeoutInMinutes": sentinel.stack_timeout
            })
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack._wait_for_completion")
    def test_create_sends_correct_request_with_no_failure_no_timeout(
            self, mock_wait_for_completion):
        self.stack._template.get_boto_call_parameter.return_value = {
            "Template": sentinel.template
        }
        self.stack.on_failure = None
        self.stack.stack_timeout = 0

        self.stack.create()

        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": [{
                    "ParameterKey": "key1",
                    "ParameterValue": "val1"
                }],
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [sentinel.notification],
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }]
            })
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack._wait_for_completion")
    def test_update_sends_correct_request(self, mock_wait_for_completion):
        self.stack._template = Mock(spec=Template)
        self.stack._template.get_boto_call_parameter.return_value = {
            "Template": sentinel.template
        }

        self.stack.update()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="update_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": [{
                    "ParameterKey": "key1",
                    "ParameterValue": "val1"
                }],
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [sentinel.notification],
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }]
            })
        mock_wait_for_completion.assert_called_once_with(
            sentinel.stack_timeout)

    @patch("sceptre.stack.Stack._wait_for_completion")
    def test_update_cancels_after_timeout(self, mock_wait_for_completion):
        self.stack._template = Mock(spec=Template)
        self.stack._template.get_boto_call_parameter.return_value = {
            "Template": sentinel.template
        }
        mock_wait_for_completion.return_value = StackStatus.IN_PROGRESS

        self.stack.update()
        calls = [
            call(service="cloudformation",
                 command="update_stack",
                 kwargs={
                     "StackName":
                     sentinel.external_name,
                     "Template":
                     sentinel.template,
                     "Parameters": [{
                         "ParameterKey": "key1",
                         "ParameterValue": "val1"
                     }],
                     "Capabilities":
                     ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                     "RoleARN":
                     sentinel.role_arn,
                     "NotificationARNs": [sentinel.notification],
                     "Tags": [{
                         "Key": "tag1",
                         "Value": "val1"
                     }]
                 }),
            call(service="cloudformation",
                 command="cancel_update_stack",
                 kwargs={"StackName": sentinel.external_name})
        ]
        self.stack.connection_manager.call.assert_has_calls(calls)
        mock_wait_for_completion.assert_has_calls(
            [call(sentinel.stack_timeout),
             call()])

    @patch("sceptre.stack.Stack._wait_for_completion")
    def test_update_sends_correct_request_no_notification(
            self, mock_wait_for_completion):
        self.stack._template = Mock(spec=Template)
        self.stack._template.get_boto_call_parameter.return_value = {
            "Template": sentinel.template
        }

        self.stack.notifications = []
        self.stack.update()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="update_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": [{
                    "ParameterKey": "key1",
                    "ParameterValue": "val1"
                }],
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [],
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }]
            })
        mock_wait_for_completion.assert_called_once_with(
            sentinel.stack_timeout)

    @patch("sceptre.stack.Stack._wait_for_completion")
    def test_cancel_update_sends_correct_request(self,
                                                 mock_wait_for_completion):
        self.stack.cancel_stack_update()
        self.stack.connection_manager.call.assert_called_once_with(
            service="cloudformation",
            command="cancel_update_stack",
            kwargs={"StackName": sentinel.external_name})
        mock_wait_for_completion.assert_called_once_with()

    @patch("sceptre.stack.Stack.create")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_stack_that_does_not_exist(self, mock_get_status,
                                                   mock_create):
        mock_get_status.side_effect = StackDoesNotExistError()
        mock_create.return_value = sentinel.launch_response
        response = self.stack.launch()
        mock_create.assert_called_once_with()
        assert response == sentinel.launch_response

    @patch("sceptre.stack.Stack.create")
    @patch("sceptre.stack.Stack.delete")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_stack_that_failed_to_create(self, mock_get_status,
                                                     mock_delete, mock_create):
        mock_get_status.return_value = "CREATE_FAILED"
        mock_create.return_value = sentinel.launch_response
        response = self.stack.launch()
        mock_delete.assert_called_once_with()
        mock_create.assert_called_once_with()
        assert response == sentinel.launch_response

    @patch("sceptre.stack.Stack.update")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_complete_stack_with_updates_to_perform(
            self, mock_get_status, mock_update):
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_update.return_value = sentinel.launch_response
        response = self.stack.launch()
        mock_update.assert_called_once_with()
        assert response == sentinel.launch_response

    @patch("sceptre.stack.Stack.update")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_complete_stack_with_no_updates_to_perform(
            self, mock_get_status, mock_update):
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_update.side_effect = ClientError(
            {
                "Error": {
                    "Code": "NoUpdateToPerformError",
                    "Message": "No updates are to be performed."
                }
            }, sentinel.operation)
        response = self.stack.launch()
        mock_update.assert_called_once_with()
        assert response == StackStatus.COMPLETE

    @patch("sceptre.stack.Stack.update")
    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_complete_stack_with_unknown_client_error(
            self, mock_get_status, mock_update):
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_update.side_effect = ClientError(
            {"Error": {
                "Code": "Boom!",
                "Message": "Boom!"
            }}, sentinel.operation)
        with pytest.raises(ClientError):
            self.stack.launch()

    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_in_progress_stack(self, mock_get_status):
        mock_get_status.return_value = "CREATE_IN_PROGRESS"
        response = self.stack.launch()
        assert response == StackStatus.IN_PROGRESS

    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_failed_stack(self, mock_get_status):
        mock_get_status.return_value = "UPDATE_FAILED"
        with pytest.raises(CannotUpdateFailedStackError):
            response = self.stack.launch()
            assert response == StackStatus.FAILED

    @patch("sceptre.stack.Stack.get_status")
    def test_launch_with_unknown_stack_status(self, mock_get_status):
        mock_get_status.return_value = "UNKNOWN_STATUS"
        with pytest.raises(UnknownStackStatusError):
            self.stack.launch()

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_with_created_stack(self, mock_get_status,
                                       mock_wait_for_completion):
        mock_get_status.return_value = "CREATE_COMPLETE"

        self.stack.delete()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="delete_stack",
            kwargs={
                "StackName": sentinel.external_name,
                "RoleARN": sentinel.role_arn
            })

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_when_wait_for_completion_raises_stack_does_not_exist_error(
            self, mock_get_status, mock_wait_for_completion):
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_wait_for_completion.side_effect = StackDoesNotExistError()
        status = self.stack.delete()
        assert status == StackStatus.COMPLETE

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_when_wait_for_completion_raises_non_existent_client_error(
            self, mock_get_status, mock_wait_for_completion):
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_wait_for_completion.side_effect = ClientError(
            {
                "Error": {
                    "Code": "DoesNotExistException",
                    "Message": "Stack does not exist"
                }
            }, sentinel.operation)
        status = self.stack.delete()
        assert status == StackStatus.COMPLETE

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_when_wait_for_completion_raises_unexpected_client_error(
            self, mock_get_status, mock_wait_for_completion):
        mock_get_status.return_value = "CREATE_COMPLETE"
        mock_wait_for_completion.side_effect = ClientError(
            {"Error": {
                "Code": "DoesNotExistException",
                "Message": "Boom"
            }}, sentinel.operation)
        with pytest.raises(ClientError):
            self.stack.delete()

    @patch("sceptre.stack.Stack._wait_for_completion")
    @patch("sceptre.stack.Stack.get_status")
    def test_delete_with_non_existent_stack(self, mock_get_status,
                                            mock_wait_for_completion):
        mock_get_status.side_effect = StackDoesNotExistError()
        status = self.stack.delete()
        assert status == StackStatus.COMPLETE

    def test_describe_stack_sends_correct_request(self):
        self.stack.describe()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_stacks",
            kwargs={"StackName": sentinel.external_name})

    def test_describe_events_sends_correct_request(self):
        self.stack.describe_events()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_stack_events",
            kwargs={"StackName": sentinel.external_name})

    def test_describe_resources_sends_correct_request(self):
        self.stack.connection_manager.call.return_value = {
            "StackResources": [{
                "LogicalResourceId": sentinel.logical_resource_id,
                "PhysicalResourceId": sentinel.physical_resource_id,
                "OtherParam": sentinel.other_param
            }]
        }
        response = self.stack.describe_resources()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_stack_resources",
            kwargs={"StackName": sentinel.external_name})
        assert response == [{
            "LogicalResourceId": sentinel.logical_resource_id,
            "PhysicalResourceId": sentinel.physical_resource_id
        }]

    @patch("sceptre.stack.Stack.describe")
    def test_describe_outputs_sends_correct_request(self, mock_describe):
        mock_describe.return_value = {
            "Stacks": [{
                "Outputs": sentinel.outputs
            }]
        }
        response = self.stack.describe_outputs()
        mock_describe.assert_called_once_with()
        assert response == sentinel.outputs

    @patch("sceptre.stack.Stack.describe")
    def test_describe_outputs_handles_stack_with_no_outputs(
            self, mock_describe):
        mock_describe.return_value = {"Stacks": [{}]}
        response = self.stack.describe_outputs()
        assert response == []

    def test_continue_update_rollback_sends_correct_request(self):
        self.stack.continue_update_rollback()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="continue_update_rollback",
            kwargs={
                "StackName": sentinel.external_name,
                "RoleARN": sentinel.role_arn
            })

    def test_set_stack_policy_sends_correct_request(self):
        self.stack.set_policy("tests/fixtures/stack_policies/unlock.json")
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="set_stack_policy",
            kwargs={
                "StackName":
                sentinel.external_name,
                "StackPolicyBody":
                """{
  "Statement" : [
    {
      "Effect" : "Allow",
      "Action" : "Update:*",
      "Principal": "*",
      "Resource" : "*"
    }
  ]
}
"""
            })

    def test_get_stack_policy_sends_correct_request(self):
        self.stack.get_policy()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="get_stack_policy",
            kwargs={"StackName": sentinel.external_name})

    def test_create_change_set_sends_correct_request(self):
        self.stack._template.get_boto_call_parameter.return_value = {
            "Template": sentinel.template
        }

        self.stack.create_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_change_set",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": [{
                    "ParameterKey": "key1",
                    "ParameterValue": "val1"
                }],
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "ChangeSetName": sentinel.change_set_name,
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [sentinel.notification],
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }]
            })

    def test_create_change_set_sends_correct_request_no_notifications(self):
        self.stack._template = Mock(spec=Template)
        self.stack._template.get_boto_call_parameter.return_value = {
            "Template": sentinel.template
        }
        self.stack.notifications = []

        self.stack.create_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="create_change_set",
            kwargs={
                "StackName": sentinel.external_name,
                "Template": sentinel.template,
                "Parameters": [{
                    "ParameterKey": "key1",
                    "ParameterValue": "val1"
                }],
                "Capabilities": ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'],
                "ChangeSetName": sentinel.change_set_name,
                "RoleARN": sentinel.role_arn,
                "NotificationARNs": [],
                "Tags": [{
                    "Key": "tag1",
                    "Value": "val1"
                }]
            })

    def test_delete_change_set_sends_correct_request(self):
        self.stack.delete_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="delete_change_set",
            kwargs={
                "ChangeSetName": sentinel.change_set_name,
                "StackName": sentinel.external_name
            })

    def test_describe_change_set_sends_correct_request(self):
        self.stack.describe_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="describe_change_set",
            kwargs={
                "ChangeSetName": sentinel.change_set_name,
                "StackName": sentinel.external_name
            })

    @patch("sceptre.stack.Stack._wait_for_completion")
    def test_execute_change_set_sends_correct_request(
            self, mock_wait_for_completion):
        self.stack.execute_change_set(sentinel.change_set_name)
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="execute_change_set",
            kwargs={
                "ChangeSetName": sentinel.change_set_name,
                "StackName": sentinel.external_name
            })
        mock_wait_for_completion.assert_called_once_with()

    def test_list_change_sets_sends_correct_request(self):
        self.stack.list_change_sets()
        self.stack.connection_manager.call.assert_called_with(
            service="cloudformation",
            command="list_change_sets",
            kwargs={"StackName": sentinel.external_name})

    @patch("sceptre.stack.Stack.set_policy")
    @patch("os.path.join")
    def test_lock_calls_set_stack_policy_with_policy(self, mock_join,
                                                     mock_set_policy):
        mock_join.return_value = "tests/fixtures/stack_policies/lock.json"
        self.stack.lock()
        mock_set_policy.assert_called_once_with(
            "tests/fixtures/stack_policies/lock.json")

    @patch("sceptre.stack.Stack.set_policy")
    @patch("os.path.join")
    def test_unlock_calls_set_stack_policy_with_policy(self, mock_join,
                                                       mock_set_policy):
        mock_join.return_value = "tests/fixtures/stack_policies/unlock.json"
        self.stack.unlock()
        mock_set_policy.assert_called_once_with(
            "tests/fixtures/stack_policies/unlock.json")

    def test_format_parameters_with_sting_values(self):
        parameters = {"key1": "value1", "key2": "value2", "key3": "value3"}
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(formatted_parameters,
                                             key=lambda x: x["ParameterKey"])
        assert sorted_formatted_parameters == [{
            "ParameterKey": "key1",
            "ParameterValue": "value1"
        }, {
            "ParameterKey": "key2",
            "ParameterValue": "value2"
        }, {
            "ParameterKey": "key3",
            "ParameterValue": "value3"
        }]

    def test_format_parameters_with_none_values(self):
        parameters = {"key1": None, "key2": None, "key3": None}
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(formatted_parameters,
                                             key=lambda x: x["ParameterKey"])
        assert sorted_formatted_parameters == []

    def test_format_parameters_with_none_and_string_values(self):
        parameters = {"key1": "value1", "key2": None, "key3": "value3"}
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(formatted_parameters,
                                             key=lambda x: x["ParameterKey"])
        assert sorted_formatted_parameters == [{
            "ParameterKey": "key1",
            "ParameterValue": "value1"
        }, {
            "ParameterKey": "key3",
            "ParameterValue": "value3"
        }]

    def test_format_parameters_with_list_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": ["value4", "value5", "value6"],
            "key3": ["value7", "value8", "value9"]
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(formatted_parameters,
                                             key=lambda x: x["ParameterKey"])
        assert sorted_formatted_parameters == [{
            "ParameterKey":
            "key1",
            "ParameterValue":
            "value1,value2,value3"
        }, {
            "ParameterKey":
            "key2",
            "ParameterValue":
            "value4,value5,value6"
        }, {
            "ParameterKey":
            "key3",
            "ParameterValue":
            "value7,value8,value9"
        }]

    def test_format_parameters_with_none_and_list_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": None,
            "key3": ["value7", "value8", "value9"]
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(formatted_parameters,
                                             key=lambda x: x["ParameterKey"])
        assert sorted_formatted_parameters == [{
            "ParameterKey":
            "key1",
            "ParameterValue":
            "value1,value2,value3"
        }, {
            "ParameterKey":
            "key3",
            "ParameterValue":
            "value7,value8,value9"
        }]

    def test_format_parameters_with_list_and_string_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": "value4",
            "key3": ["value5", "value6", "value7"]
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(formatted_parameters,
                                             key=lambda x: x["ParameterKey"])
        assert sorted_formatted_parameters == [{
            "ParameterKey":
            "key1",
            "ParameterValue":
            "value1,value2,value3"
        }, {
            "ParameterKey": "key2",
            "ParameterValue": "value4"
        }, {
            "ParameterKey":
            "key3",
            "ParameterValue":
            "value5,value6,value7"
        }]

    def test_format_parameters_with_none_list_and_string_values(self):
        parameters = {
            "key1": ["value1", "value2", "value3"],
            "key2": "value4",
            "key3": None
        }
        formatted_parameters = self.stack._format_parameters(parameters)
        sorted_formatted_parameters = sorted(formatted_parameters,
                                             key=lambda x: x["ParameterKey"])
        assert sorted_formatted_parameters == [
            {
                "ParameterKey": "key1",
                "ParameterValue": "value1,value2,value3"
            },
            {
                "ParameterKey": "key2",
                "ParameterValue": "value4"
            },
        ]

    @patch("sceptre.stack.Stack.describe")
    def test_get_status_with_created_stack(self, mock_describe):
        mock_describe.return_value = {
            "Stacks": [{
                "StackStatus": "CREATE_COMPLETE"
            }]
        }
        status = self.stack.get_status()
        assert status == "CREATE_COMPLETE"

    @patch("sceptre.stack.Stack.describe")
    def test_get_status_with_non_existent_stack(self, mock_describe):
        mock_describe.side_effect = ClientError(
            {
                "Error": {
                    "Code": "DoesNotExistException",
                    "Message": "Stack does not exist"
                }
            }, sentinel.operation)
        with pytest.raises(StackDoesNotExistError):
            self.stack.get_status()

    @patch("sceptre.stack.Stack.describe")
    def test_get_status_with_unknown_clinet_error(self, mock_describe):
        mock_describe.side_effect = ClientError(
            {"Error": {
                "Code": "DoesNotExistException",
                "Message": "Boom!"
            }}, sentinel.operation)
        with pytest.raises(ClientError):
            self.stack.get_status()

    def test_get_role_arn_without_role(self):
        self.stack.role_arn = None
        assert self.stack._get_role_arn() == {}

    def test_get_role_arn_with_role(self):
        assert self.stack._get_role_arn() == {"RoleARN": sentinel.role_arn}

    def test_protect_execution_without_protection(self):
        # Function should do nothing if protect == False
        self.stack._protect_execution()

    def test_protect_execution_without_explicit_protection(self):
        self.stack._protect_execution()

    def test_protect_execution_with_protection(self):
        self.stack.protected = True
        with pytest.raises(ProtectedStackError):
            self.stack._protect_execution()

    @patch("sceptre.stack.time")
    @patch("sceptre.stack.Stack._log_new_events")
    @patch("sceptre.stack.Stack.get_status")
    @patch("sceptre.stack.Stack._get_simplified_status")
    def test_wait_for_completion_calls_log_new_events(
            self, mock_get_simplified_status, mock_get_status,
            mock_log_new_events, mock_time):
        mock_get_simplified_status.return_value = StackStatus.COMPLETE

        self.stack._wait_for_completion()
        mock_log_new_events.assert_called_once_with()

    @pytest.mark.parametrize("test_input,expected",
                             [("ROLLBACK_COMPLETE", StackStatus.FAILED),
                              ("STACK_COMPLETE", StackStatus.COMPLETE),
                              ("STACK_IN_PROGRESS", StackStatus.IN_PROGRESS),
                              ("STACK_FAILED", StackStatus.FAILED)])
    def test_get_simplified_status_with_known_stack_statuses(
            self, test_input, expected):
        response = self.stack._get_simplified_status(test_input)
        assert response == expected

    def test_get_simplified_status_with_stack_in_unknown_state(self):
        with pytest.raises(UnknownStackStatusError):
            self.stack._get_simplified_status("UNKOWN_STATUS")

    @patch("sceptre.stack.Stack.describe_events")
    def test_log_new_events_calls_describe_events(self, mock_describe_events):
        mock_describe_events.return_value = {"StackEvents": []}
        self.stack._log_new_events()
        self.stack.describe_events.assert_called_once_with()

    @patch("sceptre.stack.Stack.describe_events")
    def test_log_new_events_prints_correct_event(self, mock_describe_events):
        self.stack.name = "stack-name"
        mock_describe_events.return_value = {
            "StackEvents": [{
                "Timestamp":
                datetime.datetime(2016, 3, 15, 14, 2, 0, 0, tzinfo=tzutc()),
                "LogicalResourceId":
                "id-2",
                "ResourceType":
                "type-2",
                "ResourceStatus":
                "resource-status"
            }, {
                "Timestamp":
                datetime.datetime(2016, 3, 15, 14, 1, 0, 0, tzinfo=tzutc()),
                "LogicalResourceId":
                "id-1",
                "ResourceType":
                "type-1",
                "ResourceStatus":
                "resource",
                "ResourceStatusReason":
                "User Initiated"
            }]
        }
        self.stack.most_recent_event_datetime = (datetime.datetime(
            2016, 3, 15, 14, 0, 0, 0, tzinfo=tzutc()))
        self.stack._log_new_events()

    @patch("sceptre.stack.time")
    @patch("sceptre.stack.Stack._get_cs_status")
    def test_wait_for_cs_completion_calls_get_cs_status(
            self, mock_get_cs_status, mock_time):
        mock_get_cs_status.side_effect = [
            StackChangeSetStatus.PENDING, StackChangeSetStatus.READY
        ]

        self.stack.wait_for_cs_completion(sentinel.change_set_name)
        mock_get_cs_status.assert_called_with(sentinel.change_set_name)

    @patch("sceptre.stack.Stack.describe_change_set")
    def test_get_cs_status_handles_all_statuses(self,
                                                mock_describe_change_set):
        scss = StackChangeSetStatus
        return_values = {  # NOQA
            "Status": ('CREATE_PENDING', 'CREATE_IN_PROGRESS',
                       'CREATE_COMPLETE', 'DELETE_COMPLETE', 'FAILED'),  # NOQA
            "ExecutionStatus": {  # NOQA
                'UNAVAILABLE': (scss.PENDING, scss.PENDING, scss.PENDING,
                                scss.DEFUNCT, scss.DEFUNCT),  # NOQA
                'AVAILABLE': (scss.PENDING, scss.PENDING, scss.READY,
                              scss.DEFUNCT, scss.DEFUNCT),  # NOQA
                'EXECUTE_IN_PROGRESS':
                (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT,
                 scss.DEFUNCT),  # NOQA
                'EXECUTE_COMPLETE': (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT,
                                     scss.DEFUNCT, scss.DEFUNCT),  # NOQA
                'EXECUTE_FAILED': (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT,
                                   scss.DEFUNCT, scss.DEFUNCT),  # NOQA
                'OBSOLETE': (scss.DEFUNCT, scss.DEFUNCT, scss.DEFUNCT,
                             scss.DEFUNCT, scss.DEFUNCT),  # NOQA
            }  # NOQA
        }  # NOQA

        for i, status in enumerate(return_values['Status']):
            for exec_status, returns in \
                    return_values['ExecutionStatus'].items():
                mock_describe_change_set.return_value = {
                    "Status": status,
                    "ExecutionStatus": exec_status
                }
                response = self.stack._get_cs_status(sentinel.change_set_name)
                assert response == returns[i]

        for status in return_values['Status']:
            mock_describe_change_set.return_value = {
                "Status": status,
                "ExecutionStatus": 'UNKOWN_STATUS'
            }
            with pytest.raises(UnknownStackChangeSetStatusError):
                self.stack._get_cs_status(sentinel.change_set_name)

        for exec_status in return_values['ExecutionStatus'].keys():
            mock_describe_change_set.return_value = {
                "Status": 'UNKOWN_STATUS',
                "ExecutionStatus": exec_status
            }
            with pytest.raises(UnknownStackChangeSetStatusError):
                self.stack._get_cs_status(sentinel.change_set_name)

        mock_describe_change_set.return_value = {
            "Status": 'UNKOWN_STATUS',
            "ExecutionStatus": 'UNKOWN_STATUS',
        }
        with pytest.raises(UnknownStackChangeSetStatusError):
            self.stack._get_cs_status(sentinel.change_set_name)

    @patch("sceptre.stack.Stack.describe_change_set")
    def test_get_cs_status_raises_unexpected_exceptions(
            self, mock_describe_change_set):
        mock_describe_change_set.side_effect = ClientError(
            {
                "Error": {
                    "Code": "ChangeSetNotFound",
                    "Message": "ChangeSet [*] does not exist"
                }
            }, sentinel.operation)
        with pytest.raises(ClientError):
            self.stack._get_cs_status(sentinel.change_set_name)