Repository: getmoto/moto
Pull request #6866 (2023-09-29 10:45:19)

cognito-idp: Use default values for unspecified settings in update_user_pool
Hi!

In this PR, I've fixed the behavior of `update_user_pool`.

I discovered that `update_user_pool` overwrites default values even when the request doesn't contain those parameters.

However, the [AWS docs](https://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_UpdateUserPool.html) state:

>If you don't provide a value for an attribute, Amazon Cognito sets it to its default value.

Therefore, I've added a method to extend the default config, ensuring that UserPool uses it during both creation and configuration updates.

Best regards.


Patch:
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index 1e9c82ec8..f6ddc62e0 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -399,22 +399,7 @@ class CognitoIdpUserPool(BaseModel):
         self.name = name
         self.status = None
 
-        self.extended_config = DEFAULT_USER_POOL_CONFIG.copy()
-        self.extended_config.update(extended_config or {})
-
-        message_template = self.extended_config.get("VerificationMessageTemplate")
-        if message_template and "SmsVerificationMessage" not in extended_config:
-            self.extended_config["SmsVerificationMessage"] = message_template.get(
-                "SmsMessage"
-            )
-        if message_template and "EmailVerificationSubject" not in extended_config:
-            self.extended_config["EmailVerificationSubject"] = message_template.get(
-                "EmailSubject"
-            )
-        if message_template and "EmailVerificationMessage" not in extended_config:
-            self.extended_config["EmailVerificationMessage"] = message_template.get(
-                "EmailMessage"
-            )
+        self.update_extended_config(extended_config)
 
         self.creation_date = utcnow()
         self.last_modified_date = utcnow()
@@ -472,6 +457,24 @@ class CognitoIdpUserPool(BaseModel):
             None,
         )
 
+    def update_extended_config(self, extended_config: Dict[str, Any]) -> None:
+        self.extended_config = DEFAULT_USER_POOL_CONFIG.copy()
+        self.extended_config.update(extended_config or {})
+
+        message_template = self.extended_config.get("VerificationMessageTemplate")
+        if message_template and "SmsVerificationMessage" not in extended_config:
+            self.extended_config["SmsVerificationMessage"] = message_template.get(
+                "SmsMessage"
+            )
+        if message_template and "EmailVerificationSubject" not in extended_config:
+            self.extended_config["EmailVerificationSubject"] = message_template.get(
+                "EmailSubject"
+            )
+        if message_template and "EmailVerificationMessage" not in extended_config:
+            self.extended_config["EmailVerificationMessage"] = message_template.get(
+                "EmailMessage"
+            )
+
     def _base_json(self) -> Dict[str, Any]:
         return {
             "Id": self.id,
@@ -978,7 +981,7 @@ class CognitoIdpBackend(BaseBackend):
         self, user_pool_id: str, extended_config: Dict[str, Any]
     ) -> None:
         user_pool = self.describe_user_pool(user_pool_id)
-        user_pool.extended_config = extended_config
+        user_pool.update_extended_config(extended_config)
 
     def delete_user_pool(self, user_pool_id: str) -> None:
         self.describe_user_pool(user_pool_id)
diff --git a/moto/ec2/models/fleets.py b/moto/ec2/models/fleets.py
index 9563efe4e..a76a26ea5 100644
--- a/moto/ec2/models/fleets.py
+++ b/moto/ec2/models/fleets.py
@@ -308,12 +308,19 @@ class FleetsBackend:
     def delete_fleets(
         self, fleet_ids: List[str], terminate_instances: bool
     ) -> List[Fleet]:
+
         fleets = []
         for fleet_id in fleet_ids:
             fleet = self.fleets[fleet_id]
             if terminate_instances:
+                # State indicates the fleet is in the process of being terminated
+                # AWS will change the state to `deleted` after a few seconds/minutes
+                # Note that it will stay in the `deleted`-state for at least a few hours
+                fleet.state = "deleted_terminating"
                 fleet.target_capacity = 0
                 fleet.terminate_instances()
+            else:
+                # State is different, and indicates that instances are still running
+                fleet.state = "deleted_running"
             fleets.append(fleet)
-            fleet.state = "deleted"
         return fleets
diff --git a/moto/ec2/responses/fleets.py b/moto/ec2/responses/fleets.py
index 9bf247672..e63a74322 100644
--- a/moto/ec2/responses/fleets.py
+++ b/moto/ec2/responses/fleets.py
@@ -4,7 +4,7 @@ from ._base_response import EC2BaseResponse
 class Fleets(EC2BaseResponse):
     def delete_fleets(self) -> str:
         fleet_ids = self._get_multi_param("FleetId.")
