class TestProviderDefaultMode(unittest.TestCase): """Tests for runway.cfngin.providers.aws.default default mode.""" def setUp(self): """Run before tests.""" region = "us-east-1" self.session = get_session(region=region) self.provider = Provider(self.session, region=region, recreate_failed=False) self.stubber = Stubber(self.provider.cloudformation) def test_destroy_stack(self): """Test destroy stack.""" stack = {'StackName': 'MockStack'} self.stubber.add_response('delete_stack', {}, stack) with self.stubber: self.assertIsNone(self.provider.destroy_stack(stack)) self.stubber.assert_no_pending_responses() def test_get_stack_stack_does_not_exist(self): """Test get stack stack does not exist.""" stack_name = "MockStack" self.stubber.add_client_error( "describe_stacks", service_error_code="ValidationError", service_message="Stack with id %s does not exist" % stack_name, expected_params={"StackName": stack_name}) with self.assertRaises(exceptions.StackDoesNotExist): with self.stubber: self.provider.get_stack(stack_name) def test_get_stack_stack_exists(self): """Test get stack stack exists.""" stack_name = "MockStack" stack_response = { "Stacks": [generate_describe_stacks_stack(stack_name)] } self.stubber.add_response("describe_stacks", stack_response, expected_params={"StackName": stack_name}) with self.stubber: response = self.provider.get_stack(stack_name) self.assertEqual(response["StackName"], stack_name) def test_select_destroy_method(self): """Test select destroy method.""" for i in [[{ 'force_interactive': False }, self.provider.noninteractive_destroy_stack], [{ 'force_interactive': True }, self.provider.interactive_destroy_stack]]: self.assertEqual(self.provider.select_destroy_method(**i[0]), i[1]) def test_select_update_method(self): """Test select update method.""" for i in [[{ 'force_interactive': True, 'force_change_set': False }, self.provider.interactive_update_stack], [{ 'force_interactive': False, 'force_change_set': False }, self.provider.default_update_stack], [{ 'force_interactive': False, 'force_change_set': True }, self.provider.noninteractive_changeset_update], [{ 'force_interactive': True, 'force_change_set': True }, self.provider.interactive_update_stack]]: self.assertEqual(self.provider.select_update_method(**i[0]), i[1]) def test_prepare_stack_for_update_completed(self): """Test prepare stack for update completed.""" stack_name = "MockStack" stack = generate_describe_stacks_stack(stack_name, stack_status="UPDATE_COMPLETE") with self.stubber: self.assertTrue(self.provider.prepare_stack_for_update(stack, [])) def test_prepare_stack_for_update_in_progress(self): """Test prepare stack for update in progress.""" stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="UPDATE_IN_PROGRESS") with self.assertRaises(exceptions.StackUpdateBadStatus) as raised: with self.stubber: self.provider.prepare_stack_for_update(stack, []) self.assertIn('in-progress', str(raised.exception)) def test_prepare_stack_for_update_non_recreatable(self): """Test prepare stack for update non recreatable.""" stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="REVIEW_IN_PROGRESS") with self.assertRaises(exceptions.StackUpdateBadStatus) as raised: with self.stubber: self.provider.prepare_stack_for_update(stack, []) self.assertIn('Unsupported state', str(raised.exception)) def test_prepare_stack_for_update_disallowed(self): """Test prepare stack for update disallowed.""" stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="ROLLBACK_COMPLETE") with self.assertRaises(exceptions.StackUpdateBadStatus) as raised: with self.stubber: self.provider.prepare_stack_for_update(stack, []) self.assertIn('re-creation is disabled', str(raised.exception)) # Ensure we point out to the user how to enable re-creation self.assertIn('--recreate-failed', str(raised.exception)) def test_prepare_stack_for_update_bad_tags(self): """Test prepare stack for update bad tags.""" stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="ROLLBACK_COMPLETE") self.provider.recreate_failed = True with self.assertRaises(exceptions.StackUpdateBadStatus) as raised: with self.stubber: self.provider.prepare_stack_for_update(stack, tags=[{ 'Key': 'cfngin_namespace', 'Value': 'test' }]) self.assertIn('tags differ', str(raised.exception).lower()) def test_prepare_stack_for_update_recreate(self): """Test prepare stack for update recreate.""" stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="ROLLBACK_COMPLETE") self.stubber.add_response("delete_stack", {}, expected_params={"StackName": stack_name}) self.provider.recreate_failed = True with self.stubber: self.assertFalse(self.provider.prepare_stack_for_update(stack, [])) def test_noninteractive_changeset_update_no_stack_policy(self): """Test noninteractive changeset update no stack policy.""" stack_name = "MockStack" self.stubber.add_response("create_change_set", { 'Id': 'CHANGESETID', 'StackId': 'STACKID' }) changes = [] changes.append(generate_change()) self.stubber.add_response( "describe_change_set", generate_change_set_response( status="CREATE_COMPLETE", execution_status="AVAILABLE", changes=changes, )) self.stubber.add_response("execute_change_set", {}) with self.stubber: self.provider.noninteractive_changeset_update( fqn=stack_name, template=Template(url="http://fake.template.url.com/"), old_parameters=[], parameters=[], stack_policy=None, tags=[], ) def test_noninteractive_changeset_update_with_stack_policy(self): """Test noninteractive changeset update with stack policy.""" stack_name = "MockStack" self.stubber.add_response("create_change_set", { 'Id': 'CHANGESETID', 'StackId': 'STACKID' }) changes = [] changes.append(generate_change()) self.stubber.add_response( "describe_change_set", generate_change_set_response( status="CREATE_COMPLETE", execution_status="AVAILABLE", changes=changes, )) self.stubber.add_response("set_stack_policy", {}) self.stubber.add_response("execute_change_set", {}) with self.stubber: self.provider.noninteractive_changeset_update( fqn=stack_name, template=Template(url="http://fake.template.url.com/"), old_parameters=[], parameters=[], stack_policy=Template(body="{}"), tags=[], ) @patch('runway.cfngin.providers.aws.default.output_full_changeset') def test_get_stack_changes_update(self, mock_output_full_cs): """Test get stack changes update.""" stack_name = "MockStack" mock_stack = generate_stack_object(stack_name) self.stubber.add_response( 'describe_stacks', {'Stacks': [generate_describe_stacks_stack(stack_name)]}) self.stubber.add_response('get_template', generate_get_template('cfn_template.yaml')) self.stubber.add_response("create_change_set", { 'Id': 'CHANGESETID', 'StackId': stack_name }) changes = [] changes.append(generate_change()) self.stubber.add_response( "describe_change_set", generate_change_set_response( status="CREATE_COMPLETE", execution_status="AVAILABLE", changes=changes, )) self.stubber.add_response("delete_change_set", {}) self.stubber.add_response( 'describe_stacks', {'Stacks': [generate_describe_stacks_stack(stack_name)]}) with self.stubber: result = self.provider.get_stack_changes( stack=mock_stack, template=Template(url="http://fake.template.url.com/"), parameters=[], tags=[]) mock_output_full_cs.assert_called_with(full_changeset=changes, params_diff=[], fqn=stack_name, answer='y') expected_outputs = { 'FakeOutput': '<inferred-change: MockStack.FakeOutput={}>'.format( str({"Ref": "FakeResource"})) } self.assertEqual(self.provider.get_outputs(stack_name), expected_outputs) self.assertEqual(result, expected_outputs) @patch('runway.cfngin.providers.aws.default.output_full_changeset') def test_get_stack_changes_create(self, mock_output_full_cs): """Test get stack changes create.""" stack_name = "MockStack" mock_stack = generate_stack_object(stack_name) self.stubber.add_response( 'describe_stacks', { 'Stacks': [ generate_describe_stacks_stack( stack_name, stack_status='REVIEW_IN_PROGRESS') ] }) self.stubber.add_response("create_change_set", { 'Id': 'CHANGESETID', 'StackId': stack_name }) changes = [] changes.append(generate_change()) self.stubber.add_response( "describe_change_set", generate_change_set_response( status="CREATE_COMPLETE", execution_status="AVAILABLE", changes=changes, )) self.stubber.add_response("delete_change_set", {}) self.stubber.add_response( 'describe_stacks', { 'Stacks': [ generate_describe_stacks_stack( stack_name, stack_status='REVIEW_IN_PROGRESS') ] }) self.stubber.add_response( 'describe_stacks', { 'Stacks': [ generate_describe_stacks_stack( stack_name, stack_status='REVIEW_IN_PROGRESS') ] }) self.stubber.add_response("delete_stack", {}) with self.stubber: self.provider.get_stack_changes( stack=mock_stack, template=Template(url="http://fake.template.url.com/"), parameters=[], tags=[]) mock_output_full_cs.assert_called_with(full_changeset=changes, params_diff=[], fqn=stack_name, answer='y') def test_tail_stack_retry_on_missing_stack(self): """Test tail stack retry on missing stack.""" stack_name = "SlowToCreateStack" stack = MagicMock(spec=Stack) stack.fqn = "my-namespace-{}".format(stack_name) default.TAIL_RETRY_SLEEP = .01 # Ensure the stack never appears before we run out of retries for i in range(MAX_TAIL_RETRIES + 5): self.stubber.add_client_error( "describe_stack_events", service_error_code="ValidationError", service_message="Stack [{}] does not exist".format(stack_name), http_status_code=400, response_meta={"attempt": i + 1}, ) with self.stubber: try: self.provider.tail_stack(stack, threading.Event()) except ClientError as exc: self.assertEqual(exc.response["ResponseMetadata"]["attempt"], MAX_TAIL_RETRIES) def test_tail_stack_retry_on_missing_stack_eventual_success(self): """Test tail stack retry on missing stack eventual success.""" stack_name = "SlowToCreateStack" stack = MagicMock(spec=Stack) stack.fqn = "my-namespace-{}".format(stack_name) default.TAIL_RETRY_SLEEP = .01 default.GET_EVENTS_SLEEP = .01 received_events = [] def mock_log_func(event): received_events.append(event) def valid_event_response(stack, event_id): return { "StackEvents": [ { "StackId": stack.fqn + "12345", "EventId": event_id, "StackName": stack.fqn, "Timestamp": datetime.now() }, ] } # Ensure the stack never appears before we run out of retries for i in range(3): self.stubber.add_client_error( "describe_stack_events", service_error_code="ValidationError", service_message="Stack [{}] does not exist".format(stack_name), http_status_code=400, response_meta={"attempt": i + 1}, ) self.stubber.add_response("describe_stack_events", valid_event_response(stack, "InitialEvents")) self.stubber.add_response("describe_stack_events", valid_event_response(stack, "Event1")) with self.stubber: try: self.provider.tail_stack(stack, threading.Event(), log_func=mock_log_func) except UnStubbedResponseError: # Eventually we run out of responses - could not happen in # regular execution # normally this would just be dealt with when the threads were # shutdown, but doing so here is a little difficult because # we can't control the `tail_stack` loop pass self.assertEqual(received_events[0]["EventId"], "Event1")
class TestProviderDefaultMode(unittest.TestCase): """Tests for runway.cfngin.providers.aws.default default mode.""" def setUp(self) -> None: """Run before tests.""" region = "us-east-1" self.session = get_session(region=region) self.provider = Provider(self.session, region=region, recreate_failed=False) self.stubber = Stubber(self.provider.cloudformation) def test_create_stack_no_changeset(self) -> None: """Test create_stack, no changeset, template url.""" stack_name = "fake_stack" template = Template(url="http://fake.template.url.com/") parameters: List[Any] = [] tags: List[Any] = [] expected_args = generate_cloudformation_args( stack_name, parameters, tags, template ) expected_args["EnableTerminationProtection"] = False expected_args["TimeoutInMinutes"] = 60 self.stubber.add_response( "create_stack", {"StackId": stack_name}, expected_args ) with self.stubber: self.provider.create_stack( stack_name, template, parameters, tags, timeout=60 ) self.stubber.assert_no_pending_responses() @patch("runway.cfngin.providers.aws.default.Provider.update_termination_protection") @patch("runway.cfngin.providers.aws.default.create_change_set") def test_create_stack_with_changeset( self, patched_create_change_set: MagicMock, patched_update_term: MagicMock ) -> None: """Test create_stack, force changeset, termination protection.""" stack_name = "fake_stack" template_path = Path("./tests/unit/cfngin/fixtures/cfn_template.yaml") template = Template(body=template_path.read_text()) parameters: List[Any] = [] tags: List[Any] = [] changeset_id = "CHANGESETID" patched_create_change_set.return_value = ([], changeset_id) self.stubber.add_response( "execute_change_set", {}, {"ChangeSetName": changeset_id} ) with self.stubber: self.provider.create_stack( stack_name, template, parameters, tags, force_change_set=True, termination_protection=True, ) self.stubber.assert_no_pending_responses() patched_create_change_set.assert_called_once_with( self.provider.cloudformation, stack_name, template, parameters, tags, "CREATE", service_role=self.provider.service_role, ) patched_update_term.assert_called_once_with(stack_name, True) def test_destroy_stack(self) -> None: """Test destroy stack.""" stack = {"StackName": "MockStack"} self.stubber.add_response("delete_stack", {}, stack) with self.stubber: self.assertIsNone(self.provider.destroy_stack(stack)) # type: ignore self.stubber.assert_no_pending_responses() def test_get_stack_stack_does_not_exist(self) -> None: """Test get stack stack does not exist.""" stack_name = "MockStack" self.stubber.add_client_error( "describe_stacks", service_error_code="ValidationError", service_message="Stack with id %s does not exist" % stack_name, expected_params={"StackName": stack_name}, ) with self.assertRaises(exceptions.StackDoesNotExist): with self.stubber: self.provider.get_stack(stack_name) def test_get_stack_stack_exists(self) -> None: """Test get stack stack exists.""" stack_name = "MockStack" stack_response = {"Stacks": [generate_describe_stacks_stack(stack_name)]} self.stubber.add_response( "describe_stacks", stack_response, expected_params={"StackName": stack_name} ) with self.stubber: response = self.provider.get_stack(stack_name) self.assertEqual(response["StackName"], stack_name) def test_select_destroy_method(self) -> None: """Test select destroy method.""" for i in [ [{"force_interactive": False}, self.provider.noninteractive_destroy_stack], [{"force_interactive": True}, self.provider.interactive_destroy_stack], ]: self.assertEqual( self.provider.select_destroy_method(**i[0]), i[1] # type: ignore ) def test_select_update_method(self) -> None: """Test select update method.""" for i in [ [ {"force_interactive": True, "force_change_set": False}, self.provider.interactive_update_stack, ], [ {"force_interactive": False, "force_change_set": False}, self.provider.default_update_stack, ], [ {"force_interactive": False, "force_change_set": True}, self.provider.noninteractive_changeset_update, ], [ {"force_interactive": True, "force_change_set": True}, self.provider.interactive_update_stack, ], ]: self.assertEqual( self.provider.select_update_method(**i[0]), i[1] # type: ignore ) def test_prepare_stack_for_update_completed(self) -> None: """Test prepare stack for update completed.""" with self.stubber: stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="UPDATE_COMPLETE" ) self.assertTrue(self.provider.prepare_stack_for_update(stack, [])) def test_prepare_stack_for_update_in_progress(self) -> None: """Test prepare stack for update in progress.""" stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="UPDATE_IN_PROGRESS" ) with self.assertRaises(exceptions.StackUpdateBadStatus) as raised: with self.stubber: self.provider.prepare_stack_for_update(stack, []) self.assertIn("in-progress", str(raised.exception)) def test_prepare_stack_for_update_non_recreatable(self) -> None: """Test prepare stack for update non recreatable.""" stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="REVIEW_IN_PROGRESS" ) with self.assertRaises(exceptions.StackUpdateBadStatus) as raised: with self.stubber: self.provider.prepare_stack_for_update(stack, []) self.assertIn("Unsupported state", str(raised.exception)) def test_prepare_stack_for_update_disallowed(self) -> None: """Test prepare stack for update disallowed.""" stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="ROLLBACK_COMPLETE" ) with self.assertRaises(exceptions.StackUpdateBadStatus) as raised: with self.stubber: self.provider.prepare_stack_for_update(stack, []) self.assertIn("re-creation is disabled", str(raised.exception)) # Ensure we point out to the user how to enable re-creation self.assertIn("--recreate-failed", str(raised.exception)) def test_prepare_stack_for_update_bad_tags(self) -> None: """Test prepare stack for update bad tags.""" stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="ROLLBACK_COMPLETE" ) self.provider.recreate_failed = True with self.assertRaises(exceptions.StackUpdateBadStatus) as raised: with self.stubber: self.provider.prepare_stack_for_update( stack, tags=[{"Key": "cfngin_namespace", "Value": "test"}] ) self.assertIn("tags differ", str(raised.exception).lower()) def test_prepare_stack_for_update_recreate(self) -> None: """Test prepare stack for update recreate.""" stack_name = "MockStack" stack = generate_describe_stacks_stack( stack_name, stack_status="ROLLBACK_COMPLETE" ) self.stubber.add_response( "delete_stack", {}, expected_params={"StackName": stack_name} ) self.provider.recreate_failed = True with self.stubber: self.assertFalse(self.provider.prepare_stack_for_update(stack, [])) def test_noninteractive_changeset_update_no_stack_policy(self) -> None: """Test noninteractive changeset update no stack policy.""" self.stubber.add_response( "create_change_set", {"Id": "CHANGESETID", "StackId": "STACKID"} ) changes = [generate_change()] self.stubber.add_response( "describe_change_set", generate_change_set_response( status="CREATE_COMPLETE", execution_status="AVAILABLE", changes=changes ), ) self.stubber.add_response("execute_change_set", {}) with self.stubber: stack_name = "MockStack" self.provider.noninteractive_changeset_update( fqn=stack_name, template=Template(url="http://fake.template.url.com/"), old_parameters=[], parameters=[], stack_policy=None, tags=[], ) def test_noninteractive_changeset_update_with_stack_policy(self) -> None: """Test noninteractive changeset update with stack policy.""" self.stubber.add_response( "create_change_set", {"Id": "CHANGESETID", "StackId": "STACKID"} ) changes = [generate_change()] self.stubber.add_response( "describe_change_set", generate_change_set_response( status="CREATE_COMPLETE", execution_status="AVAILABLE", changes=changes ), ) self.stubber.add_response("set_stack_policy", {}) self.stubber.add_response("execute_change_set", {}) with self.stubber: stack_name = "MockStack" self.provider.noninteractive_changeset_update( fqn=stack_name, template=Template(url="http://fake.template.url.com/"), old_parameters=[], parameters=[], stack_policy=Template(body="{}"), tags=[], ) def test_noninteractive_destroy_stack_termination_protected(self) -> None: """Test noninteractive_destroy_stack with termination protection.""" self.stubber.add_client_error("delete_stack") with self.stubber, self.assertRaises(ClientError): self.provider.noninteractive_destroy_stack("fake-stack") self.stubber.assert_no_pending_responses() @patch("runway.cfngin.providers.aws.default.output_full_changeset") def test_get_stack_changes_update(self, mock_output_full_cs: MagicMock) -> None: """Test get stack changes update.""" stack_name = "MockStack" mock_stack = generate_stack_object(stack_name) self.stubber.add_response( "describe_stacks", {"Stacks": [generate_describe_stacks_stack(stack_name)]} ) self.stubber.add_response( "get_template", generate_get_template("cfn_template.yaml") ) self.stubber.add_response( "create_change_set", {"Id": "CHANGESETID", "StackId": stack_name} ) changes = [generate_change()] self.stubber.add_response( "describe_change_set", generate_change_set_response( status="CREATE_COMPLETE", execution_status="AVAILABLE", changes=changes ), ) self.stubber.add_response("delete_change_set", {}) self.stubber.add_response( "describe_stacks", {"Stacks": [generate_describe_stacks_stack(stack_name)]} ) with self.stubber: result = self.provider.get_stack_changes( stack=mock_stack, template=Template(url="http://fake.template.url.com/"), parameters=[], tags=[], ) mock_output_full_cs.assert_called_with( full_changeset=changes, params_diff=[], fqn=stack_name, answer="y" ) expected_outputs = { "FakeOutput": "<inferred-change: MockStack.FakeOutput={}>".format( str({"Ref": "FakeResource"}) ) } self.assertEqual(self.provider.get_outputs(stack_name), expected_outputs) self.assertEqual(result, expected_outputs) @patch("runway.cfngin.providers.aws.default.output_full_changeset") def test_get_stack_changes_create(self, mock_output_full_cs: MagicMock) -> None: """Test get stack changes create.""" stack_name = "MockStack" mock_stack = generate_stack_object(stack_name) self.stubber.add_response( "describe_stacks", { "Stacks": [ generate_describe_stacks_stack( stack_name, stack_status="REVIEW_IN_PROGRESS" ) ] }, ) self.stubber.add_response( "create_change_set", {"Id": "CHANGESETID", "StackId": stack_name} ) changes = [generate_change()] self.stubber.add_response( "describe_change_set", generate_change_set_response( status="CREATE_COMPLETE", execution_status="AVAILABLE", changes=changes ), ) self.stubber.add_response("delete_change_set", {}) self.stubber.add_response( "describe_stacks", { "Stacks": [ generate_describe_stacks_stack( stack_name, stack_status="REVIEW_IN_PROGRESS" ) ] }, ) self.stubber.add_response( "describe_stacks", { "Stacks": [ generate_describe_stacks_stack( stack_name, stack_status="REVIEW_IN_PROGRESS" ) ] }, ) self.stubber.add_response("delete_stack", {}) with self.stubber: self.provider.get_stack_changes( stack=mock_stack, template=Template(url="http://fake.template.url.com/"), parameters=[], tags=[], ) mock_output_full_cs.assert_called_with( full_changeset=changes, params_diff=[], fqn=stack_name, answer="y" ) def test_tail_stack_retry_on_missing_stack(self) -> None: """Test tail stack retry on missing stack.""" stack_name = "SlowToCreateStack" stack = MagicMock(spec=Stack) stack.fqn = "my-namespace-{}".format(stack_name) default.TAIL_RETRY_SLEEP = 0.01 # Ensure the stack never appears before we run out of retries for i in range(MAX_TAIL_RETRIES + 5): self.stubber.add_client_error( "describe_stack_events", service_error_code="ValidationError", service_message="Stack [{}] does not exist".format(stack_name), http_status_code=400, response_meta={"attempt": i + 1}, ) with self.stubber: try: self.provider.tail_stack(stack, threading.Event()) except ClientError as exc: self.assertEqual( exc.response["ResponseMetadata"]["attempt"], MAX_TAIL_RETRIES ) def test_tail_stack_retry_on_missing_stack_eventual_success(self) -> None: """Test tail stack retry on missing stack eventual success.""" stack_name = "SlowToCreateStack" stack = MagicMock(spec=Stack) stack.fqn = "my-namespace-{}".format(stack_name) default.TAIL_RETRY_SLEEP = 0.01 default.GET_EVENTS_SLEEP = 0.01 received_events: List[Any] = [] def mock_log_func(event: Any) -> None: received_events.append(event) def valid_event_response(stack: Stack, event_id: str) -> Dict[str, Any]: return { "StackEvents": [ { "StackId": stack.fqn + "12345", "EventId": event_id, "StackName": stack.fqn, "Timestamp": datetime.now(), }, ] } # Ensure the stack never appears before we run out of retries for i in range(3): self.stubber.add_client_error( "describe_stack_events", service_error_code="ValidationError", service_message="Stack [{}] does not exist".format(stack_name), http_status_code=400, response_meta={"attempt": i + 1}, ) self.stubber.add_response( "describe_stack_events", valid_event_response(stack, "InitialEvents") ) self.stubber.add_response( "describe_stack_events", valid_event_response(stack, "Event1") ) with self.stubber: try: self.provider.tail_stack( stack, threading.Event(), log_func=mock_log_func ) except UnStubbedResponseError: # Eventually we run out of responses - could not happen in # regular execution # normally this would just be dealt with when the threads were # shutdown, but doing so here is a little difficult because # we can't control the `tail_stack` loop pass self.assertEqual(received_events[0]["EventId"], "Event1") def test_update_termination_protection(self) -> None: """Test update_termination_protection.""" stack_name = "fake-stack" test_cases = [ MutableMap(aws=False, defined=True, expected=True), MutableMap(aws=True, defined=False, expected=False), MutableMap(aws=True, defined=True, expected=None), MutableMap(aws=False, defined=False, expected=None), ] for test in test_cases: self.stubber.add_response( "describe_stacks", { "Stacks": [ generate_describe_stacks_stack( stack_name, termination_protection=test["aws"] ) ] }, {"StackName": stack_name}, ) if isinstance(test["expected"], bool): self.stubber.add_response( "update_termination_protection", {"StackId": stack_name}, { "EnableTerminationProtection": test["expected"], "StackName": stack_name, }, ) with self.stubber: self.provider.update_termination_protection(stack_name, test["defined"]) self.stubber.assert_no_pending_responses()