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)
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)
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)