-        terminate_instances = self._get_param("TerminateInstances")
+        terminate_instances = self._get_bool_param("TerminateInstances")
         fleets = self.ec2_backend.delete_fleets(fleet_ids, terminate_instances)
         template = self.response_template(DELETE_FLEETS_TEMPLATE)
         return template.render(fleets=fleets)
diff --git a/moto/glue/models.py b/moto/glue/models.py
index 838b70d6d..695ea1e32 100644
--- a/moto/glue/models.py
+++ b/moto/glue/models.py
@@ -131,7 +131,9 @@ class GlueBackend(BaseBackend):
         if database_name in self.databases:
             raise DatabaseAlreadyExistsException()
 
-        database = FakeDatabase(database_name, database_input)
+        database = FakeDatabase(
+            database_name, database_input, catalog_id=self.account_id
+        )
         self.databases[database_name] = database
         return database
 
@@ -165,7 +167,9 @@ class GlueBackend(BaseBackend):
         if table_name in database.tables:
             raise TableAlreadyExistsException()
 
-        table = FakeTable(database_name, table_name, table_input)
+        table = FakeTable(
+            database_name, table_name, table_input, catalog_id=self.account_id
+        )
         database.tables[table_name] = table
         return table
 
@@ -1041,9 +1045,12 @@ class GlueBackend(BaseBackend):
 
 
 class FakeDatabase(BaseModel):
-    def __init__(self, database_name: str, database_input: Dict[str, Any]):
+    def __init__(
+        self, database_name: str, database_input: Dict[str, Any], catalog_id: str
+    ):
         self.name = database_name
         self.input = database_input
+        self.catalog_id = catalog_id
         self.created_time = utcnow()
         self.tables: Dict[str, FakeTable] = OrderedDict()
 
@@ -1058,16 +1065,21 @@ class FakeDatabase(BaseModel):
                 "CreateTableDefaultPermissions"
             ),
             "TargetDatabase": self.input.get("TargetDatabase"),
-            "CatalogId": self.input.get("CatalogId"),
+            "CatalogId": self.input.get("CatalogId") or self.catalog_id,
         }
 
 
 class FakeTable(BaseModel):
     def __init__(
-        self, database_name: str, table_name: str, table_input: Dict[str, Any]
+        self,
+        database_name: str,
+        table_name: str,
+        table_input: Dict[str, Any],
+        catalog_id: str,
     ):
         self.database_name = database_name
         self.name = table_name
+        self.catalog_id = catalog_id
         self.partitions: Dict[str, FakePartition] = OrderedDict()
         self.created_time = utcnow()
         self.updated_time: Optional[datetime] = None
@@ -1104,6 +1116,7 @@ class FakeTable(BaseModel):
             **self.get_version(str(version)),
             # Add VersionId after we get the version-details, just to make sure that it's a valid version (int)
             "VersionId": str(version),
+            "CatalogId": self.catalog_id,
         }
         if self.updated_time is not None:
             obj["UpdateTime"] = unix_time(self.updated_time)
diff --git a/moto/redshift/models.py b/moto/redshift/models.py
index 1f868e69f..66031dc4a 100644
--- a/moto/redshift/models.py
+++ b/moto/redshift/models.py
@@ -157,6 +157,7 @@ class Cluster(TaggableResourceMixin, CloudFormationModel):
         self.restored_from_snapshot = restored_from_snapshot
         self.kms_key_id = kms_key_id
         self.cluster_snapshot_copy_status: Optional[Dict[str, Any]] = None
+        self.total_storage_capacity = 0
 
     @staticmethod
     def cloudformation_name_type() -> str:
@@ -313,6 +314,7 @@ class Cluster(TaggableResourceMixin, CloudFormationModel):
                 for iam_role_arn in self.iam_roles_arn
             ],
             "KmsKeyId": self.kms_key_id,
+            "TotalStorageCapacityInMegaBytes": self.total_storage_capacity,
         }
         if self.restored_from_snapshot:
             json_response["RestoreStatus"] = {
diff --git a/moto/ses/models.py b/moto/ses/models.py
index 1303a7a6c..0427b4105 100644
--- a/moto/ses/models.py
+++ b/moto/ses/models.py
@@ -518,6 +518,9 @@ class SESBackend(BaseBackend):
         )
         return rendered_template
 
+    def delete_template(self, name: str) -> None:
+        self.templates.pop(name)
+
     def create_receipt_rule_set(self, rule_set_name: str) -> None:
         if self.receipt_rule_set.get(rule_set_name) is not None:
             raise RuleSetNameAlreadyExists("Duplicate Receipt Rule Set Name.")
diff --git a/moto/ses/responses.py b/moto/ses/responses.py
index 186feddb8..ceef3ae19 100644
--- a/moto/ses/responses.py
+++ b/moto/ses/responses.py
@@ -281,6 +281,11 @@ class EmailResponse(BaseResponse):
         template = self.response_template(RENDER_TEMPLATE)
         return template.render(template=rendered_template)
 
+    def delete_template(self) -> str:
+        name = self._get_param("TemplateName")
+        self.backend.delete_template(name)
+        return self.response_template(DELETE_TEMPLATE).render()
+
     def create_receipt_rule_set(self) -> str:
         rule_set_name = self._get_param("RuleSetName")
         self.backend.create_receipt_rule_set(rule_set_name)
@@ -627,6 +632,14 @@ RENDER_TEMPLATE = """
 </TestRenderTemplateResponse>
 """
 
+DELETE_TEMPLATE = """<DeleteTemplateResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
+    <DeleteTemplateResult>
+    </DeleteTemplateResult>
+    <ResponseMetadata>
+        <RequestId>47e0ef1a-9bf2-11e1-9279-0100e8cf12ba</RequestId>
+    </ResponseMetadata>
+</DeleteTemplateResponse>"""
+
 CREATE_RECEIPT_RULE_SET = """<CreateReceiptRuleSetResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
   <CreateReceiptRuleSetResult/>
   <ResponseMetadata>
diff --git a/moto/ses/template.py b/moto/ses/template.py
index a153cf0db..987dc9fb9 100644
--- a/moto/ses/template.py
+++ b/moto/ses/template.py
@@ -1,53 +1,180 @@
-from typing import Any, Dict, Optional
+from typing import Any, Dict, Optional, Type
 
 from .exceptions import MissingRenderingAttributeException
 from moto.utilities.tokenizer import GenericTokenizer
 
 
+class BlockProcessor:
+    def __init__(
+        self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer
+    ):
+        self.template = template
+        self.template_data = template_data
+        self.tokenizer = tokenizer
+
+    def parse(self) -> str:
+        # Added to make MyPy happy
+        # Not all implementations have this method
+        # It's up to the caller to know whether to call this method
+        raise NotImplementedError
+
+
+class EachBlockProcessor(BlockProcessor):
+    def __init__(
+        self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer
+    ):
+        self.template = template
+        self.tokenizer = tokenizer
+
+        self.tokenizer.skip_characters("#each")
+        self.tokenizer.skip_white_space()
+        var_name = self.tokenizer.read_until("}}").strip()
+        self.tokenizer.skip_characters("}}")
+        self.template_data = template_data.get(var_name, [])
+
+    def parse(self) -> str:
+        parsed = ""
+        current_pos = self.tokenizer.token_pos
+
+        for template_data in self.template_data:
+            self.tokenizer.token_pos = current_pos
+            for char in self.tokenizer:
+                if char == "{" and self.tokenizer.peek() == "{":
+                    self.tokenizer.skip_characters("{")
+                    self.tokenizer.skip_white_space()
+
+                    _processor = get_processor(self.tokenizer)(
+                        self.template, template_data, self.tokenizer  # type: ignore
+                    )
+                    # If we've reached the end, we should stop processing
+                    # Our parent will continue with whatever comes after {{/each}}
+                    if type(_processor) == EachEndBlockProcessor:
+                        break
+                    # If we've encountered another processor, they can continue
+                    parsed += _processor.parse()
+
+                    continue
+
+                parsed += char
+
+        return parsed
+
+
+class EachEndBlockProcessor(BlockProcessor):
+    def __init__(
+        self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer
+    ):
+        super().__init__(template, template_data, tokenizer)
+
+        self.tokenizer.skip_characters("/each")
+        self.tokenizer.skip_white_space()
+        self.tokenizer.skip_characters("}}")
+
+
+class IfBlockProcessor(BlockProcessor):
+    def __init__(
+        self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer
+    ):
+        super().__init__(template, template_data, tokenizer)
+
+        self.tokenizer.skip_characters("#if")
+        self.tokenizer.skip_white_space()
+        condition = self.tokenizer.read_until("}}").strip()
+        self.tokenizer.skip_characters("}}")
+        self.parse_contents = template_data.get(condition)
+
+    def parse(self) -> str:
+        parsed = ""
+
+        for char in self.tokenizer:
+            if char == "{" and self.tokenizer.peek() == "{":
+                self.tokenizer.skip_characters("{")
+                self.tokenizer.skip_white_space()
+
+                _processor = get_processor(self.tokenizer)(
+                    self.template, self.template_data, self.tokenizer
+                )
+                if type(_processor) == IfEndBlockProcessor:
+                    break
+                elif type(_processor) == ElseBlockProcessor:
+                    self.parse_contents = not self.parse_contents
+                    continue
+                if self.parse_contents:
+                    parsed += _processor.parse()
+
+                continue
+
+            if self.parse_contents:
+                parsed += char
+
+        return parsed
+
+
+class IfEndBlockProcessor(BlockProcessor):
+    def __init__(
+        self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer
+    ):
+        super().__init__(template, template_data, tokenizer)
+
+        self.tokenizer.skip_characters("/if")
+        self.tokenizer.skip_white_space()
+        self.tokenizer.skip_characters("}}")
+
+
+class ElseBlockProcessor(BlockProcessor):
+    def __init__(
+        self, template: str, template_data: Dict[str, Any], tokenizer: GenericTokenizer
+    ):
+        super().__init__(template, template_data, tokenizer)
+
+        self.tokenizer.skip_characters("else")
+        self.tokenizer.skip_white_space()
+        self.tokenizer.skip_characters("}}")
+
+
+class VarBlockProcessor(BlockProcessor):
+    def parse(self) -> str:
+        var_name = self.tokenizer.read_until("}}").strip()
+        if self.template_data.get(var_name) is None:
+            raise MissingRenderingAttributeException(var_name)
+        data: str = self.template_data.get(var_name)  # type: ignore
+        self.tokenizer.skip_white_space()
+        self.tokenizer.skip_characters("}}")
+        return data
+
+
+def get_processor(tokenizer: GenericTokenizer) -> Type[BlockProcessor]:
+    if tokenizer.peek(5) == "#each":
+        return EachBlockProcessor
+    if tokenizer.peek(5) == "/each":
+        return EachEndBlockProcessor
+    if tokenizer.peek(3) == "#if":
+        return IfBlockProcessor
+    if tokenizer.peek(3) == "/if":
+        return IfEndBlockProcessor
+    if tokenizer.peek(4) == "else":
+        return ElseBlockProcessor
+    return VarBlockProcessor
+
+
 def parse_template(
     template: str,
     template_data: Dict[str, Any],
     tokenizer: Optional[GenericTokenizer] = None,
-    until: Optional[str] = None,
 ) -> str:
     tokenizer = tokenizer or GenericTokenizer(template)
-    state = ""
+
     parsed = ""
-    var_name = ""
     for char in tokenizer:
-        if until is not None and (char + tokenizer.peek(len(until) - 1)) == until:
-            return parsed
         if char == "{" and tokenizer.peek() == "{":
             # Two braces next to each other indicate a variable/language construct such as for-each
+            # We have different processors handling different constructs
             tokenizer.skip_characters("{")
             tokenizer.skip_white_space()
-            if tokenizer.peek() == "#":
-                state = "LOOP"
-                tokenizer.skip_characters("#each")
-                tokenizer.skip_white_space()
-            else:
-                state = "VAR"
-            continue
-        if char == "}":
-            if state in ["LOOP", "VAR"]:
-                tokenizer.skip_characters("}")
-                if state == "VAR":
-                    if template_data.get(var_name) is None:
-                        raise MissingRenderingAttributeException(var_name)
-                    parsed += template_data.get(var_name)  # type: ignore
-                else:
-                    current_position = tokenizer.token_pos
-                    for item in template_data.get(var_name, []):
-                        tokenizer.token_pos = current_position
-                        parsed += parse_template(
-                            template, item, tokenizer, until="{{/each}}"
-                        )
-                        tokenizer.skip_characters("{/each}}")
-                var_name = ""
-                state = ""
-                continue
-        if state in ["LOOP", "VAR"]:
-            var_name += char
+
+            _processor = get_processor(tokenizer)(template, template_data, tokenizer)
+            parsed += _processor.parse()
             continue
+
         parsed += char
     return parsed
diff --git a/moto/utilities/tokenizer.py b/moto/utilities/tokenizer.py
index 7872f8a7c..38433d84b 100644
--- a/moto/utilities/tokenizer.py
+++ b/moto/utilities/tokenizer.py
@@ -38,6 +38,14 @@ class GenericTokenizer:
                 return ""
             raise StopIteration
 
+    def read_until(self, phrase: str) -> str:
+        chars_read = ""
+        for char in self:
+            chars_read += char
+            if self.peek(len(phrase)) == phrase:
+                return chars_read
+        return chars_read
+
     def skip_characters(self, phrase: str, case_sensitive: bool = False) -> None:
         """
         Skip the characters in the supplied phrase.


Test patch:
diff --git a/.github/workflows/tests_real_aws.yml b/.github/workflows/tests_real_aws.yml
index b3fa3303a..342769732 100644
--- a/.github/workflows/tests_real_aws.yml
+++ b/.github/workflows/tests_real_aws.yml
@@ -42,4 +42,4 @@ jobs:
       env:
         MOTO_TEST_ALLOW_AWS_REQUEST: ${{ true }}
       run: |
-        pytest -sv tests/test_s3 -m aws_verified
+        pytest -sv tests/test_ec2/ tests/test_ses/ tests/test_s3 -m aws_verified
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
index b79464285..152570f20 100644
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -885,6 +885,12 @@ def test_update_user_pool():
         UserPoolId=user_pool_details["UserPool"]["Id"]
     )
     assert updated_user_pool_details["UserPool"]["Policies"] == new_policies
+    assert updated_user_pool_details["UserPool"]["AdminCreateUserConfig"] is not None
+    assert updated_user_pool_details["UserPool"]["EmailConfiguration"] is not None
+    assert (
+        updated_user_pool_details["UserPool"]["VerificationMessageTemplate"] is not None
+    )
+    assert updated_user_pool_details["UserPool"]["AccountRecoverySetting"] is not None
 
 
 @mock_cognitoidp
diff --git a/tests/test_ec2/__init__.py b/tests/test_ec2/__init__.py
index e69de29bb..3e0ec5ea0 100644
--- a/tests/test_ec2/__init__.py
+++ b/tests/test_ec2/__init__.py
@@ -0,0 +1,33 @@
+import os
+from functools import wraps
+from moto import mock_ec2, mock_ssm
+
+
+def ec2_aws_verified(func):
+    """
+    Function that is verified to work against AWS.
+    Can be run against AWS at any time by setting:
+      MOTO_TEST_ALLOW_AWS_REQUEST=true
+
+    If this environment variable is not set, the function runs in a `mock_s3` context.
+
+    This decorator will:
+      - Create a bucket
+      - Run the test and pass the bucket_name as an argument
+      - Delete the objects and the bucket itself
+    """
+
+    @wraps(func)
+    def pagination_wrapper():
+        allow_aws = (
+            os.environ.get("MOTO_TEST_ALLOW_AWS_REQUEST", "false").lower() == "true"
+        )
+
+        if allow_aws:
+            resp = func()
+        else:
+            with mock_ec2(), mock_ssm():
+                resp = func()
+        return resp
+
+    return pagination_wrapper
diff --git a/tests/test_ec2/test_fleets.py b/tests/test_ec2/test_fleets.py
index 7ad3ee551..2dfda6991 100644
--- a/tests/test_ec2/test_fleets.py
+++ b/tests/test_ec2/test_fleets.py
@@ -5,6 +5,8 @@ from moto import mock_ec2
 from tests import EXAMPLE_AMI_ID
 from uuid import uuid4
 
+from . import ec2_aws_verified
+
 
 def get_subnet_id(conn):
     vpc = conn.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]
@@ -15,11 +17,11 @@ def get_subnet_id(conn):
     return subnet_id
 
 
-def get_launch_template(conn, instance_type="t2.micro"):
+def get_launch_template(conn, instance_type="t2.micro", ami_id=EXAMPLE_AMI_ID):
     launch_template = conn.create_launch_template(
         LaunchTemplateName="test" + str(uuid4()),
         LaunchTemplateData={
-            "ImageId": EXAMPLE_AMI_ID,
+            "ImageId": ami_id,
             "InstanceType": instance_type,
             "KeyName": "test",
             "SecurityGroups": ["sg-123456"],
@@ -40,6 +42,29 @@ def get_launch_template(conn, instance_type="t2.micro"):
     return launch_template_id, launch_template_name
 
 
+class launch_template_context:
+    def __init__(self, region: str = "us-west-2"):
+        self.ec2 = boto3.client("ec2", region_name=region)
+        self.ssm = boto3.client("ssm", region_name=region)
+
+    def __enter__(self):
+        kernel_61 = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64"
+        ami_id = self.ssm.get_parameter(Name=kernel_61)["Parameter"]["Value"]
+        self.lt_id, self.lt_name = get_launch_template(self.ec2, ami_id=ami_id)
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.ec2.delete_launch_template(LaunchTemplateId=self.lt_id)
+
+
+@ec2_aws_verified
+def test_launch_template_is_created_properly():
+    with launch_template_context() as ctxt:
+        template = ctxt.ec2.describe_launch_templates()["LaunchTemplates"][0]
+        assert template["DefaultVersionNumber"] == 1
+        assert template["LatestVersionNumber"] == 1
+
+
 @mock_ec2
 def test_create_spot_fleet_with_lowest_price():
     conn = boto3.client("ec2", region_name="us-west-2")
@@ -354,61 +379,66 @@ def test_create_fleet_using_launch_template_config__overrides():
     assert instance["SubnetId"] == subnet_id
 
 
-@mock_ec2
+@ec2_aws_verified
 def test_delete_fleet():
-    conn = boto3.client("ec2", region_name="us-west-2")
-
-    launch_template_id, _ = get_launch_template(conn)
+    with launch_template_context() as ctxt:
 
-    fleet_res = conn.create_fleet(
-        LaunchTemplateConfigs=[
-            {
-                "LaunchTemplateSpecification": {
-                    "LaunchTemplateId": launch_template_id,
-                    "Version": "1",
+        fleet_res = ctxt.ec2.create_fleet(
+            LaunchTemplateConfigs=[
+                {
+                    "LaunchTemplateSpecification": {
+                        "LaunchTemplateId": ctxt.lt_id,
+                        "Version": "1",
+                    },
                 },
+            ],
+            TargetCapacitySpecification={
+                "DefaultTargetCapacityType": "spot",
+                "OnDemandTargetCapacity": 1,
+                "SpotTargetCapacity": 2,
+                "TotalTargetCapacity": 3,
             },
-        ],
-        TargetCapacitySpecification={
-            "DefaultTargetCapacityType": "spot",
-            "OnDemandTargetCapacity": 1,
-            "SpotTargetCapacity": 2,
-            "TotalTargetCapacity": 3,
-        },
-        SpotOptions={
-            "AllocationStrategy": "lowestPrice",
-            "InstanceInterruptionBehavior": "terminate",
-        },
-        Type="maintain",
-        ValidFrom="2020-01-01T00:00:00Z",
-        ValidUntil="2020-12-31T00:00:00Z",
-    )
-    fleet_id = fleet_res["FleetId"]
+            SpotOptions={
+                "AllocationStrategy": "lowestPrice",
+                "InstanceInterruptionBehavior": "terminate",
+            },
+            Type="maintain",
+        )
+        fleet_id = fleet_res["FleetId"]
 
-    delete_fleet_out = conn.delete_fleets(FleetIds=[fleet_id], TerminateInstances=True)
+        successfull_deletions = ctxt.ec2.delete_fleets(
+            FleetIds=[fleet_id], TerminateInstances=False
+        )["SuccessfulFleetDeletions"]
 
-    assert len(delete_fleet_out["SuccessfulFleetDeletions"]) == 1
-    assert delete_fleet_out["SuccessfulFleetDeletions"][0]["FleetId"] == fleet_id
-    assert (
-        delete_fleet_out["SuccessfulFleetDeletions"][0]["CurrentFleetState"]
-        == "deleted"
-    )
+        assert len(successfull_deletions) == 1
+        assert successfull_deletions[0]["FleetId"] == fleet_id
+        assert successfull_deletions[0]["CurrentFleetState"] == "deleted_running"
 
-    fleets = conn.describe_fleets(FleetIds=[fleet_id])["Fleets"]
-    assert len(fleets) == 1
+        successfull_deletions = ctxt.ec2.delete_fleets(
+            FleetIds=[fleet_id], TerminateInstances=True
+        )["SuccessfulFleetDeletions"]
 
-    target_capacity_specification = fleets[0]["TargetCapacitySpecification"]
-    assert target_capacity_specification["TotalTargetCapacity"] == 0
-    assert fleets[0]["FleetState"] == "deleted"
+        assert len(successfull_deletions) == 1
+        assert successfull_deletions[0]["FleetId"] == fleet_id
+        assert successfull_deletions[0]["CurrentFleetState"] == "deleted_terminating"
 
-    # Instances should be terminated
-    instance_res = conn.describe_fleet_instances(FleetId=fleet_id)
-    instances = instance_res["ActiveInstances"]
-    assert len(instances) == 0
+        fleets = ctxt.ec2.describe_fleets(FleetIds=[fleet_id])["Fleets"]
+        assert len(fleets) == 1
+
+        # AWS doesn't reset this, but Moto does
+        # we should figure out why Moto sets this value to 0
+        # target_capacity_specification = fleets[0]["TargetCapacitySpecification"]
+        # assert target_capacity_specification["TotalTargetCapacity"] == 3
+        assert fleets[0]["FleetState"] == "deleted_terminating"
+
+        # Instances should be terminated
+        instance_res = ctxt.ec2.describe_fleet_instances(FleetId=fleet_id)
+        instances = instance_res["ActiveInstances"]
+        assert len(instances) == 0
 
 
 @mock_ec2
-def test_describe_fleet_instences_api():
+def test_describe_fleet_instances_api():
     conn = boto3.client("ec2", region_name="us-west-1")
 
     launch_template_id, _ = get_launch_template(conn)
diff --git a/tests/test_glue/test_datacatalog.py b/tests/test_glue/test_datacatalog.py
index c3e0336ee..b1d02f986 100644
--- a/tests/test_glue/test_datacatalog.py
+++ b/tests/test_glue/test_datacatalog.py
@@ -27,7 +27,8 @@ def test_create_database():
     response = helpers.get_database(client, database_name)
     database = response["Database"]
 
-    assert database.get("Name") == database_name
+    assert database["Name"] == database_name
+    assert database["CatalogId"] == ACCOUNT_ID
     assert database.get("Description") == database_input.get("Description")
     assert database.get("LocationUri") == database_input.get("LocationUri")
     assert database.get("Parameters") == database_input.get("Parameters")
@@ -67,14 +68,11 @@ def test_get_database_not_exits():
 
 
 @mock_glue
-def test_get_databases_empty():
+def test_get_databases():
     client = boto3.client("glue", region_name="us-east-1")
     response = client.get_databases()
     assert len(response["DatabaseList"]) == 0
 
-
-@mock_glue
-def test_get_databases_several_items():
     client = boto3.client("glue", region_name="us-east-1")
     database_name_1, database_name_2 = "firstdatabase", "seconddatabase"
 
@@ -86,7 +84,9 @@ def test_get_databases_several_items():
     )
     assert len(database_list) == 2
     assert database_list[0]["Name"] == database_name_1
+    assert database_list[0]["CatalogId"] == ACCOUNT_ID
     assert database_list[1]["Name"] == database_name_2
+    assert database_list[1]["CatalogId"] == ACCOUNT_ID
 
 
 @mock_glue
@@ -222,6 +222,7 @@ def test_get_tables():
             table["StorageDescriptor"] == table_inputs[table_name]["StorageDescriptor"]
         )
         assert table["PartitionKeys"] == table_inputs[table_name]["PartitionKeys"]
+        assert table["CatalogId"] == ACCOUNT_ID
 
 
 @mock_glue
@@ -319,6 +320,7 @@ def test_get_table_versions():
     table = client.get_table(DatabaseName=database_name, Name=table_name)["Table"]
     assert table["StorageDescriptor"]["Columns"] == []
     assert table["VersionId"] == "1"
+    assert table["CatalogId"] == ACCOUNT_ID
 
     columns = [{"Name": "country", "Type": "string"}]
     table_input = helpers.create_table_input(database_name, table_name, columns=columns)
diff --git a/tests/test_redshift/test_redshift.py b/tests/test_redshift/test_redshift.py
index 2d6baa7a2..b765e10dd 100644
--- a/tests/test_redshift/test_redshift.py
+++ b/tests/test_redshift/test_redshift.py
@@ -219,6 +219,7 @@ def test_create_cluster_all_attributes():
     assert cluster["ClusterVersion"] == "1.0"
     assert cluster["AllowVersionUpgrade"] is True
     assert cluster["NumberOfNodes"] == 3
+    assert cluster["TotalStorageCapacityInMegaBytes"] == 0
 
 
 @mock_redshift
diff --git a/tests/test_ses/__init__.py b/tests/test_ses/__init__.py
index 08a1c1568..914f7dfe5 100644
--- a/tests/test_ses/__init__.py
+++ b/tests/test_ses/__init__.py
@@ -1,1 +1,28 @@
-# This file is intentionally left blank.
+import os
+from functools import wraps
+from moto import mock_ses
+
+
+def ses_aws_verified(func):
+    """
+    Function that is verified to work against AWS.
+    Can be run against AWS at any time by setting:
+      MOTO_TEST_ALLOW_AWS_REQUEST=true
+
+    If this environment variable is not set, the function runs in a `mock_ses` context.
+    """
+
+    @wraps(func)
+    def pagination_wrapper():
+        allow_aws_request = (
+            os.environ.get("MOTO_TEST_ALLOW_AWS_REQUEST", "false").lower() == "true"
+        )
+
+        if allow_aws_request:
+            resp = func()
+        else:
+            with mock_ses():
+                resp = func()
+        return resp
+
+    return pagination_wrapper
diff --git a/tests/test_ses/test_ses_boto3.py b/tests/test_ses/test_ses_boto3.py
index 7e04985c9..eda9cb1b0 100644
--- a/tests/test_ses/test_ses_boto3.py
+++ b/tests/test_ses/test_ses_boto3.py
@@ -7,6 +7,7 @@ from botocore.exceptions import ClientError
 from botocore.exceptions import ParamValidationError
 import pytest
 from moto import mock_ses
+from . import ses_aws_verified
 
 
 @mock_ses
@@ -1296,8 +1297,8 @@ def test_render_template():
     )
 
 
-@mock_ses
-def test_render_template__with_foreach():
+@ses_aws_verified
+def test_render_template__advanced():
     conn = boto3.client("ses", region_name="us-east-1")
 
     kwargs = {
@@ -1305,24 +1306,27 @@ def test_render_template__with_foreach():
         "TemplateData": json.dumps(
             {
                 "items": [
-                    {"type": "dog", "name": "bobby"},
-                    {"type": "cat", "name": "pedro"},
+                    {"type": "dog", "name": "bobby", "best": True},
+                    {"type": "cat", "name": "pedro", "best": False},
                 ]
             }
         ),
     }
 
-    conn.create_template(
-        Template={
-            "TemplateName": "MTT",
-            "SubjectPart": "..",
-            "TextPart": "..",
-            "HtmlPart": "{{#each items}} {{name}} is a {{type}}, {{/each}}",
-        }
-    )
-    result = conn.test_render_template(**kwargs)
-    assert "bobby is a dog" in result["RenderedTemplate"]
-    assert "pedro is a cat" in result["RenderedTemplate"]
+    try:
+        conn.create_template(
+            Template={
+                "TemplateName": "MTT",
+                "SubjectPart": "..",
+                "TextPart": "..",
+                "HtmlPart": "{{#each items}} {{name}} is {{#if best}}the best{{else}}a {{type}}{{/if}}, {{/each}}",
+            }
+        )
+        result = conn.test_render_template(**kwargs)
+        assert "bobby is the best" in result["RenderedTemplate"]
+        assert "pedro is a cat" in result["RenderedTemplate"]
+    finally:
+        conn.delete_template(TemplateName="MTT")
 
 
 @mock_ses
diff --git a/tests/test_ses/test_templating.py b/tests/test_ses/test_templating.py
index 9d2ba4372..723d77649 100644
--- a/tests/test_ses/test_templating.py
+++ b/tests/test_ses/test_templating.py
@@ -34,3 +34,50 @@ def test_template_with_single_curly_brace():
     assert parse_template(template, template_data={"name": "John"}) == (
         "Dear John my {brace} is fine."
     )
+
+
+def test_template_with_if():
+    template = "Dear {{ #if name }}John{{ /if }}"
+    assert parse_template(template, template_data={"name": True}) == "Dear John"
+    assert parse_template(template, template_data={"name": False}) == "Dear "
+
+
+def test_template_with_if_else():
+    template = "Dear {{ #if name }}John{{ else }}English{{ /if }}"
+    assert parse_template(template, template_data={"name": True}) == "Dear John"
+    assert parse_template(template, template_data={"name": False}) == "Dear English"
+
+
+def test_template_with_nested_foreaches():
+    template = (
+        "{{#each l}} - {{obj}} {{#each l2}} {{o1}} {{/each}} infix-each {{/each}}"
+    )
+    kwargs = {
+        "name": True,
+        "l": [
+            {
+                "obj": "it1",
+                "l2": [{"o1": "ob1", "o2": "ob2"}, {"o1": "oc1", "o2": "oc2"}],
+            },
+            {"obj": "it2", "l2": [{"o1": "ob3"}]},
+        ],
+    }
+    assert (
+        parse_template(template, kwargs)
+        == " - it1  ob1  oc1  infix-each  - it2  ob3  infix-each "
+    )
+
+
+def test_template_with_nested_ifs_and_foreaches():
+    template = "{{#each l}} - {{obj}} {{#if name}} post-if {{#each l2}} {{o1}} {{/each}} pre-if-end {{else}}elsedata{{/if}} post-if {{/each}}"
+    kwargs = {
+        "name": True,
+        "l": [
+            {"obj": "it1", "name": True, "l2": [{"o1": "ob1"}]},
+            {"obj": "it2", "name": False},
+        ],
+    }
+    assert (
+        parse_template(template, kwargs)
+        == " - it1  post-if  ob1  pre-if-end  post-if  - it2 elsedata post-if "
+    )