Repository: getmoto/moto
Pull request #7420 (2024-03-04 01:50:33)

S3: Add EventBridge Notification for DeleteObject
PR for DeleteObject in #7363 
 - send EventBridge notification when Object is Deleted(for buckets that are versioned and not versioned)
 - added a test case for sending eventbridge notification


Patch:
diff --git a/moto/athena/models.py b/moto/athena/models.py
index bdba60cc0..a7d5e4a4b 100644
--- a/moto/athena/models.py
+++ b/moto/athena/models.py
@@ -86,7 +86,9 @@ class DataCatalog(TaggableResourceMixin, BaseModel):
 
 
 class Execution(BaseModel):
-    def __init__(self, query: str, context: str, config: str, workgroup: WorkGroup):
+    def __init__(
+        self, query: str, context: str, config: Dict[str, Any], workgroup: WorkGroup
+    ):
         self.id = str(mock_random.uuid4())
         self.query = query
         self.context = context
@@ -95,6 +97,11 @@ class Execution(BaseModel):
         self.start_time = time.time()
         self.status = "SUCCEEDED"
 
+        if "OutputLocation" in self.config:
+            if not self.config["OutputLocation"].endswith("/"):
+                self.config["OutputLocation"] += "/"
+            self.config["OutputLocation"] += f"{self.id}.csv"
+
 
 class QueryResults(BaseModel):
     def __init__(self, rows: List[Dict[str, Any]], column_info: List[Dict[str, str]]):
@@ -213,7 +220,7 @@ class AthenaBackend(BaseBackend):
         }
 
     def start_query_execution(
-        self, query: str, context: str, config: str, workgroup: WorkGroup
+        self, query: str, context: str, config: Dict[str, Any], workgroup: WorkGroup
     ) -> str:
         execution = Execution(
             query=query, context=context, config=config, workgroup=workgroup
diff --git a/moto/backend_index.py b/moto/backend_index.py
index c413f9288..5e0d4455b 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -142,6 +142,10 @@ backend_url_patterns = [
         "route53resolver",
         re.compile("https?://route53resolver\\.(.+)\\.amazonaws\\.com"),
     ),
+    (
+        "route53domains",
+        re.compile("https?://route53domains\\.(.+)\\.amazonaws\\.com"),
+    ),
     ("s3", re.compile("https?://s3(?!-control)(.*)\\.amazonaws.com")),
     (
         "s3",
diff --git a/moto/backends.py b/moto/backends.py
index 14ba4ad92..e7d636f1e 100644
--- a/moto/backends.py
+++ b/moto/backends.py
@@ -108,6 +108,7 @@ if TYPE_CHECKING:
     from moto.resourcegroupstaggingapi.models import ResourceGroupsTaggingAPIBackend
     from moto.robomaker.models import RoboMakerBackend
     from moto.route53.models import Route53Backend
+    from moto.route53domains.models import Route53DomainsBackend
     from moto.route53resolver.models import Route53ResolverBackend
     from moto.s3.models import S3Backend
     from moto.s3control.models import S3ControlBackend
@@ -266,6 +267,7 @@ SERVICE_NAMES = Union[
     "Literal['robomaker']",
     "Literal['route53']",
     "Literal['route53resolver']",
+    "Literal['route53domains']",
     "Literal['s3']",
     "Literal['s3bucket_path']",
     "Literal['s3control']",
@@ -506,6 +508,8 @@ def get_backend(name: "Literal['route53']") -> "BackendDict[Route53Backend]": ..
 @overload
 def get_backend(name: "Literal['route53resolver']") -> "BackendDict[Route53ResolverBackend]": ...
 @overload
+def get_backend(name: "Literal['route53domains']") -> "BackendDict[Route53DomainsBackend]": ...
+@overload
 def get_backend(name: "Literal['s3']") -> "BackendDict[S3Backend]": ...
 @overload
 def get_backend(name: "Literal['s3bucket_path']") -> "BackendDict[S3Backend]": ...
diff --git a/moto/cloudfront/models.py b/moto/cloudfront/models.py
index d5217f317..588030408 100644
--- a/moto/cloudfront/models.py
+++ b/moto/cloudfront/models.py
@@ -354,20 +354,7 @@ class CloudFrontBackend(BaseBackend):
             raise NoSuchDistribution
         dist = self.distributions[_id]
 
-        if dist_config.get("Aliases", {}).get("Items") is not None:
-            aliases = dist_config["Aliases"]["Items"]["CNAME"]
-            dist.distribution_config.aliases = aliases
-        origin = dist_config["Origins"]["Items"]["Origin"]
-        dist.distribution_config.config = dist_config
-        dist.distribution_config.origins = (
-            [Origin(o) for o in origin]
-            if isinstance(origin, list)
-            else [Origin(origin)]
-        )
-        if dist_config.get("DefaultRootObject") is not None:
-            dist.distribution_config.default_root_object = dist_config[
-                "DefaultRootObject"
-            ]
+        dist.distribution_config = DistributionConfig(dist_config)
         self.distributions[_id] = dist
         dist.advance()
         return dist, dist.location, dist.etag
diff --git a/moto/core/responses.py b/moto/core/responses.py
index b6ae0270e..d091b2157 100644
--- a/moto/core/responses.py
+++ b/moto/core/responses.py
@@ -633,9 +633,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
         if self.body is not None:
             try:
                 return json.loads(self.body)[param_name]
-            except ValueError:
-                pass
-            except KeyError:
+            except (ValueError, KeyError):
                 pass
         # try to get path parameter
         if self.uri_match:
diff --git a/moto/ec2/models/core.py b/moto/ec2/models/core.py
index 0ecc94dcf..581e44dd3 100644
--- a/moto/ec2/models/core.py
+++ b/moto/ec2/models/core.py
@@ -41,3 +41,10 @@ class TaggedEC2Resource(BaseModel):
             return value
 
         raise FilterNotImplementedError(filter_name, method_name)
+
+    def match_tags(self, filters: Dict[str, str]) -> bool:
+        for tag_name in filters.keys():
+            tag_value = self.get_filter_value(tag_name)
+            if tag_value == filters[tag_name][0]:
+                return True
+        return False
diff --git a/moto/ec2/models/security_groups.py b/moto/ec2/models/security_groups.py
index b432cca39..0a776d76f 100644
--- a/moto/ec2/models/security_groups.py
+++ b/moto/ec2/models/security_groups.py
@@ -33,12 +33,15 @@ class SecurityRule(TaggedEC2Resource):
         self,
         ec2_backend: Any,
         ip_protocol: str,
+        group_id: str,
         from_port: Optional[str],
         to_port: Optional[str],
         ip_ranges: Optional[List[Any]],
         source_groups: List[Dict[str, Any]],
         prefix_list_ids: Optional[List[Dict[str, str]]] = None,
         is_egress: bool = True,
+        tags: Dict[str, str] = {},
+        description: str = "",
     ):
         self.ec2_backend = ec2_backend
         self.id = random_security_group_rule_id()
@@ -48,6 +51,8 @@ class SecurityRule(TaggedEC2Resource):
         self.prefix_list_ids = prefix_list_ids or []
         self.from_port = self.to_port = None
         self.is_egress = is_egress
+        self.description = description
+        self.group_id = group_id
 
         if self.ip_protocol and self.ip_protocol != "-1":
             self.from_port = int(from_port)  # type: ignore[arg-type]
@@ -73,6 +78,7 @@ class SecurityRule(TaggedEC2Resource):
             else None
         )
         self.ip_protocol = proto if proto else self.ip_protocol
+        self.add_tags(tags)
 
     @property
     def owner_id(self) -> str:
@@ -156,6 +162,7 @@ class SecurityGroup(TaggedEC2Resource, CloudFormationModel):
                     SecurityRule(
                         self.ec2_backend,
                         "-1",
+                        self.id,
                         None,
                         None,
                         [{"CidrIp": "0.0.0.0/0"}],
@@ -167,6 +174,7 @@ class SecurityGroup(TaggedEC2Resource, CloudFormationModel):
                     SecurityRule(
                         self.ec2_backend,
                         "-1",
+                        self.id,
                         None,
                         None,
                         [{"CidrIpv6": "::/0"}],
@@ -571,22 +579,60 @@ class SecurityGroupBackend:
         return matches
 
     def describe_security_group_rules(
-        self, group_ids: Optional[List[str]] = None, filters: Any = None
-    ) -> Dict[str, List[SecurityRule]]:
-        matches = self.describe_security_groups(group_ids=group_ids, filters=filters)
-        if not matches:
-            raise InvalidSecurityGroupNotFoundError(
-                "No security groups found matching the filters provided."
+        self,
+        group_ids: Optional[List[str]] = None,
+        sg_rule_ids: List[str] = [],
+        filters: Any = None,
+    ) -> List[SecurityRule]:
+        results = []
+
+        if sg_rule_ids:
+            # go thru all the rules in the backend to find a match
+            for sg_rule_id in sg_rule_ids:
+                for sg in self.sg_old_ingress_ruls:
+                    for rule in self.sg_old_ingress_ruls[sg]:
+                        if rule.id == sg_rule_id:
+                            results.append(rule)
+
+            return results
+
+        if group_ids:
+            all_sgs = self.describe_security_groups(group_ids=group_ids)
+            for group in all_sgs:
+                results.extend(group.ingress_rules)
+                results.extend(group.egress_rules)
+
+            return results
+
+        if filters and "group-id" in filters:
+            matches = self.describe_security_groups(
+                group_ids=group_ids, filters=filters
             )
-        rules = {}
-        for group in matches:
-            group_rules = []
-            group_rules.extend(group.ingress_rules)
-            group_rules.extend(group.egress_rules)
-            if group_rules:
-                rules[group.group_id] = group_rules
+            if not matches:
+                raise InvalidSecurityGroupNotFoundError(
+                    "No security groups found matching the filters provided."
+                )
+            for group in matches:
+                results.extend(group.ingress_rules)
+                results.extend(group.egress_rules)
+
+            return results
+
+        all_sgs = self.describe_security_groups()
 
-        return rules
+        for group in all_sgs:
+            results.extend(self._match_sg_rules(group.ingress_rules, filters))
+            results.extend(self._match_sg_rules(group.egress_rules, filters))
+
+        return results
+
+    @staticmethod
+    def _match_sg_rules(rules_list: List[SecurityRule], filters: Any) -> List[SecurityRule]:  # type: ignore[misc]
+        results = []
+        for rule in rules_list:
+            if rule.match_tags(filters):
+                results.append(rule)
+        return results
 
     def _delete_security_group(self, vpc_id: Optional[str], group_id: str) -> None:
         vpc_id = vpc_id or self.default_vpc.id  # type: ignore[attr-defined]
@@ -657,6 +703,7 @@ class SecurityGroupBackend:
         from_port: str,
         to_port: str,
         ip_ranges: List[Any],
+        sgrule_tags: Dict[str, str] = {},
         source_groups: Optional[List[Dict[str, str]]] = None,
         prefix_list_ids: Optional[List[Dict[str, str]]] = None,
         security_rule_ids: Optional[List[str]] = None,  # pylint:disable=unused-argument
@@ -695,12 +742,14 @@ class SecurityGroupBackend:
         security_rule = SecurityRule(
             self,
             ip_protocol,
+            group.group_id,
             from_port,
             to_port,
             ip_ranges,
             _source_groups,
             prefix_list_ids,
             is_egress=False,
+            tags=sgrule_tags,
         )
 
         if security_rule in group.ingress_rules:
@@ -738,6 +787,7 @@ class SecurityGroupBackend:
                 break
         else:
             group.add_ingress_rule(security_rule)
+
         return security_rule, group
 
     def revoke_security_group_ingress(
@@ -765,6 +815,7 @@ class SecurityGroupBackend:
         security_rule = SecurityRule(
             self,
             ip_protocol,
+            group.group_id,
             from_port,
             to_port,
             ip_ranges,
@@ -819,6 +870,7 @@ class SecurityGroupBackend:
         from_port: str,
         to_port: str,
         ip_ranges: List[Any],
+        sgrule_tags: Dict[str, str] = {},
         source_groups: Optional[List[Dict[str, Any]]] = None,
         prefix_list_ids: Optional[List[Dict[str, str]]] = None,
         security_rule_ids: Optional[List[str]] = None,  # pylint:disable=unused-argument
@@ -860,11 +912,13 @@ class SecurityGroupBackend:
         security_rule = SecurityRule(
             self,
             ip_protocol,
+            group.group_id,
             from_port,
             to_port,
             ip_ranges,
             _source_groups,
             prefix_list_ids,
+            tags=sgrule_tags,
         )
 
         if security_rule in group.egress_rules:
@@ -944,6 +998,7 @@ class SecurityGroupBackend:
         security_rule = SecurityRule(
             self,
             ip_protocol,
+            group.group_id,
             from_port,
             to_port,
             ip_ranges,
@@ -1030,6 +1085,7 @@ class SecurityGroupBackend:
         security_rule = SecurityRule(
             self,
             ip_protocol,
+            group.group_id,
             from_port,
             to_port,
             ip_ranges,
@@ -1085,6 +1141,7 @@ class SecurityGroupBackend:
         security_rule = SecurityRule(
             self,
             ip_protocol,
+            group.group_id,
             from_port,
             to_port,
             ip_ranges,
diff --git a/moto/ec2/models/vpcs.py b/moto/ec2/models/vpcs.py
index 4c4b67f5f..299f17d4b 100644
--- a/moto/ec2/models/vpcs.py
+++ b/moto/ec2/models/vpcs.py
@@ -263,6 +263,22 @@ class VPC(TaggedEC2Resource, CloudFormationModel):
         # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html
         return "AWS::EC2::VPC"
 
+    def get_cfn_attribute(self, attribute_name: str) -> Any:
+        from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
+
+        if attribute_name == "CidrBlock":
+            return self.cidr_block
+        elif attribute_name == "CidrBlockAssociations":
+            return self.cidr_block_association_set
+        elif attribute_name == "DefaultSecurityGroup":
+            sec_group = self.ec2_backend.get_security_group_from_name(
+                "default", vpc_id=self.id
+            )
+            return sec_group.id
+        elif attribute_name == "VpcId":
+            return self.id
+        raise UnformattedGetAttTemplateException()
+
     @classmethod
     def create_from_cloudformation_json(  # type: ignore[misc]
         cls,
diff --git a/moto/ec2/responses/security_groups.py b/moto/ec2/responses/security_groups.py
index b65949291..49553d05e 100644
--- a/moto/ec2/responses/security_groups.py
+++ b/moto/ec2/responses/security_groups.py
@@ -86,6 +86,8 @@ class SecurityGroups(EC2BaseResponse):
                 d = d[subkey]
             d[key_splitted[-1]] = value
 
+        sg_rule_tags = self._parse_tag_specification().get("security-group-rule", {})
+
         if "IpPermissions" not in querytree:
             # Handle single rule syntax
             (
@@ -97,16 +99,17 @@ class SecurityGroups(EC2BaseResponse):
                 prefix_list_ids,
             ) = parse_sg_attributes_from_dict(querytree)
 
-            yield (
-                group_name_or_id,
-                ip_protocol,
-                from_port,
-                to_port,
-                ip_ranges,
-                source_groups,
-                prefix_list_ids,
-                security_rule_ids,
-            )
+            yield {
+                "group_name_or_id": group_name_or_id,
+                "ip_protocol": ip_protocol,
+                "from_port": from_port,
+                "to_port": to_port,
+                "ip_ranges": ip_ranges,
+                "sgrule_tags": sg_rule_tags,
+                "source_groups": source_groups,
+                "prefix_list_ids": prefix_list_ids,
+                "security_rule_ids": security_rule_ids,
+            }
 
         ip_permissions = querytree.get("IpPermissions") or {}
         for ip_permission_idx in sorted(ip_permissions.keys()):
@@ -121,22 +124,23 @@ class SecurityGroups(EC2BaseResponse):
                 prefix_list_ids,
             ) = parse_sg_attributes_from_dict(ip_permission)
 
-            yield (
-                group_name_or_id,
-                ip_protocol,
-                from_port,
-                to_port,
-                ip_ranges,
-                source_groups,
-                prefix_list_ids,
-                security_rule_ids,
-            )
+            yield {
+                "group_name_or_id": group_name_or_id,
+                "ip_protocol": ip_protocol,
+                "from_port": from_port,
+                "to_port": to_port,
+                "ip_ranges": ip_ranges,
+                "sgrule_tags": sg_rule_tags,
+                "source_groups": source_groups,
+                "prefix_list_ids": prefix_list_ids,
+                "security_rule_ids": security_rule_ids,
+            }
 
     def authorize_security_group_egress(self) -> str:
         self.error_on_dryrun()
 
-        for args in self._process_rules_from_querystring():
-            rule, group = self.ec2_backend.authorize_security_group_egress(*args)
+        for kwargs in self._process_rules_from_querystring():
+            rule, group = self.ec2_backend.authorize_security_group_egress(**kwargs)
         self.ec2_backend.sg_old_egress_ruls[group.id] = group.egress_rules.copy()
         template = self.response_template(AUTHORIZE_SECURITY_GROUP_EGRESS_RESPONSE)
         return template.render(rule=rule, group=group)
@@ -144,8 +148,8 @@ class SecurityGroups(EC2BaseResponse):
     def authorize_security_group_ingress(self) -> str:
         self.error_on_dryrun()
 
-        for args in self._process_rules_from_querystring():
-            rule, group = self.ec2_backend.authorize_security_group_ingress(*args)
+        for kwargs in self._process_rules_from_querystring():
+            rule, group = self.ec2_backend.authorize_security_group_ingress(**kwargs)
         self.ec2_backend.sg_old_ingress_ruls[group.id] = group.ingress_rules.copy()
         template = self.response_template(AUTHORIZE_SECURITY_GROUP_INGRESS_RESPONSE)
         return template.render(rule=rule, group=group)
@@ -198,11 +202,19 @@ class SecurityGroups(EC2BaseResponse):
 
     def describe_security_group_rules(self) -> str:
         group_id = self._get_param("GroupId")
+        sg_rule_ids = self._get_param("SecurityGroupRuleId.1")
         filters = self._filters_from_querystring()
 
         self.error_on_dryrun()
 
-        rules = self.ec2_backend.describe_security_group_rules(group_id, filters)
+        # if sg rule ids are not None then wrap in a list
+        # as expected by ec2_backend.describe_security_group_rules
+        if sg_rule_ids:
+            sg_rule_ids = [sg_rule_ids]
+
+        rules = self.ec2_backend.describe_security_group_rules(
+            group_id, sg_rule_ids, filters
+        )
         template = self.response_template(DESCRIBE_SECURITY_GROUP_RULES_RESPONSE)
         return template.render(rules=rules)
 
@@ -210,28 +222,36 @@ class SecurityGroups(EC2BaseResponse):
         self.error_on_dryrun()
 
         for args in self._process_rules_from_querystring():
-            self.ec2_backend.revoke_security_group_egress(*args)
+            # we don't need this parameter to revoke
+            del args["sgrule_tags"]
+            self.ec2_backend.revoke_security_group_egress(**args)
         return REVOKE_SECURITY_GROUP_EGRESS_RESPONSE
 
     def revoke_security_group_ingress(self) -> str:
         self.error_on_dryrun()
 
         for args in self._process_rules_from_querystring():
-            self.ec2_backend.revoke_security_group_ingress(*args)
+            # we don't need this parameter to revoke
+            del args["sgrule_tags"]
+            self.ec2_backend.revoke_security_group_ingress(**args)
         return REVOKE_SECURITY_GROUP_INGRESS_RESPONSE
 
     def update_security_group_rule_descriptions_ingress(self) -> str:
         for args in self._process_rules_from_querystring():
+            # we don't need this parameter to revoke
+            del args["sgrule_tags"]
             group = self.ec2_backend.update_security_group_rule_descriptions_ingress(
-                *args
+                **args
             )
         self.ec2_backend.sg_old_ingress_ruls[group.id] = group.ingress_rules.copy()
         return UPDATE_SECURITY_GROUP_RULE_DESCRIPTIONS_INGRESS
 
     def update_security_group_rule_descriptions_egress(self) -> str:
         for args in self._process_rules_from_querystring():
+            # we don't need this parameter to revoke
+            del args["sgrule_tags"]
             group = self.ec2_backend.update_security_group_rule_descriptions_egress(
-                *args
+                **args
             )
         self.ec2_backend.sg_old_egress_ruls[group.id] = group.egress_rules.copy()
         return UPDATE_SECURITY_GROUP_RULE_DESCRIPTIONS_EGRESS
@@ -255,8 +275,7 @@ DESCRIBE_SECURITY_GROUP_RULES_RESPONSE = """
 <DescribeSecurityGroupRulesResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
   <requestId>{{ request_id }}</requestId>
   <securityGroupRuleSet>
-    {% for group, rule_list in rules.items() %}
-        {% for rule in rule_list %}
+        {% for rule in rules %}
             <item>
                 {% if rule.from_port is not none %}
                 <fromPort>{{ rule.from_port }}</fromPort>
@@ -268,7 +287,7 @@ DESCRIBE_SECURITY_GROUP_RULES_RESPONSE = """
                   <cidrIpv4>{{ rule.ip_ranges[0]['CidrIp'] }}</cidrIpv4>
                 {% endif %}
                 <ipProtocol>{{ rule.ip_protocol }}</ipProtocol>
-                <groupId>{{ group }}</groupId>
+                <groupId>{{ rule.group_id }}</groupId>
                 <groupOwnerId>{{ rule.owner_id }}</groupOwnerId>
                 <isEgress>{{ 'true' if rule.is_egress else 'false' }}</isEgress>
                 <securityGroupRuleId>{{ rule.id }}</securityGroupRuleId>
@@ -282,7 +301,6 @@ DESCRIBE_SECURITY_GROUP_RULES_RESPONSE = """
                 </tagSet> 
             </item>
         {% endfor %}
-    {% endfor %}
   </securityGroupRuleSet>
 </DescribeSecurityGroupRulesResponse>"""
 
@@ -470,6 +488,14 @@ AUTHORIZE_SECURITY_GROUP_INGRESS_RESPONSE = """<AuthorizeSecurityGroupIngressRes
             {% if rule.to_port is not none %}
             <toPort>{{ rule.to_port }}</toPort>
             {% endif %}
+            <tagSet>
+                {% for tag in rule.get_tags() %}
+                    <item>
+                      <key>{{ tag.key }}</key>
+                      <value>{{ tag.value }}</value>
+                    </item>
+                {% endfor %}
+            </tagSet> 
         </item>
     {% endfor %}
     {% for item in rule.prefix_list_ids %}
@@ -550,6 +576,14 @@ AUTHORIZE_SECURITY_GROUP_EGRESS_RESPONSE = """<AuthorizeSecurityGroupEgressRespo
             {% if rule.to_port is not none %}
             <toPort>{{ rule.to_port }}</toPort>
             {% endif %}
+            <tagSet>
+                {% for tag in rule.get_tags() %}
+                    <item>
+                      <key>{{ tag.key }}</key>
+                      <value>{{ tag.value }}</value>
+                    </item>
+                {% endfor %}
+            </tagSet> 
         </item>
     {% endfor %}
     {% for item in rule.prefix_list_ids %}
diff --git a/moto/efs/models.py b/moto/efs/models.py
index 33df67491..ce1419243 100644
--- a/moto/efs/models.py
+++ b/moto/efs/models.py
@@ -10,7 +10,7 @@ from copy import deepcopy
 from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union
 
 from moto.core.base_backend import BackendDict, BaseBackend
-from moto.core.common_models import BaseModel, CloudFormationModel
+from moto.core.common_models import CloudFormationModel
 from moto.core.utils import camelcase_to_underscores, underscores_to_camelcase
 from moto.ec2 import ec2_backends
 from moto.ec2.exceptions import InvalidSubnetIdError
@@ -43,7 +43,7 @@ def _lookup_az_id(account_id: str, az_name: str) -> Optional[str]:
     return None
 
 
-class AccessPoint(BaseModel):
+class AccessPoint(CloudFormationModel):
     def __init__(
         self,
         account_id: str,
@@ -84,6 +84,38 @@ class AccessPoint(BaseModel):
             "LifeCycleState": "available",
         }
 
+    @staticmethod
+    def cloudformation_type() -> str:
+        return "AWS::EFS::AccessPoint"
+
+    @classmethod
+    def create_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+        **kwargs: Any,
+    ) -> "AccessPoint":
+        props = cloudformation_json["Properties"]
+        file_system_id = props["FileSystemId"]
+        posix_user = props.get("PosixUser", {})
+        root_directory = props.get("RootDirectory", {})
+        tags = props.get("AccessPointTags", [])
+
+        backend: EFSBackend = efs_backends[account_id][region_name]
+        return backend.create_access_point(
+            resource_name,
+            file_system_id=file_system_id,
+            posix_user=posix_user,
+            root_directory=root_directory,
+            tags=tags,
+        )
+
+    def delete(self, account_id: str, region_name: str) -> None:
+        backend: EFSBackend = efs_backends[account_id][region_name]
+        backend.delete_access_point(self.access_point_id)
+
 
 class FileSystem(CloudFormationModel):
     """A model for an EFS File System Volume."""
@@ -223,40 +255,36 @@ class FileSystem(CloudFormationModel):
         region_name: str,
         **kwargs: Any,
     ) -> "FileSystem":
-        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-efs-filesystem.html
-        props = deepcopy(cloudformation_json["Properties"])
-        props = {camelcase_to_underscores(k): v for k, v in props.items()}
-        if "file_system_tags" in props:
-            props["tags"] = props.pop("file_system_tags")
-        if "backup_policy" in props:
-            if "status" not in props["backup_policy"]:
-                raise ValueError("BackupPolicy must be of type BackupPolicy.")
-            status = props.pop("backup_policy")["status"]
-            if status not in ["ENABLED", "DISABLED"]:
-                raise ValueError(f'Invalid status: "{status}".')
-            props["backup"] = status == "ENABLED"
-        if "bypass_policy_lockout_safety_check" in props:
-            raise ValueError(
-                "BypassPolicyLockoutSafetyCheck not currently "
-                "supported by AWS Cloudformation."
-            )
-
-        return efs_backends[account_id][region_name].create_file_system(
-            resource_name, **props
+        props = cloudformation_json["Properties"]
+        performance_mode = props.get("PerformanceMode", "generalPurpose")
+        encrypted = props.get("Encrypted", False)
+        kms_key_id = props.get("KmsKeyId")
+        throughput_mode = props.get("ThroughputMode", "bursting")
+        provisioned_throughput_in_mibps = props.get("ProvisionedThroughputInMibps", 0)
+        availability_zone_name = props.get("AvailabilityZoneName")
+        backup = props.get("BackupPolicy", {}).get("Status") == "ENABLED"
+        tags = props.get("FileSystemTags", [])
+
+        lifecycle_policies = props.get("LifecyclePolicies", [])
+
+        backend: EFSBackend = efs_backends[account_id][region_name]
+        fs = backend.create_file_system(
+            resource_name,
+            performance_mode=performance_mode,
+            encrypted=encrypted,
+            kms_key_id=kms_key_id,
+            throughput_mode=throughput_mode,
+            provisioned_throughput_in_mibps=provisioned_throughput_in_mibps,
+            availability_zone_name=availability_zone_name,
+            backup=backup,
+            tags=tags,
         )
 
-    @classmethod
-    def update_from_cloudformation_json(  # type: ignore[misc]
-        cls,
-        original_resource: Any,
-        new_resource_name: str,
-        cloudformation_json: Any,
-        account_id: str,
-        region_name: str,
-    ) -> None:
-        raise NotImplementedError(
-            "Update of EFS File System via cloudformation is not yet implemented."
-        )
+        if lifecycle_policies:
+            backend.put_lifecycle_configuration(
+                file_system_id=fs.file_system_id, policies=lifecycle_policies
+            )
+        return fs
 
     @classmethod
     def delete_from_cloudformation_json(  # type: ignore[misc]
@@ -357,7 +385,6 @@ class MountTarget(CloudFormationModel):
         region_name: str,
         **kwargs: Any,
     ) -> "MountTarget":
-        # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-efs-mounttarget.html
         props = deepcopy(cloudformation_json["Properties"])
         props = {camelcase_to_underscores(k): v for k, v in props.items()}
         return efs_backends[account_id][region_name].create_mount_target(**props)
diff --git a/moto/emr/models.py b/moto/emr/models.py
index 58f65613c..c73923919 100644
--- a/moto/emr/models.py
+++ b/moto/emr/models.py
@@ -449,13 +449,64 @@ class FakeCluster(CloudFormationModel):
         cluster.add_tags(tags)
         return cluster
 
+    @classmethod
+    def delete_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        resource_name: str,
+        cloudformation_json: Dict[str, Any],
+        account_id: str,
+        region_name: str,
+    ) -> None:
+        emr_backend: ElasticMapReduceBackend = emr_backends[account_id][region_name]
+
+        emr_backend.terminate_job_flows([resource_name])
 
-class FakeSecurityConfiguration(BaseModel):
+
+class FakeSecurityConfiguration(CloudFormationModel):
     def __init__(self, name: str, security_configuration: str):
         self.name = name
         self.security_configuration = security_configuration
         self.creation_date_time = datetime.now(timezone.utc)
 
+    @property
+    def physical_resource_id(self) -> str:
+        return self.name
+
+    @staticmethod
+    def cloudformation_type() -> str:
+        return "AWS::EMR::SecurityConfiguration"
+
+    @classmethod
+    def create_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        resource_name: str,
+        cloudformation_json: Any,
+        account_id: str,
+        region_name: str,
+        **kwargs: Any,
+    ) -> "FakeSecurityConfiguration":
+        emr_backend: ElasticMapReduceBackend = emr_backends[account_id][region_name]
+
+        properties = cloudformation_json["Properties"]
+        return emr_backend.create_security_configuration(
+            name=properties.get("Name") or resource_name,
+            security_configuration=properties.get("SecurityConfiguration", {}),
+        )
+
+    @classmethod
+    def delete_from_cloudformation_json(  # type: ignore[misc]
+        cls,
+        resource_name: str,
+        cloudformation_json: Dict[str, Any],
+        account_id: str,
+        region_name: str,
+    ) -> None:
+        emr_backend: ElasticMapReduceBackend = emr_backends[account_id][region_name]
+
+        properties = cloudformation_json["Properties"]
+        name = properties.get("Name") or resource_name
+        emr_backend.delete_security_configuration(name)
+
 
 class ElasticMapReduceBackend(BaseBackend):
     def __init__(self, region_name: str, account_id: str):
diff --git a/moto/iam/models.py b/moto/iam/models.py
index 6a21e0781..1231544cb 100644
--- a/moto/iam/models.py
+++ b/moto/iam/models.py
@@ -957,7 +957,9 @@ class InstanceProfile(CloudFormationModel):
         account_id: str,
         region_name: str,
     ) -> None:
-        iam_backends[account_id]["global"].delete_instance_profile(resource_name)
+        iam_backends[account_id]["global"].delete_instance_profile(
+            resource_name, ignore_attached_roles=True
+        )
 
     def delete_role(self, role_name: str) -> None:
         self.roles = [role for role in self.roles if role.name != role_name]
@@ -2370,9 +2372,11 @@ class IAMBackend(BaseBackend):
         self.instance_profiles[name] = instance_profile
         return instance_profile
 
-    def delete_instance_profile(self, name: str) -> None:
+    def delete_instance_profile(
+        self, name: str, ignore_attached_roles: bool = False
+    ) -> None:
         instance_profile = self.get_instance_profile(name)
-        if len(instance_profile.roles) > 0:
+        if len(instance_profile.roles) > 0 and not ignore_attached_roles:
             raise IAMConflictException(
                 code="DeleteConflict",
                 message="Cannot delete entity, must remove roles from instance profile first.",
diff --git a/moto/route53domains/__init__.py b/moto/route53domains/__init__.py
new file mode 100644
index 000000000..132e14902
--- /dev/null
+++ b/moto/route53domains/__init__.py
@@ -0,0 +1,1 @@
+from .models import route53domains_backends  # noqa: F401
diff --git a/moto/route53domains/exceptions.py b/moto/route53domains/exceptions.py
new file mode 100644
index 000000000..bb4714ce6
--- /dev/null
+++ b/moto/route53domains/exceptions.py
@@ -0,0 +1,43 @@
+from typing import List
+
+from moto.core.exceptions import JsonRESTError
+
+
+class DomainLimitExceededException(JsonRESTError):
+    code = 400
+
+    def __init__(self) -> None:
+        super().__init__(
+            "DomainLimitExceeded",
+            "The number of registered domains has exceeded the allowed threshold for this account. If you want to "
+            "register more domains please request a higher quota",
+        )
+
+
+class DuplicateRequestException(JsonRESTError):
+    code = 400
+
+    def __init__(self) -> None:
+        super().__init__(
+            "DuplicateRequest", "The request is already in progress for the domain."
+        )
+
+
+class InvalidInputException(JsonRESTError):
+    code = 400
+
+    def __init__(self, error_msgs: List[str]):
+        error_msgs_str = "\n\t".join(error_msgs)
+        super().__init__(
+            "InvalidInput", f"The requested item is not acceptable.\n\t{error_msgs_str}"
+        )
+
+
+class UnsupportedTLDException(JsonRESTError):
+    code = 400
+
+    def __init__(self, tld: str):
+        super().__init__(
+            "UnsupportedTLD",
+            f"Amazon Route53 does not support the top-level domain (TLD) `.{tld}`.",
+        )
diff --git a/moto/route53domains/models.py b/moto/route53domains/models.py
new file mode 100644
index 000000000..3208b6f8d
--- /dev/null
+++ b/moto/route53domains/models.py
@@ -0,0 +1,302 @@
+from datetime import datetime, timedelta, timezone
+from typing import Any, Dict, List, Optional
+
+from moto.core.base_backend import BackendDict, BaseBackend
+from moto.route53 import route53_backends
+from moto.route53.models import Route53Backend
+from moto.utilities.paginator import paginate
+
+from .exceptions import (
+    DomainLimitExceededException,
+    DuplicateRequestException,
+    InvalidInputException,
+)
+from .validators import (
+    DOMAIN_OPERATION_STATUSES,
+    DOMAIN_OPERATION_TYPES,
+    DomainFilterField,
+    DomainsFilter,
+    DomainSortOrder,
+    DomainsSortCondition,
+    NameServer,
+    Route53Domain,
+    Route53DomainsContactDetail,
+    Route53DomainsOperation,
+    ValidationException,
+)
+
+
+class Route53DomainsBackend(BaseBackend):
+    """Implementation of Route53Domains APIs."""
+
+    DEFAULT_MAX_DOMAINS_COUNT = 20
+    PAGINATION_MODEL = {
+        "list_domains": {
+            "input_token": "marker",
+            "limit_key": "max_items",
+            "limit_default": 20,
+            "unique_attribute": "domain_name",
+        },
+        "list_operations": {
+            "input_token": "marker",
+            "limit_key": "max_items",
+            "limit_default": 20,
+            "unique_attribute": "id",
+        },
+    }
+
+    def __init__(self, region_name: str, account_id: str):
+        super().__init__(region_name, account_id)
+        self.__route53_backend: Route53Backend = route53_backends[account_id]["global"]
+        self.__domains: Dict[str, Route53Domain] = {}
+        self.__operations: Dict[str, Route53DomainsOperation] = {}
+
+    def register_domain(
+        self,
+        domain_name: str,
+        duration_in_years: int,
+        auto_renew: bool,
+        admin_contact: Dict[str, Any],
+        registrant_contact: Dict[str, Any],
+        tech_contact: Dict[str, Any],
+        private_protect_admin_contact: bool,
+        private_protect_registrant_contact: bool,
+        private_protect_tech_contact: bool,
+        extra_params: List[Dict[str, Any]],
+    ) -> Route53DomainsOperation:
+        """Register a domain"""
+
+        if len(self.__domains) == self.DEFAULT_MAX_DOMAINS_COUNT:
+            raise DomainLimitExceededException()
+
+        requested_operation = Route53DomainsOperation.validate(
+            domain_name=domain_name, status="SUCCESSFUL", type_="REGISTER_DOMAIN"
+        )
+
+        self.__validate_duplicate_operations(requested_operation)
+
+        expiration_date = datetime.now(timezone.utc) + timedelta(
+            days=365 * duration_in_years
+        )
+
+        try:
+
+            domain = Route53Domain.validate(
+                domain_name=domain_name,
+                auto_renew=auto_renew,
+                admin_contact=Route53DomainsContactDetail.validate_dict(admin_contact),
+                registrant_contact=Route53DomainsContactDetail.validate_dict(
+                    registrant_contact
+                ),
+                tech_contact=Route53DomainsContactDetail.validate_dict(tech_contact),
+                admin_privacy=private_protect_admin_contact,
+                registrant_privacy=private_protect_registrant_contact,
+                tech_privacy=private_protect_tech_contact,
+                expiration_date=expiration_date,
+                extra_params=extra_params,
+            )
+
+        except ValidationException as e:
+            raise InvalidInputException(e.errors)
+        self.__operations[requested_operation.id] = requested_operation
+
+        self.__route53_backend.create_hosted_zone(
+            name=domain.domain_name, private_zone=False
+        )
+
+        self.__domains[domain_name] = domain
+        return requested_operation
+
+    def delete_domain(self, domain_name: str) -> Route53DomainsOperation:
+        requested_operation = Route53DomainsOperation.validate(
+            domain_name=domain_name, status="SUCCESSFUL", type_="DELETE_DOMAIN"
+        )
+        self.__validate_duplicate_operations(requested_operation)
+
+        input_errors: List[str] = []
+        Route53Domain.validate_domain_name(domain_name, input_errors)
+
+        if input_errors:
+            raise InvalidInputException(input_errors)
+
+        if domain_name not in self.__domains:
+            raise InvalidInputException(
+                [f"Domain {domain_name} isn't registered in the current account"]
+            )
+
+        self.__operations[requested_operation.id] = requested_operation
+        del self.__domains[domain_name]
+        return requested_operation
+
+    def __validate_duplicate_operations(
+        self, requested_operation: Route53DomainsOperation
+    ) -> None:
+        for operation in self.__operations.values():
+            if (
+                operation.domain_name == requested_operation.domain_name
+                and operation.type == requested_operation.type
+            ):
+                raise DuplicateRequestException()
+
+    def get_domain(self, domain_name: str) -> Route53Domain:
+        input_errors: List[str] = []
+        Route53Domain.validate_domain_name(domain_name, input_errors)
+        if input_errors:
+            raise InvalidInputException(input_errors)
+
+        if domain_name not in self.__domains:
+            raise InvalidInputException(["Domain is not associated with this account"])
+
+        return self.__domains[domain_name]
+
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    def list_domains(
+        self,
+        filter_conditions: Optional[List[Dict[str, Any]]] = None,
+        sort_condition: Optional[Dict[str, Any]] = None,
+    ) -> List[Route53Domain]:
+        try:
+            filters: List[DomainsFilter] = (
+                [DomainsFilter.validate_dict(f) for f in filter_conditions]
+                if filter_conditions
+                else []
+            )
+            sort: Optional[DomainsSortCondition] = (
+                DomainsSortCondition.validate_dict(sort_condition)
+                if sort_condition
+                else None
+            )
+        except ValidationException as e:
+            raise InvalidInputException(e.errors)
+
+        filter_fields = [f.name for f in filters]
+        if sort and filter_fields and sort.name not in filter_fields:
+            raise InvalidInputException(
+                ["Sort condition must be the same as the filter condition"]
+            )
+
+        domains_to_return: List[Route53Domain] = []
+
+        for domain in self.__domains.values():
+            if all([f.filter(domain) for f in filters]):
+                domains_to_return.append(domain)
+
+        if sort:
+            if sort.name == DomainFilterField.DOMAIN_NAME:
+                domains_to_return.sort(
+                    key=lambda d: d.domain_name,
+                    reverse=(sort.sort_order == DomainSortOrder.DESCENDING),
+                )
+            else:
+                domains_to_return.sort(
+                    key=lambda d: d.expiration_date,
+                    reverse=(sort.sort_order == DomainSortOrder.DESCENDING),
+                )
+
+        return domains_to_return
+
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    def list_operations(
+        self,
+        submitted_since_timestamp: Optional[int] = None,
+        statuses: Optional[List[str]] = None,
+        types: Optional[List[str]] = None,
+        sort_by: Optional[str] = None,
+        sort_order: Optional[str] = None,
+    ) -> List[Route53DomainsOperation]:
+
+        input_errors: List[str] = []
+        statuses = statuses or []
+        types = types or []
+
+        if any(status not in DOMAIN_OPERATION_STATUSES for status in statuses):
+            input_errors.append("Status is invalid")
+        if any(type_ not in DOMAIN_OPERATION_TYPES for type_ in types):
+            input_errors.append("Type is invalid")
+
+        if input_errors:
+            raise InvalidInputException(input_errors)
+
+        submitted_since = (
+            datetime.fromtimestamp(submitted_since_timestamp, timezone.utc)
+            if submitted_since_timestamp
+            else None
+        )
+
+        operations_to_return: List[Route53DomainsOperation] = []
+
+        for operation in self.__operations.values():
+            if statuses and operation.status not in statuses:
+                continue
+
+            if types and operation.type not in types:
+                continue
+
+            if submitted_since and operation.submitted_date < submitted_since:
+                continue
+
+            operations_to_return.append(operation)
+
+        if sort_by == "SubmittedDate":
+            operations_to_return.sort(
+                key=lambda op: op.submitted_date,
+                reverse=sort_order == DomainSortOrder.ASCENDING,
+            )
+
+        return operations_to_return
+
+    def get_operation(self, operation_id: str) -> Route53DomainsOperation:
+        if operation_id not in self.__operations:
+            raise InvalidInputException(
+                [f"Operation with id {operation_id} doesn't exist"]
+            )
+
+        return self.__operations[operation_id]
+
+    def update_domain_nameservers(
+        self, domain_name: str, nameservers: List[Dict[str, Any]]
+    ) -> Route53DomainsOperation:
+        input_errors: List[str] = []
+        Route53Domain.validate_domain_name(domain_name, input_errors)
+        if len(nameservers) < 1:
+            input_errors.append("Must supply nameservers")
+
+        servers: List[NameServer] = []
+        try:
+            servers = [NameServer.validate_dict(obj) for obj in nameservers]
+        except ValidationException as e:
+            input_errors += e.errors
+
+        for server in servers:
+            if domain_name in server.name and not server.glue_ips:
+                input_errors.append(
+                    f"Must supply glue IPs for name server {server.name} because it is a subdomain of "
+                    f"the domain"
+                )
+
+        if input_errors:
+            raise InvalidInputException(input_errors)
+
+        if domain_name not in self.__domains:
+            raise InvalidInputException(
+                [f"Domain {domain_name} is not registered to the current AWS account"]
+            )
+
+        requested_operation = Route53DomainsOperation.validate(
+            domain_name=domain_name, status="SUCCESSFUL", type_="UPDATE_NAMESERVER"
+        )
+        self.__validate_duplicate_operations(requested_operation)
+
+        domain = self.__domains[domain_name]
+        domain.nameservers = servers
+        self.__operations[requested_operation.id] = requested_operation
+
+        return requested_operation
+
+
+route53domains_backends = BackendDict(
+    Route53DomainsBackend,
+    "route53domains",
+    use_boto3_regions=False,
+    additional_regions=["global"],
+)
diff --git a/moto/route53domains/responses.py b/moto/route53domains/responses.py
new file mode 100644
index 000000000..78287c7bc
--- /dev/null
+++ b/moto/route53domains/responses.py
@@ -0,0 +1,138 @@
+import json
+from typing import Any, Dict
+
+from moto.core.responses import BaseResponse
+from moto.route53domains.models import Route53DomainsBackend, route53domains_backends
+from moto.route53domains.validators import Route53Domain, Route53DomainsOperation
+
+
+class Route53DomainsResponse(BaseResponse):
+    def __init__(self) -> None:
+        super().__init__(service_name="route53-domains")
+
+    @property
+    def route53domains_backend(self) -> Route53DomainsBackend:
+        return route53domains_backends[self.current_account]["global"]
+
+    def register_domain(self) -> str:
+        domain_name = self._get_param("DomainName")
+        duration_in_years = self._get_int_param("DurationInYears")
+        auto_renew = self._get_bool_param("AutoRenew", if_none=True)
+        admin_contact = self._get_param("AdminContact")
+        registrant_contact = self._get_param("RegistrantContact")
+        tech_contact = self._get_param("TechContact")
+        privacy_protection_admin_contact = self._get_bool_param(
+            "PrivacyProtectAdminContact", if_none=True
+        )
+        privacy_protection_registrant_contact = self._get_bool_param(
+            "PrivacyProtectRegistrantContact", if_none=True
+        )
+        privacy_protection_tech_contact = self._get_bool_param(
+            "PrivacyProtectTechContact", if_none=True
+        )
+        extra_params = self._get_param("ExtraParams")
+
+        operation = self.route53domains_backend.register_domain(
+            domain_name=domain_name,
+            duration_in_years=duration_in_years,
+            auto_renew=auto_renew,
+            admin_contact=admin_contact,
+            registrant_contact=registrant_contact,
+            tech_contact=tech_contact,
+            private_protect_admin_contact=privacy_protection_admin_contact,
+            private_protect_registrant_contact=privacy_protection_registrant_contact,
+            private_protect_tech_contact=privacy_protection_tech_contact,
+            extra_params=extra_params,
+        )
+
+        return json.dumps({"OperationId": operation.id})
+
+    def delete_domain(self) -> str:
+        domain_name = self._get_param("DomainName")
+        operation = self.route53domains_backend.delete_domain(domain_name)
+
+        return json.dumps({"OperationId": operation.id})
+
+    def get_domain_detail(self) -> str:
+        domain_name = self._get_param("DomainName")
+
+        return json.dumps(
+            self.route53domains_backend.get_domain(domain_name=domain_name).to_json()
+        )
+
+    def list_domains(self) -> str:
+        filter_conditions = self._get_param("FilterConditions")
+        sort_condition = self._get_param("SortCondition")
+        marker = self._get_param("Marker")
+        max_items = self._get_param("MaxItems")
+        domains, marker = self.route53domains_backend.list_domains(
+            filter_conditions=filter_conditions,
+            sort_condition=sort_condition,
+            marker=marker,
+            max_items=max_items,
+        )
+        res = {
+            "Domains": list(map(self.__map_domains_to_info, domains)),
+        }
+
+        if marker:
+            res["NextPageMarker"] = marker
+
+        return json.dumps(res)
+
+    @staticmethod
+    def __map_domains_to_info(domain: Route53Domain) -> Dict[str, Any]:  # type: ignore[misc]
+        return {
+            "DomainName": domain.domain_name,
+            "AutoRenew": domain.auto_renew,
+            "Expiry": domain.expiration_date.timestamp(),
+            "TransferLock": True,
+        }
+
+    def list_operations(self) -> str:
+        submitted_since_timestamp = self._get_int_param("SubmittedSince")
+        max_items = self._get_int_param("MaxItems")
+        statuses = self._get_param("Status")
+        marker = self._get_param("Marker")
+        types = self._get_param("Type")
+        sort_by = self._get_param("SortBy")
+        sort_order = self._get_param("SortOrder")
+
+        operations, marker = self.route53domains_backend.list_operations(
+            submitted_since_timestamp=submitted_since_timestamp,
+            max_items=max_items,
+            marker=marker,
+            statuses=statuses,
+            types=types,
+            sort_by=sort_by,
+            sort_order=sort_order,
+        )
+
+        res = {
+            "Operations": [operation.to_json() for operation in operations],
+        }
+
+        if marker:
+            res["NextPageMarker"] = marker
+
+        return json.dumps(res)
+
+    def get_operation_detail(self) -> str:
+        operation_id = self._get_param("OperationId")
+        operation: Route53DomainsOperation = self.route53domains_backend.get_operation(
+            operation_id
+        )
+
+        return json.dumps(operation.to_json())
+
+    def update_domain_nameservers(self) -> str:
+        domain_name = self._get_param("DomainName")
+        nameservers = self._get_param("Nameservers")
+
+        operation: Route53DomainsOperation = (
+            self.route53domains_backend.update_domain_nameservers(
+                domain_name=domain_name, nameservers=nameservers
+            )
+        )
+
+        return json.dumps({"OperationId": operation.id})
diff --git a/moto/route53domains/urls.py b/moto/route53domains/urls.py
new file mode 100644
index 000000000..500444d22
--- /dev/null
+++ b/moto/route53domains/urls.py
@@ -0,0 +1,11 @@
+"""route53domains base URL and path."""
+from .responses import Route53DomainsResponse
+
+url_bases = [
+    r"https?://route53domains\.(.+)\.amazonaws\.com",
+]
+
+
+url_paths = {
+    "{0}/$": Route53DomainsResponse.dispatch,
+}
diff --git a/moto/route53domains/validators.py b/moto/route53domains/validators.py
new file mode 100644
index 000000000..b5bfb0eb2
--- /dev/null
+++ b/moto/route53domains/validators.py
@@ -0,0 +1,1189 @@
+import re
+from datetime import datetime, timedelta, timezone
+from enum import Enum
+from ipaddress import IPv4Address, IPv6Address, ip_address
+from typing import Any, Dict, List, Optional, Type
+
+from moto.core.common_models import BaseModel
+from moto.moto_api._internal import mock_random
+from moto.route53domains.exceptions import UnsupportedTLDException
+
+DOMAIN_OPERATION_STATUSES = (
+    "SUBMITTED",
+    "IN_PROGRESS",
+    "ERROR",
+    "SUCCESSFUL",
+    "FAILED",
+)
+
+DOMAIN_OPERATION_TYPES = (
+    "REGISTER_DOMAIN",
+    "DELETE_DOMAIN",
+    "TRANSFER_IN_DOMAIN",
+    "UPDATE_DOMAIN_CONTACT",
+    "UPDATE_NAMESERVER",
+    "CHANGE_PRIVACY_PROTECTION",
+    "DOMAIN_LOCK",
+    "ENABLE_AUTORENEW",
+    "DISABLE_AUTORENEW",
+    "ADD_DNSSEC",
+    "REMOVE_DNSSEC",
+    "EXPIRE_DOMAIN",
+    "TRANSFER_OUT_DOMAIN",
+    "CHANGE_DOMAIN_OWNER",
+    "RENEW_DOMAIN",
+    "PUSH_DOMAIN",
+    "INTERNAL_TRANSFER_OUT_DOMAIN",
+    "INTERNAL_TRANSFER_IN_DOMAIN",
+)
+
+DOMAIN_OPERATION_STATUS_FLAGS = (
+    "PENDING_ACCEPTANCE",
+    "PENDING_CUSTOMER_ACTION",
+    "PENDING_AUTHORIZATION",
+    "PENDING_PAYMENT_VERIFICATION",
+    "PENDING_SUPPORT_CASE",
+)
+
+DOMAIN_CONTACT_DETAIL_CONTACT_TYPES = (
+    "PERSON",
+    "COMPANY",
+    "ASSOCIATION",
+    "PUBLIC_BODY",
+    "RESELLER",
+    "ORGANIZATION",
+)
+
+DOMAIN_CONTACT_DETAIL_COUNTRY_CODES = (
+    "AC",
+    "AD",
+    "AE",
+    "AF",
+    "AG",
+    "AI",
+    "AL",
+    "AM",
+    "AN",
+    "AO",
+    "AQ",
+    "AR",
+    "AS",
+    "AT",
+    "AU",
+    "AW",
+    "AX",
+    "AZ",
+    "BA",
+    "BB",
+    "BD",
+    "BE",
+    "BF",
+    "BG",
+    "BH",
+    "BI",
+    "BJ",
+    "BL",
+    "BM",
+    "BN",
+    "BO",
+    "BQ",
+    "BR",
+    "BS",
+    "BT",
+    "BV",
+    "BW",
+    "BY",
+    "BZ",
+    "CA",
+    "CC",
+    "CD",
+    "CF",
+    "CG",
+    "CH",
+    "CI",
+    "CK",
+    "CL",
+    "CM",
+    "CN",
+    "CO",
+    "CR",
+    "CU",
+    "CV",
+    "CW",
+    "CX",
+    "CY",
+    "CZ",
+    "DE",
+    "DJ",
+    "DK",
+    "DM",
+    "DO",
+    "DZ",
+    "EC",
+    "EE",
+    "EG",
+    "EH",
+    "ER",
+    "ES",
+    "ET",
+    "FI",
+    "FJ",
+    "FK",
+    "FM",
+    "FO",
+    "FR",
+    "GA",
+    "GB",
+    "GD",
+    "GE",
+    "GF",
+    "GG",
+    "GH",
+    "GI",
+    "GL",
+    "GM",
+    "GN",
+    "GP",
+    "GQ",
+    "GR",
+    "GS",
+    "GT",
+    "GU",
+    "GW",
+    "GY",
+    "HK",
+    "HM",
+    "HN",
+    "HR",
+    "HT",
+    "HU",
+    "ID",
+    "IE",
+    "IL",
+    "IM",
+    "IN",
+    "IO",
+    "IQ",
+    "IR",
+    "IS",
+    "IT",
+    "JE",
+    "JM",
+    "JO",
+    "JP",
+    "KE",
+    "KG",
+    "KH",
+    "KI",
+    "KM",
+    "KN",
+    "KP",
+    "KR",
+    "KW",
+    "KY",
+    "KZ",
+    "LA",
+    "LB",
+    "LC",
+    "LI",
+    "LK",
+    "LR",
+    "LS",
+    "LT",
+    "LU",
+    "LV",
+    "LY",
+    "MA",
+    "MC",
+    "MD",
+    "ME",
+    "MF",
+    "MG",
+    "MH",
+    "MK",
+    "ML",
+    "MM",
+    "MN",
+    "MO",
+    "MP",
+    "MQ",
+    "MR",
+    "MS",
+    "MT",
+    "MU",
+    "MV",
+    "MW",
+    "MX",
+    "MY",
+    "MZ",
+    "NA",
+    "NC",
+    "NE",
+    "NF",
+    "NG",
+    "NI",
+    "NL",
+    "NO",
+    "NP",
+    "NR",
+    "NU",
+    "NZ",
+    "OM",
+    "PA",
+    "PE",
+    "PF",
+    "PG",
+    "PH",
+    "PK",
+    "PL",
+    "PM",
+    "PN",
+    "PR",
+    "PS",
+    "PT",
+    "PW",
+    "PY",
+    "QA",
+    "RE",
+    "RO",
+    "RS",
+    "RU",
+    "RW",
+    "SA",
+    "SB",
+    "SC",
+    "SD",
+    "SE",
+    "SG",
+    "SH",
+    "SI",
+    "SJ",
+    "SK",
+    "SL",
+    "SM",
+    "SN",
+    "SO",
+    "SR",
+    "SS",
+    "ST",
+    "SV",
+    "SX",
+    "SY",
+    "SZ",
+    "TC",
+    "TD",
+    "TF",
+    "TG",
+    "TH",
+    "TJ",
+    "TK",
+    "TL",
+    "TM",
+    "TN",
+    "TO",
+    "TP",
+    "TR",
+    "TT",
+    "TV",
+    "TW",
+    "TZ",
+    "UA",
+    "UG",
+    "US",
+    "UY",
+    "UZ",
+    "VA",
+    "VC",
+    "VE",
+    "VG",
+    "VI",
+    "VN",
+    "VU",
+    "WF",
+    "WS",
+    "YE",
+    "YT",
+    "ZA",
+    "ZM",
+    "ZW",
+)
+
+# List of supported top-level domains that you can register with Amazon Route53
+AWS_SUPPORTED_TLDS = (
+    "ac",
+    "academy",
+    "accountants",
+    "actor",
+    "adult",
+    "agency",
+    "airforce",
+    "apartments",
+    "associates",
+    "auction",
+    "audio",
+    "band",
+    "bargains",
+    "bet",
+    "bike",
+    "bingo",
+    "biz",
+    "black",
+    "blue",
+    "boutique",
+    "builders",
+    "business",
+    "buzz",
+    "cab",
+    "cafe",
+    "camera",
+    "camp",
+    "capital",
+    "cards",
+    "care",
+    "careers",
+    "cash",
+    "casino",
+    "catering",
+    "cc",
+    "center",
+    "ceo",
+    "chat",
+    "cheap",
+    "church",
+    "city",
+    "claims",
+    "cleaning",
+    "click",
+    "clinic",
+    "clothing",
+    "cloud",
+    "club",
+    "coach",
+    "codes",
+    "coffee",
+    "college",
+    "com",
+    "community",
+    "company",
+    "computer",
+    "condos",
+    "construction",
+    "consulting",
+    "contractors",
+    "cool",
+    "coupons",
+    "credit",
+    "creditcard",
+    "cruises",
+    "dance",
+    "dating",
+    "deals",
+    "degree",
+    "delivery",
+    "democrat",
+    "dental",
+    "diamonds",
+    "diet",
+    "digital",
+    "direct",
+    "directory",
+    "discount",
+    "dog",
+    "domains",
+    "education",
+    "email",
+    "energy",
+    "engineering",
+    "enterprises",
+    "equipment",
+    "estate",
+    "events",
+    "exchange",
+    "expert",
+    "exposed",
+    "express",
+    "fail",
+    "farm",
+    "finance",
+    "financial",
+    "fish",
+    "fitness",
+    "flights",
+    "florist",
+    "flowers",
+    "fm",
+    "football",
+    "forsale",
+    "foundation",
+    "fund",
+    "furniture",
+    "futbol",
+    "fyi",
+    "gallery",
+    "games",
+    "gift",
+    "gifts",
+    "gives",
+    "glass",
+    "global",
+    "gmbh",
+    "gold",
+    "golf",
+    "graphics",
+    "gratis",
+    "green",
+    "gripe",
+    "group",
+    "guide",
+    "guitars",
+    "guru",
+    "haus",
+    "healthcare",
+    "help",
+    "hiv",
+    "hockey",
+    "holdings",
+    "holiday",
+    "host",
+    "hosting",
+    "house",
+    "im",
+    "immo",
+    "immobilien",
+    "industries",
+    "info",
+    "ink",
+    "institute",
+    "insure",
+    "international",
+    "investments",
+    "io",
+    "irish",
+    "jewelry",
+    "juegos",
+    "kaufen",
+    "kim",
+    "kitchen",
+    "kiwi",
+    "land",
+    "lease",
+    "legal",
+    "lgbt",
+    "life",
+    "lighting",
+    "limited",
+    "limo",
+    "link",
+    "live",
+    "loan",
+    "loans",
+    "lol",
+    "maison",
+    "management",
+    "marketing",
+    "mba",
+    "media",
+    "memorial",
+    "mobi",
+    "moda",
+    "money",
+    "mortgage",
+    "movie",
+    "name",
+    "net",
+    "network",
+    "news",
+    "ninja",
+    "onl",
+    "online",
+    "org",
+    "partners",
+    "parts",
+    "photo",
+    "photography",
+    "photos",
+    "pics",
+    "pictures",
+    "pink",
+    "pizza",
+    "place",
+    "plumbing",
+    "plus",
+    "poker",
+    "porn",
+    "press",
+    "pro",
+    "productions",
+    "properties",
+    "property",
+    "pub",
+    "qpon",
+    "recipes",
+    "red",
+    "reise",
+    "reisen",
+    "rentals",
+    "repair",
+    "report",
+    "republican",
+    "restaurant",
+    "reviews",
+    "rip",
+    "rocks",
+    "run",
+    "sale",
+    "sarl",
+    "school",
+    "schule",
+    "services",
+    "sex",
+    "sexy",
+    "shiksha",
+    "shoes",
+    "show",
+    "singles",
+    "site",
+    "soccer",
+    "social",
+    "solar",
+    "solutions",
+    "space",
+    "store",
+    "studio",
+    "style",
+    "sucks",
+    "supplies",
+    "supply",
+    "support",
+    "surgery",
+    "systems",
+    "tattoo",
+    "tax",
+    "taxi",
+    "team",
+    "tech",
+    "technology",
+    "tennis",
+    "theater",
+    "tienda",
+    "tips",
+    "tires",
+    "today",
+    "tools",
+    "tours",
+    "town",
+    "toys",
+    "trade",
+    "training",
+    "tv",
+    "university",
+    "uno",
+    "vacations",
+    "vegas",
+    "ventures",
+    "vg",
+    "viajes",
+    "video",
+    "villas",
+    "vision",
+    "voyage",
+    "watch",
+    "website",
+    "wedding",
+    "wiki",
+    "wine",
+    "works",
+    "world",
+    "wtf",
+    "xyz",
+    "zone",
+)
+
+VALID_DOMAIN_REGEX = re.compile(
+    r"(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]"
+)
+PHONE_NUMBER_REGEX = re.compile(r"\+\d*\.\d+$")
+
+
+class DomainFilterField(str, Enum):
+    DOMAIN_NAME = "DomainName"
+    EXPIRY = "Expiry"
+
+
+class DomainSortOrder(str, Enum):
+    ASCENDING = "ASC"
+    DESCENDING = "DES"
+
+
+class DomainFilterOperator(str, Enum):
+    LE = "LE"
+    GE = "GE"
+    BEGINS_WITH = "BEGINS_WITH"
+
+
+def is_valid_enum(value: Any, enum_cls: Type[Enum]) -> bool:
+    try:
+        enum_cls(value)
+        return True
+    except ValueError:
+        return False
+
+
+class ValidationException(Exception):
+    def __init__(self, errors: List[str]):
+        super().__init__("\n\t".join(errors))
+        self.errors = errors
+
+
+class Route53DomainsOperation(BaseModel):
+    def __init__(
+        self,
+        id_: str,
+        domain_name: str,
+        status: str,
+        type_: str,
+        submitted_date: datetime,
+        last_updated_date: datetime,
+        message: Optional[str] = None,
+        status_flag: Optional[str] = None,
+    ):
+        self.id = id_
+        self.domain_name = domain_name
+        self.status = status
+        self.type = type_
+        self.submitted_date = submitted_date
+        self.last_updated_date = last_updated_date
+        self.message = message
+        self.status_flag = status_flag
+
+    @classmethod
+    def validate(  # type: ignore[misc,no-untyped-def]
+        cls,
+        domain_name: str,
+        status: str,
+        type_: str,
+        message: Optional[str] = None,
+        status_flag: Optional[str] = None,
+    ):
+
+        id_ = str(mock_random.uuid4())
+        submitted_date = datetime.now(timezone.utc)
+        last_updated_date = datetime.now(timezone.utc)
+
+        return cls(
+            id_,
+            domain_name,
+            status,
+            type_,
+            submitted_date,
+            last_updated_date,
+            message,
+            status_flag,
+        )
+
+    def to_json(self) -> Dict[str, Any]:
+        d = {
+            "OperationId": self.id,
+            "Status": self.status,
+            "StatusFlag": self.status_flag,
+            "DomainName": self.domain_name,
+            "LastUpdatedDate": self.last_updated_date.timestamp(),
+            "SubmittedDate": self.submitted_date.timestamp(),
+            "Type": self.type,
+        }
+
+        if self.message:
+            d["Message"] = self.message
+
+        return d
+
+
+class Route53DomainsContactDetail(BaseModel):
+    def __init__(
+        self,
+        address_line_1: Optional[str] = None,
+        address_line_2: Optional[str] = None,
+        city: Optional[str] = None,
+        contact_type: Optional[str] = None,
+        country_code: Optional[str] = None,
+        email: Optional[str] = None,
+        extra_params: Optional[List[Dict[str, Any]]] = None,
+        fax: Optional[str] = None,
+        first_name: Optional[str] = None,
+        last_name: Optional[str] = None,
+        organization_name: Optional[str] = None,
+        phone_number: Optional[str] = None,
+        state: Optional[str] = None,
+        zip_code: Optional[str] = None,
+    ):
+        super().__init__()
+        self.address_line_1 = address_line_1
+        self.address_line_2 = address_line_2
+        self.city = city
+        self.contact_type = contact_type
+        self.country_code = country_code
+        self.email = email
+        self.extra_params = extra_params
+        self.fax = fax
+        self.first_name = first_name
+        self.last_name = last_name
+        self.organization_name = organization_name
+        self.phone_number = phone_number
+        self.state = state
+        self.zip_code = zip_code
+
+    @classmethod
+    def validate(  # type: ignore[misc, no-untyped-def]
+        cls,
+        address_line_1: Optional[str] = None,
+        address_line_2: Optional[str] = None,
+        city: Optional[str] = None,
+        contact_type: Optional[str] = None,
+        country_code: Optional[str] = None,
+        email: Optional[str] = None,
+        extra_params: Optional[List[Dict[str, Any]]] = None,
+        fax: Optional[str] = None,
+        first_name: Optional[str] = None,
+        last_name: Optional[str] = None,
+        organization_name: Optional[str] = None,
+        phone_number: Optional[str] = None,
+        state: Optional[str] = None,
+        zip_code: Optional[str] = None,
+    ):
+        input_errors: List[str] = []
+
+        cls.__validate_str_len(address_line_1, "AddressLine1", 255, input_errors)
+        cls.__validate_str_len(address_line_2, "AddressLine2", 255, input_errors)
+        cls.__validate_str_len(city, "City", 255, input_errors)
+        cls.__validate_str_len(email, "Email", 255, input_errors)
+        cls.__validate_str_len(fax, "Fax", 255, input_errors)
+        cls.__validate_str_len(first_name, "FirstName", 255, input_errors)
+        cls.__validate_str_len(last_name, "LastName", 255, input_errors)
+        cls.__validate_str_len(state, "State", 255, input_errors)
+        cls.__validate_str_len(zip_code, "ZipCode", 255, input_errors)
+
+        if contact_type:
+            if contact_type not in DOMAIN_CONTACT_DETAIL_CONTACT_TYPES:
+                input_errors.append(f"Invalid contact type {contact_type}")
+            else:
+                if contact_type != "PERSON" and not organization_name:
+                    input_errors.append(
+                        "Must supply OrganizationName when ContactType is not PERSON"
+                    )
+
+        if country_code and country_code not in DOMAIN_CONTACT_DETAIL_COUNTRY_CODES:
+            input_errors.append(f"CountryCode {country_code} is invalid")
+
+        if phone_number and not PHONE_NUMBER_REGEX.match(phone_number):
+            input_errors.append("PhoneNumber is in an invalid format")
+
+        if input_errors:
+            raise ValidationException(input_errors)
+
+        return cls(
+            address_line_1,
+            address_line_2,
+            city,
+            contact_type,
+            country_code,
+            email,
+            extra_params,
+            fax,
+            first_name,
+            last_name,
+            organization_name,
+            phone_number,
+            state,
+            zip_code,
+        )
+
+    @classmethod
+    def validate_dict(cls, d: Dict[str, Any]):  # type: ignore[misc, no-untyped-def]
+        address_line_1 = d.get("AddressLine1")
+        address_line_2 = d.get("AddressLine2")
+        city = d.get("City")
+        contact_type = d.get("ContactType")
+        country_code = d.get("CountryCode")
+        email = d.get("Email")
+        extra_params = d.get("ExtraParams")
+        fax = d.get("Fax")
+        first_name = d.get("FirstName")
+        last_name = d.get("LastName")
+        organization_name = d.get("OrganizationName")
+        phone_number = d.get("PhoneNumber")
+        state = d.get("State")
+        zip_code = d.get("ZipCode")
+        return cls.validate(
+            address_line_1=address_line_1,
+            address_line_2=address_line_2,
+            city=city,
+            contact_type=contact_type,
+            country_code=country_code,
+            email=email,
+            extra_params=extra_params,
+            fax=fax,
+            first_name=first_name,
+            last_name=last_name,
+            organization_name=organization_name,
+            phone_number=phone_number,
+            state=state,
+            zip_code=zip_code,
+        )
+
+    @staticmethod
+    def __validate_str_len(
+        value: Optional[str], field_name: str, max_len: int, input_errors: List[str]
+    ) -> None:
+        if value and len(value) > max_len:
+            input_errors.append(f"Length of {field_name} is more than {max_len}")
+
+    def to_json(self) -> Dict[str, Any]:
+        d = {
+            "FirstName": self.first_name,
+            "LastName": self.last_name,
+            "ContactType": self.contact_type,
+            "OrganizationName": self.organization_name,
+            "AddressLine1": self.address_line_1,
+            "AddressLine2": self.address_line_2,
+            "City": self.city,
+            "State": self.state,
+            "CountryCode": self.country_code,
+            "ZipCode": self.zip_code,
+            "PhoneNumber": self.phone_number,
+            "Email": self.email,
+            "Fax": self.fax,
+            "ExtraParams": self.extra_params,
+        }
+
+        return {key: value for key, value in d.items() if value is not None}
+
+
+class NameServer:
+    def __init__(self, name: str, glue_ips: List[str]):
+        self.name = name
+        self.glue_ips = glue_ips
+
+    @classmethod
+    def validate(cls, name: str, glue_ips: Optional[List[str]] = None):  # type: ignore[misc,no-untyped-def]
+        glue_ips = glue_ips or []
+        input_errors: List[str] = []
+
+        if not VALID_DOMAIN_REGEX.match(name):
+            input_errors.append(f"{name} is not a valid host name")
+
+        num_ipv4_addresses = 0
+        num_ipv6_addresses = 0
+        for ip in glue_ips:
+            try:
+                address = ip_address(ip)
+                if isinstance(address, IPv4Address):
+                    num_ipv4_addresses += 1
+                elif isinstance(address, IPv6Address):
+                    num_ipv6_addresses += 1
+            except ValueError:
+                input_errors.append(f"{ip} is not a valid IP address")
+
+        if num_ipv4_addresses > 1:
+            input_errors.append("GlueIps list must include only 1 IPv4 address")
+        if num_ipv6_addresses > 1:
+            input_errors.append("GlueIps list must include only 1 IPv6 address")
+
+        if input_errors:
+            raise ValidationException(input_errors)
+
+        return cls(name, glue_ips)
+
+    @classmethod
+    def validate_dict(cls, data: Dict[str, Any]):  # type: ignore[misc,no-untyped-def]
+        name = data.get("Name")
+        glue_ips = data.get("GlueIps")
+        return cls.validate(name, glue_ips)  # type: ignore[arg-type]
+
+    def to_json(self) -> Dict[str, Any]:
+        d: Dict[str, Any] = {"Name": self.name}
+        if self.glue_ips:
+            d["GlueIps"] = self.glue_ips
+        return d
+
+
+class Route53Domain(BaseModel):
+    def __init__(
+        self,
+        domain_name: str,
+        nameservers: List[NameServer],
+        auto_renew: bool,
+        admin_contact: Route53DomainsContactDetail,
+        registrant_contact: Route53DomainsContactDetail,
+        tech_contact: Route53DomainsContactDetail,
+        admin_privacy: bool,
+        registrant_privacy: bool,
+        tech_privacy: bool,
+        registrar_name: str,
+        whois_server: str,
+        registrar_url: str,
+        abuse_contact_email: str,
+        abuse_contact_phone: str,
+        registry_domain_id: str,
+        creation_date: datetime,
+        updated_date: datetime,
+        expiration_date: datetime,
+        reseller: str,
+        status_list: List[str],
+        dns_sec_keys: List[Dict[str, Any]],
+        extra_params: List[Dict[str, Any]],
+    ):
+        self.domain_name = domain_name
+        self.nameservers = nameservers
+        self.auto_renew = auto_renew
+        self.admin_contact = admin_contact
+        self.registrant_contact = registrant_contact
+        self.tech_contact = tech_contact
+        self.admin_privacy = admin_privacy
+        self.registrant_privacy = registrant_privacy
+        self.tech_privacy = tech_privacy
+        self.registrar_name = registrar_name
+        self.whois_server = whois_server
+        self.registrar_url = registrar_url
+        self.abuse_contact_email = abuse_contact_email
+        self.abuse_contact_phone = abuse_contact_phone
+        self.registry_domain_id = registry_domain_id
+        self.creation_date = creation_date
+        self.updated_date = updated_date
+        self.expiration_date = expiration_date
+        self.reseller = reseller
+        self.status_list = status_list
+        self.dns_sec_keys = dns_sec_keys
+        self.extra_params = extra_params
+
+    @classmethod
+    def validate(  # type: ignore[misc,no-untyped-def]
+        cls,
+        domain_name: str,
+        admin_contact: Route53DomainsContactDetail,
+        registrant_contact: Route53DomainsContactDetail,
+        tech_contact: Route53DomainsContactDetail,
+        nameservers: Optional[List[Dict[str, Any]]] = None,  # type: ignore[type-arg]
+        auto_renew: bool = True,
+        admin_privacy: bool = True,
+        registrant_privacy: bool = True,
+        tech_privacy: bool = True,
+        registrar_name: Optional[str] = None,
+        whois_server: Optional[str] = None,
+        registrar_url: Optional[str] = None,
+        abuse_contact_email: Optional[str] = None,
+        abuse_contact_phone: Optional[str] = None,
+        registry_domain_id: Optional[str] = None,
+        expiration_date: Optional[datetime] = None,
+        reseller: Optional[str] = None,
+        dns_sec_keys: Optional[List[Dict[str, Any]]] = None,
+        extra_params: Optional[List[Dict[str, Any]]] = None,
+    ):
+        input_errors: List[str] = []
+
+        cls.validate_domain_name(domain_name, input_errors)
+
+        nameservers = nameservers or []
+        try:
+            nameservers = [
+                NameServer.validate_dict(nameserver) for nameserver in nameservers
+            ] or [
+                NameServer.validate(name="ns-2048.awscdn-64.net"),
+                NameServer.validate(name="ns-2051.awscdn-67.net"),
+                NameServer.validate(name="ns-2050.awscdn-66.net"),
+                NameServer.validate(name="ns-2049.awscdn-65.net"),
+            ]
+        except ValidationException as e:
+            input_errors += e.errors
+
+        creation_date = datetime.now(timezone.utc)
+        updated_date = datetime.now(timezone.utc)
+        expiration_date = expiration_date or datetime.now(timezone.utc) + timedelta(
+            days=365 * 10
+        )
+        registrar_name = registrar_name or "GANDI SAS"
+        whois_server = whois_server or "whois.gandi.net"
+        registrar_url = registrar_url or "http://www.gandi.net"
+        abuse_contact_email = abuse_contact_email or "abuse@support.gandi.net"
+        status_list = ["SUCCEEDED"]
+
+        time_until_expiration = expiration_date - datetime.now(timezone.utc)
+        if time_until_expiration < timedelta(
+            days=365
+        ) or time_until_expiration > timedelta(days=365 * 10):
+            input_errors.append(
+                "ExpirationDate must by between 1 and 10 years from now"
+            )
+
+        if input_errors:
+            raise ValidationException(input_errors)
+
+        return cls(
+            domain_name=domain_name,
+            nameservers=nameservers,  # type: ignore[arg-type]
+            auto_renew=auto_renew,  # type: ignore[arg-type]
+            admin_contact=admin_contact,
+            registrant_contact=registrant_contact,
+            tech_contact=tech_contact,
+            admin_privacy=admin_privacy,
+            registrant_privacy=registrant_privacy,
+            tech_privacy=tech_privacy,
+            registrar_name=registrar_name,
+            whois_server=whois_server,
+            registrar_url=registrar_url,
+            abuse_contact_email=abuse_contact_email,  # type: ignore[arg-type]
+            abuse_contact_phone=abuse_contact_phone,  # type: ignore[arg-type]
+            registry_domain_id=registry_domain_id,  # type: ignore[arg-type]
+            creation_date=creation_date,  # type: ignore[arg-type]
+            updated_date=updated_date,
+            expiration_date=expiration_date,
+            reseller=reseller,  # type: ignore[arg-type]
+            status_list=status_list,  # type: ignore[arg-type]
+            dns_sec_keys=dns_sec_keys,  # type: ignore[arg-type]
+            extra_params=extra_params,  # type: ignore[arg-type]
+        )
+
+    @staticmethod
+    def validate_domain_name(domain_name: str, input_errors: List[str]) -> None:
+        if not VALID_DOMAIN_REGEX.match(domain_name):
+            input_errors.append("Invalid domain name")
+            return
+
+        tld = domain_name.split(".")[-1]
+        if tld not in AWS_SUPPORTED_TLDS:
+            raise UnsupportedTLDException(tld)
+
+    def to_json(self) -> Dict[str, Any]:
+        return {
+            "DomainName": self.domain_name,
+            "Nameservers": [nameserver.to_json() for nameserver in self.nameservers],
+            "AutoRenew": self.auto_renew,
+            "AdminContact": self.admin_contact.to_json(),
+            "RegistrantContact": self.registrant_contact.to_json(),
+            "TechContact": self.tech_contact.to_json(),
+            "AdminPrivacy": self.admin_privacy,
+            "RegistrantPrivacy": self.registrant_privacy,
+            "TechPrivacy": self.tech_privacy,
+            "RegistrarName": self.registrar_name,
+            "WhoIsServer": self.whois_server,
+            "RegistrarUrl": self.registrar_url,
+            "AbuseContactEmail": self.abuse_contact_email,
+            "AbuseContactPhone": self.abuse_contact_phone,
+            "RegistryDomainId": "",
+            "CreationDate": self.creation_date.timestamp(),
+            "UpdateDate": self.updated_date.timestamp(),
+            "ExpirationDate": self.expiration_date.timestamp(),
+            "Reseller": self.reseller,
+            "DnsSec": "",
+            "StatusList": self.status_list,
+            "DnsSecKeys": self.dns_sec_keys,
+            "BillingContact": self.admin_contact.to_json(),
+        }
+
+
+class DomainsFilter:
+    def __init__(
+        self, name: DomainFilterField, operator: DomainFilterOperator, values: List[str]
+    ):
+        self.name: DomainFilterField = name
+        self.operator: DomainFilterOperator = operator
+        self.values = values
+
+    def filter(self, domain: Route53Domain) -> bool:
+        if self.name == DomainFilterField.DOMAIN_NAME:
+            return self.__filter_by_domain_name(domain)
+        return self.__filter_by_expiry_date(domain)
+
+    def __filter_by_domain_name(self, domain: Route53Domain) -> bool:
+        return any([domain.domain_name.startswith(value) for value in self.values])
+
+    def __filter_by_expiry_date(self, domain: Route53Domain) -> bool:
+        return (
+            any(
+                [
+                    value
+                    for value in self.values
+                    if domain.expiration_date
+                    >= datetime.fromtimestamp(float(value), tz=timezone.utc)
+                ]
+            )
+            if self.operator == DomainFilterOperator.GE
+            else any(
+                [
+                    value
+                    for value in self.values
+                    if domain.expiration_date
+                    <= datetime.fromtimestamp(float(value), tz=timezone.utc)
+                ]
+            )
+        )
+
+    @classmethod
+    def validate(cls, name: str, operator: str, values: List[str]):  # type: ignore[misc, no-untyped-def]
+        input_errors: List[str] = []
+
+        if not is_valid_enum(name, DomainFilterField):
+            input_errors.append(f"Cannot filter by field {name}")
+
+        if not is_valid_enum(operator, DomainFilterOperator):
+            input_errors.append(f"Invalid filter operator {operator}")
+
+        if len(values) != 1:
+            input_errors.append("Multiple filter values are not currently supported")
+
+        if (
+            name == DomainFilterField.DOMAIN_NAME
+            and operator != DomainFilterOperator.BEGINS_WITH
+        ):
+            input_errors.append(
+                f"Operator {operator} cannot be used with the DomainName filter"
+            )
+
+        if name == DomainFilterField.EXPIRY and operator not in (
+            DomainFilterOperator.GE,
+            DomainFilterOperator.LE,
+        ):
+            input_errors.append(
+                f"Operator {operator} cannot be used with the Expiry filter"
+            )
+
+        if input_errors:
+            raise ValidationException(input_errors)
+
+        return cls(
+            name=DomainFilterField(name),
+            operator=DomainFilterOperator(operator),
+            values=values,
+        )
+
+    @classmethod
+    def validate_dict(cls, data: Dict[str, Any]):  # type: ignore[misc,no-untyped-def]
+        name = data.get("Name")
+        operator = data.get("Operator")
+        values = data.get("Values")
+        return cls.validate(name=name, operator=operator, values=values)  # type: ignore[arg-type]
+
+
+class DomainsSortCondition:
+    def __init__(self, name: DomainFilterField, sort_order: DomainSortOrder):
+        self.name: DomainFilterField = name
+        self.sort_order: DomainSortOrder = sort_order
+
+    @classmethod
+    def validate(cls, name: str, sort_order: str):  # type: ignore[misc,no-untyped-def]
+        input_errors: List[str] = []
+        if not is_valid_enum(name, DomainFilterField):
+            input_errors.append(f"Cannot sort by field {name}")
+
+        if not is_valid_enum(sort_order, DomainSortOrder):
+            input_errors.append(f"Invalid sort order {sort_order}")
+
+        if input_errors:
+            raise ValidationException(input_errors)
+
+        return cls(name=DomainFilterField(name), sort_order=DomainSortOrder(sort_order))
+
+    @classmethod
+    def validate_dict(cls, data: Dict[str, Any]):  # type: ignore[misc,no-untyped-def]
+        name = data.get("Name")
+        sort_order = data.get("SortOrder")
+        return cls.validate(name=name, sort_order=sort_order)  # type: ignore[arg-type]
diff --git a/moto/s3/models.py b/moto/s3/models.py
index 31df9c21d..cf5643d03 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -1298,7 +1298,7 @@ class FakeBucket(CloudFormationModel):
                 "The target bucket for logging does not exist."
             )
 
-        target_prefix = self.logging.get("TargetPrefix", None)
+        target_prefix = logging_config.get("TargetPrefix", None)
         has_policy_permissions = self._log_permissions_enabled_policy(
             target_bucket=target_bucket, target_prefix=target_prefix
         )
@@ -2599,10 +2599,17 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
         bucket = self.get_bucket(bucket_name)
 
         response_meta = {}
+        delete_key = bucket.keys.get(key_name)
 
         try:
             if not bucket.is_versioned:
                 bucket.keys.pop(key_name)
+                notifications.send_event(
+                    self.account_id,
+                    notifications.S3NotificationEvent.OBJECT_REMOVED_DELETE_EVENT,
+                    bucket,
+                    delete_key,
+                )
             else:
                 if version_id is None:
                     delete_marker = self._set_delete_marker(bucket_name, key_name)
@@ -2645,6 +2652,13 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
 
                     if not bucket.keys.getlist(key_name):
                         bucket.keys.pop(key_name)
+                        notifications.send_event(
+                            self.account_id,
+                            notifications.S3NotificationEvent.OBJECT_REMOVED_DELETE_EVENT,
+                            bucket,
+                            delete_key,
+                        )
+
             return True, response_meta
         except KeyError:
             return False, None


Test patch:
diff --git a/tests/test_athena/test_athena.py b/tests/test_athena/test_athena.py
index 0ac70daf1..0b6dad04a 100644
--- a/tests/test_athena/test_athena.py
+++ b/tests/test_athena/test_athena.py
@@ -123,11 +123,13 @@ def test_start_query_validate_workgroup():
 
 
 @mock_aws
-def test_get_query_execution():
+@pytest.mark.parametrize(
+    "location", ["s3://bucket-name/prefix/", "s3://bucket-name/prefix_wo_slash"]
+)
+def test_get_query_execution(location):
     client = boto3.client("athena", region_name="us-east-1")
 
     query = "SELECT stuff"
-    location = "s3://bucket-name/prefix/"
     database = "database"
     # Start Query
     exex_id = client.start_query_execution(
@@ -141,7 +143,11 @@ def test_get_query_execution():
     assert details["QueryExecutionId"] == exex_id
     assert details["Query"] == query
     assert details["StatementType"] == "DML"
-    assert details["ResultConfiguration"]["OutputLocation"] == location
+    result_config = details["ResultConfiguration"]
+    if location.endswith("/"):
+        assert result_config["OutputLocation"] == f"{location}{exex_id}.csv"
+    else:
+        assert result_config["OutputLocation"] == f"{location}/{exex_id}.csv"
     assert details["QueryExecutionContext"]["Database"] == database
     assert details["Status"]["State"] == "SUCCEEDED"
     assert details["Statistics"] == {
diff --git a/tests/test_cloudfront/test_cloudfront.py b/tests/test_cloudfront/test_cloudfront.py
index 178ea583d..872a8769f 100644
--- a/tests/test_cloudfront/test_cloudfront.py
+++ b/tests/test_cloudfront/test_cloudfront.py
@@ -248,3 +248,34 @@ def test_update_default_root_object():
 
     dist_config = client.get_distribution_config(Id=dist_id)
     assert dist_config["DistributionConfig"]["DefaultRootObject"] == "index.html"
+
+
+@mock_aws
+def test_update_distribution_applies_changes():
+    client = boto3.client("cloudfront", region_name="us-east-1")
+
+    # Create standard distribution
+    config = scaffold.example_distribution_config(ref="ref")
+    dist = client.create_distribution(DistributionConfig=config)
+    dist_id = dist["Distribution"]["Id"]
+
+    # Assert that default 'Enabled' value is false
+    actual_dist_config = client.get_distribution(Id=dist_id)["Distribution"][
+        "DistributionConfig"
+    ]
+    assert not actual_dist_config["Enabled"]
+
+    # Update 'Enabled' value to true
+    get_config_response = client.get_distribution_config(Id=dist_id)
+    actual_dist_config = get_config_response["DistributionConfig"]
+    etag = get_config_response["ETag"]
+    actual_dist_config["Enabled"] = True
+    client.update_distribution(
+        DistributionConfig=actual_dist_config, Id=dist_id, IfMatch=etag
+    )
+
+    # Assert that 'Enabled' value is true
+    actual_dist_config = client.get_distribution(Id=dist_id)["Distribution"][
+        "DistributionConfig"
+    ]
+    assert actual_dist_config["Enabled"]
diff --git a/tests/test_ec2/test_security_groups.py b/tests/test_ec2/test_security_groups.py
index d826c1578..3486c395c 100644
--- a/tests/test_ec2/test_security_groups.py
+++ b/tests/test_ec2/test_security_groups.py
@@ -13,11 +13,13 @@ from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID
 from moto.ec2 import ec2_backends
 
+REGION = "us-east-1"
+
 
 @mock_aws
 def test_create_and_describe_security_group():
-    ec2 = boto3.resource("ec2", "us-west-1")
-    client = boto3.client("ec2", "us-west-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
 
     with pytest.raises(ClientError) as ex:
         client.create_security_group(GroupName="test", Description="test", DryRun=True)
@@ -51,7 +53,7 @@ def test_create_and_describe_security_group():
 
 @mock_aws
 def test_create_security_group_without_description_raises_error():
-    ec2 = boto3.resource("ec2", "us-west-1")
+    ec2 = boto3.resource("ec2", REGION)
 
     with pytest.raises(ClientError) as ex:
         ec2.create_security_group(GroupName="test security group", Description="")
@@ -62,15 +64,15 @@ def test_create_security_group_without_description_raises_error():
 
 @mock_aws
 def test_default_security_group():
-    client = boto3.client("ec2", "us-west-1")
+    client = boto3.client("ec2", REGION)
     groups = retrieve_all_sgs(client)
     assert "default" in [g["GroupName"] for g in groups]
 
 
 @mock_aws
 def test_create_and_describe_vpc_security_group():
-    ec2 = boto3.resource("ec2", "us-west-1")
-    client = boto3.client("ec2", "us-west-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
 
     name = str(uuid4())
     vpc_id = f"vpc-{str(uuid4())[0:6]}"
@@ -111,8 +113,8 @@ def test_create_and_describe_vpc_security_group():
 
 @mock_aws
 def test_create_two_security_groups_with_same_name_in_different_vpc():
-    ec2 = boto3.resource("ec2", "us-east-1")
-    client = boto3.client("ec2", "us-east-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
 
     name = str(uuid4())
     vpc_id = "vpc-5300000c"
@@ -132,7 +134,7 @@ def test_create_two_security_groups_with_same_name_in_different_vpc():
 
 @mock_aws
 def test_create_two_security_groups_in_vpc_with_ipv6_enabled():
-    ec2 = boto3.resource("ec2", region_name="us-west-1")
+    ec2 = boto3.resource("ec2", region_name=REGION)
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16", AmazonProvidedIpv6CidrBlock=True)
 
     security_group = ec2.create_security_group(
@@ -145,8 +147,8 @@ def test_create_two_security_groups_in_vpc_with_ipv6_enabled():
 
 @mock_aws
 def test_deleting_security_groups():
-    ec2 = boto3.resource("ec2", "us-west-1")
-    client = boto3.client("ec2", "us-west-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
     sg_name1 = str(uuid4())
     sg_name2 = str(uuid4())
     group1 = ec2.create_security_group(GroupName=sg_name1, Description="test desc 1")
@@ -188,8 +190,8 @@ def test_deleting_security_groups():
 
 @mock_aws
 def test_delete_security_group_in_vpc():
-    ec2 = boto3.resource("ec2", "us-west-1")
-    client = boto3.client("ec2", "us-west-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
 
     group = ec2.create_security_group(
         GroupName=str(uuid4()), Description="test1", VpcId="vpc-12345"
@@ -207,8 +209,8 @@ def test_delete_security_group_in_vpc():
 
 @mock_aws
 def test_authorize_ip_range_and_revoke():
-    ec2 = boto3.resource("ec2", "us-west-1")
-    client = boto3.client("ec2", "us-west-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
     security_group = ec2.create_security_group(
         GroupName=str(uuid4()), Description="test"
     )
@@ -332,8 +334,8 @@ def test_authorize_ip_range_and_revoke():
 
 @mock_aws
 def test_authorize_other_group_and_revoke():
-    ec2 = boto3.resource("ec2", "us-west-1")
-    client = boto3.client("ec2", "us-west-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
     sg_name = str(uuid4())
     security_group = ec2.create_security_group(
         GroupName=sg_name, Description="test desc"
@@ -389,7 +391,7 @@ def test_authorize_other_group_and_revoke():
 
 @mock_aws
 def test_authorize_other_group_egress_and_revoke():
-    ec2 = boto3.resource("ec2", region_name="us-west-1")
+    ec2 = boto3.resource("ec2", region_name=REGION)
 
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
 
@@ -476,8 +478,8 @@ def test_authorize_group_in_vpc():
 
 @mock_aws
 def test_describe_security_groups():
-    ec2 = boto3.resource("ec2", "us-west-1")
-    client = boto3.client("ec2", "us-west-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
     vpc_id = f"vpc-{str(uuid4())[0:6]}"
     name_1 = str(uuid4())
     desc_1 = str(uuid4())
@@ -514,7 +516,7 @@ def test_describe_security_groups():
 
 @mock_aws
 def test_authorize_bad_cidr_throws_invalid_parameter_value():
-    ec2 = boto3.resource("ec2", "us-west-1")
+    ec2 = boto3.resource("ec2", REGION)
     sec_group = ec2.create_security_group(GroupName=str(uuid4()), Description="test")
     with pytest.raises(ClientError) as ex:
         permissions = [
@@ -533,8 +535,8 @@ def test_authorize_bad_cidr_throws_invalid_parameter_value():
 
 @mock_aws
 def test_security_group_tag_filtering():
-    ec2 = boto3.resource("ec2", region_name="us-east-1")
-    client = boto3.client("ec2", region_name="us-east-1")
+    ec2 = boto3.resource("ec2", region_name=REGION)
+    client = boto3.client("ec2", region_name=REGION)
     sg = ec2.create_security_group(GroupName=str(uuid4()), Description="Test SG")
     tag_name = str(uuid4())[0:6]
     tag_val = str(uuid4())
@@ -553,8 +555,8 @@ def test_security_group_tag_filtering():
 
 @mock_aws
 def test_authorize_all_protocols_with_no_port_specification():
-    ec2 = boto3.resource("ec2", region_name="us-east-1")
-    client = boto3.client("ec2", region_name="us-east-1")
+    ec2 = boto3.resource("ec2", region_name=REGION)
+    client = boto3.client("ec2", region_name=REGION)
     sg_name = str(uuid4())
     sg = ec2.create_security_group(GroupName=sg_name, Description="test desc")
 
@@ -570,9 +572,9 @@ def test_authorize_all_protocols_with_no_port_specification():
 
 
 @mock_aws
-def test_security_group_rule_tagging():
-    ec2 = boto3.resource("ec2", "us-east-1")
-    client = boto3.client("ec2", "us-east-1")
+def test_security_group_rule_filtering_group_id():
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
 
     sg_name = str(uuid4())
@@ -598,10 +600,55 @@ def test_security_group_rule_tagging():
     assert response["SecurityGroupRules"][0]["Tags"][0]["Value"] == tag_val
 
 
+@mock_aws
+def test_security_group_rule_filtering_tags():
+    # Setup
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
+    vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
+    tags = [
+        {"Key": "Automation", "Value": "Lambda"},
+        {"Key": "Partner", "Value": "test"},
+    ]
+
+    sg_name = str(uuid4())
+    sg = client.create_security_group(
+        Description="Test SG",
+        GroupName=sg_name,
+        VpcId=vpc.id,
+    )
+
+    # Execute
+    response1 = client.authorize_security_group_ingress(
+        GroupId=sg["GroupId"],
+        IpPermissions=[
+            {
+                "IpProtocol": "tcp",
+                "FromPort": 80,
+                "ToPort": 80,
+                "IpRanges": [
+                    {"CidrIp": "1.2.3.4/32", "Description": "Test description"}
+                ],
+            }
+        ],
+        TagSpecifications=[{"ResourceType": "security-group-rule", "Tags": tags}],
+    )
+
+    response2 = client.describe_security_group_rules(
+        Filters=[{"Name": "tag:Partner", "Values": ["test"]}]
+    )
+
+    # Verify
+    assert response1["SecurityGroupRules"][0]["Tags"] == tags
+    assert "Tags" in response2["SecurityGroupRules"][0]
+    assert response2["SecurityGroupRules"][0]["Tags"][1]["Key"] == "Partner"
+    assert response2["SecurityGroupRules"][0]["Tags"][1]["Value"] == "test"
+
+
 @mock_aws
 def test_create_and_describe_security_grp_rule():
-    ec2 = boto3.resource("ec2", "us-east-1")
-    client = boto3.client("ec2", "us-east-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
 
     sg_name = str(uuid4())
@@ -627,8 +674,8 @@ def test_create_and_describe_security_grp_rule():
 @mock_aws
 @pytest.mark.parametrize("use_vpc", [True, False], ids=["Use VPC", "Without VPC"])
 def test_sec_group_rule_limit(use_vpc):
-    ec2 = boto3.resource("ec2", region_name="us-west-1")
-    client = boto3.client("ec2", region_name="us-west-1")
+    ec2 = boto3.resource("ec2", region_name=REGION)
+    client = boto3.client("ec2", region_name=REGION)
 
     limit = 60
     if use_vpc:
@@ -728,7 +775,7 @@ def test_sec_group_rule_limit(use_vpc):
 
 @mock_aws
 def test_add_same_rule_twice_throws_error():
-    ec2 = boto3.resource("ec2", region_name="us-west-1")
+    ec2 = boto3.resource("ec2", region_name=REGION)
 
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
     sg = ec2.create_security_group(
@@ -768,8 +815,8 @@ def test_add_same_rule_twice_throws_error():
 
 @mock_aws
 def test_description_in_ip_permissions():
-    ec2 = boto3.resource("ec2", region_name="us-west-1")
-    conn = boto3.client("ec2", region_name="us-east-1")
+    ec2 = boto3.resource("ec2", region_name=REGION)
+    conn = boto3.client("ec2", region_name=REGION)
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
     sg = conn.create_security_group(
         GroupName="sg1", Description="Test security group sg1", VpcId=vpc.id
@@ -818,7 +865,7 @@ def test_description_in_ip_permissions():
 
 @mock_aws
 def test_security_group_tagging():
-    conn = boto3.client("ec2", region_name="us-east-1")
+    conn = boto3.client("ec2", region_name=REGION)
 
     sg = conn.create_security_group(GroupName=str(uuid4()), Description="Test SG")
 
@@ -849,7 +896,7 @@ def test_security_group_tagging():
 
 @mock_aws
 def test_security_group_wildcard_tag_filter():
-    conn = boto3.client("ec2", region_name="us-east-1")
+    conn = boto3.client("ec2", region_name=REGION)
     sg = conn.create_security_group(GroupName=str(uuid4()), Description="Test SG")
 
     rand_name = str(uuid4())[0:6]
@@ -868,10 +915,10 @@ def test_security_group_wildcard_tag_filter():
 
 @mock_aws
 def test_security_group_filter_ip_permission():
-    ec2 = boto3.resource("ec2", region_name="us-east-1")
+    ec2 = boto3.resource("ec2", region_name=REGION)
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
 
-    conn = boto3.client("ec2", region_name="us-east-1")
+    conn = boto3.client("ec2", region_name=REGION)
     sg_name = str(uuid4())[0:6]
     sg = ec2.create_security_group(
         GroupName=sg_name, Description="Test SG", VpcId=vpc.id
@@ -910,7 +957,7 @@ def retrieve_all_sgs(conn, filters=[]):  # pylint: disable=W0102
 
 @mock_aws
 def test_authorize_and_revoke_in_bulk():
-    ec2 = boto3.resource("ec2", region_name="us-west-1")
+    ec2 = boto3.resource("ec2", region_name=REGION)
 
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
 
@@ -1090,8 +1137,8 @@ def test_security_group_ingress_without_multirule_after_reload():
 
 @mock_aws
 def test_get_all_security_groups_filter_with_same_vpc_id():
-    ec2 = boto3.resource("ec2", region_name="us-east-1")
-    client = boto3.client("ec2", region_name="us-east-1")
+    ec2 = boto3.resource("ec2", region_name=REGION)
+    client = boto3.client("ec2", region_name=REGION)
     vpc_id = "vpc-5300000c"
     security_group = ec2.create_security_group(
         GroupName=str(uuid4()), Description="test1", VpcId=vpc_id
@@ -1117,7 +1164,7 @@ def test_get_all_security_groups_filter_with_same_vpc_id():
 
 @mock_aws
 def test_revoke_security_group_egress():
-    ec2 = boto3.resource("ec2", "us-east-1")
+    ec2 = boto3.resource("ec2", REGION)
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
     sg = ec2.create_security_group(
         Description="Test SG", GroupName=str(uuid4()), VpcId=vpc.id
@@ -1177,8 +1224,8 @@ def test_revoke_security_group_egress__without_ipprotocol():
 
 @mock_aws
 def test_update_security_group_rule_descriptions_egress():
-    ec2 = boto3.resource("ec2", "us-east-1")
-    client = boto3.client("ec2", "us-east-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
     sg_name = str(uuid4())
     sg = ec2.create_security_group(
@@ -1214,8 +1261,8 @@ def test_update_security_group_rule_descriptions_egress():
 
 @mock_aws
 def test_update_security_group_rule_descriptions_ingress():
-    ec2 = boto3.resource("ec2", "us-east-1")
-    client = boto3.client("ec2", "us-east-1")
+    ec2 = boto3.resource("ec2", REGION)
+    client = boto3.client("ec2", REGION)
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
     sg_name = str(uuid4())
     sg = ec2.create_security_group(
@@ -1231,8 +1278,13 @@ def test_update_security_group_rule_descriptions_ingress():
             "IpRanges": [{"CidrIp": "1.2.3.4/32", "Description": "first desc"}],
         }
     ]
-    client.authorize_security_group_ingress(GroupId=sg_id, IpPermissions=ip_permissions)
-
+    client.authorize_security_group_ingress(
+        GroupId=sg_id,
+        IpPermissions=ip_permissions,
+    )
+    client.describe_security_group_rules(
+        Filters=[{"Name": "tag:Partner", "Values": ["test"]}]
+    )
     ip_ranges = client.describe_security_groups(GroupIds=[sg_id])["SecurityGroups"][0][
         "IpPermissions"
     ][0]["IpRanges"]
@@ -1260,7 +1312,7 @@ def test_update_security_group_rule_descriptions_ingress():
 
 @mock_aws
 def test_non_existent_security_group_raises_error_on_authorize():
-    client = boto3.client("ec2", "us-east-1")
+    client = boto3.client("ec2", REGION)
     non_existent_sg = "sg-123abc"
     expected_error = f"The security group '{non_existent_sg}' does not exist"
     authorize_funcs = [
@@ -1278,9 +1330,9 @@ def test_non_existent_security_group_raises_error_on_authorize():
 def test_security_group_rules_added_via_the_backend_can_be_revoked_via_the_api():
     if settings.TEST_SERVER_MODE:
         raise unittest.SkipTest("Can't test backend directly in server mode.")
-    ec2_backend = ec2_backends[DEFAULT_ACCOUNT_ID]["us-east-1"]
-    ec2_resource = boto3.resource("ec2", region_name="us-east-1")
-    ec2_client = boto3.client("ec2", region_name="us-east-1")
+    ec2_backend = ec2_backends[DEFAULT_ACCOUNT_ID][REGION]
+    ec2_resource = boto3.resource("ec2", region_name=REGION)
+    ec2_client = boto3.client("ec2", region_name=REGION)
     vpc = ec2_resource.create_vpc(CidrBlock="10.0.0.0/16")
     group_name = str(uuid4())
     sg = ec2_resource.create_security_group(
@@ -1329,7 +1381,7 @@ def test_security_group_rules_added_via_the_backend_can_be_revoked_via_the_api()
 
 @mock_aws
 def test_filter_description():
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.0.0/16")
 
     unique = str(uuid4())
@@ -1360,7 +1412,7 @@ def test_filter_ip_permission__cidr():
         raise SkipTest(
             "CIDR's might already exist due to other tests creating IP ranges"
         )
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.1.0/16")
 
     sg1 = vpc.create_security_group(
@@ -1407,7 +1459,7 @@ def test_filter_egress__ip_permission__cidr():
         raise SkipTest(
             "CIDR's might already exist due to other tests creating IP ranges"
         )
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.1.0/16")
 
     sg1 = vpc.create_security_group(
@@ -1450,7 +1502,7 @@ def test_filter_egress__ip_permission__cidr():
 
 @mock_aws
 def test_filter_egress__ip_permission__from_port():
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.1.0/16")
 
     sg1 = vpc.create_security_group(
@@ -1494,7 +1546,7 @@ def test_filter_egress__ip_permission__from_port():
 
 @mock_aws
 def test_filter_egress__ip_permission__group_id():
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.1.0/16")
 
     sg1 = vpc.create_security_group(
@@ -1547,7 +1599,7 @@ def test_filter_egress__ip_permission__group_name_create_with_id_filter_by_name(
     """
     this fails to find the group in the AWS API, so we should also fail to find it
     """
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.1.0/16")
 
     sg1 = vpc.create_security_group(
@@ -1599,7 +1651,7 @@ def test_filter_egress__ip_permission__group_name_create_with_id_filter_by_name(
 
 @mock_aws
 def test_filter_egress__ip_permission__group_name_create_with_id_filter_by_id():
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.1.0/16")
 
     sg1 = vpc.create_security_group(
@@ -1652,7 +1704,7 @@ def test_filter_egress__ip_permission__group_name_create_with_id_filter_by_id():
 
 @mock_aws
 def test_filter_egress__ip_permission__protocol():
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.1.0/16")
 
     sg1 = vpc.create_security_group(
@@ -1695,7 +1747,7 @@ def test_filter_egress__ip_permission__protocol():
 
 @mock_aws
 def test_filter_egress__ip_permission__to_port():
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.1.0/16")
 
     sg1 = vpc.create_security_group(
@@ -1739,7 +1791,7 @@ def test_filter_egress__ip_permission__to_port():
 
 @mock_aws
 def test_get_groups_by_ippermissions_group_id_filter():
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.0.0/16")
     sg1 = vpc.create_security_group(Description="test", GroupName="test-1")
     sg2 = vpc.create_security_group(Description="test", GroupName="test-2")
@@ -1772,7 +1824,7 @@ def test_get_groups_by_ippermissions_group_id_filter():
 def test_get_groups_by_ippermissions_group_id_filter_across_vpcs():
     # setup 2 VPCs, each with a single Security Group
     # where one security group authorizes the other sg (in another vpc) via GroupId
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
 
     vpc1 = ec2r.create_vpc(CidrBlock="10.250.0.0/16")
     vpc2 = ec2r.create_vpc(CidrBlock="10.251.0.0/16")
@@ -1809,7 +1861,7 @@ def test_filter_group_name():
     """
     this filter is an exact match, not a glob
     """
-    ec2r = boto3.resource("ec2", region_name="us-west-1")
+    ec2r = boto3.resource("ec2", region_name=REGION)
     vpc = ec2r.create_vpc(CidrBlock="10.250.1.0/16")
 
     uniq_sg_name_prefix = str(uuid4())[0:6]
@@ -1843,7 +1895,7 @@ def test_filter_group_name():
 
 @mock_aws
 def test_revoke_security_group_ingress():
-    ec2 = boto3.client("ec2", region_name="us-east-1")
+    ec2 = boto3.client("ec2", region_name=REGION)
 
     vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
 
diff --git a/tests/test_efs/test_efs_cloudformation.py b/tests/test_efs/test_efs_cloudformation.py
new file mode 100644
index 000000000..98b443380
--- /dev/null
+++ b/tests/test_efs/test_efs_cloudformation.py
@@ -0,0 +1,189 @@
+import json
+
+import boto3
+
+from moto import mock_aws
+
+template_fs_simple = {
+    "AWSTemplateFormatVersion": "2010-09-09",
+    "Resources": {
+        "FileSystemResource": {"Type": "AWS::EFS::FileSystem", "Properties": {}},
+    },
+}
+
+
+template_complete = {
+    "AWSTemplateFormatVersion": "2010-09-09",
+    "Resources": {
+        "MountTargetVPC": {
+            "Type": "AWS::EC2::VPC",
+            "Properties": {"CidrBlock": "172.31.0.0/16"},
+        },
+        "MountTargetSubnetOne": {
+            "Type": "AWS::EC2::Subnet",
+            "Properties": {
+                "CidrBlock": "172.31.1.0/24",
+                "VpcId": {"Ref": "MountTargetVPC"},
+                "AvailabilityZone": "us-east-1a",
+            },
+        },
+        "MountTargetSubnetTwo": {
+            "Type": "AWS::EC2::Subnet",
+            "Properties": {
+                "CidrBlock": "172.31.2.0/24",
+                "VpcId": {"Ref": "MountTargetVPC"},
+                "AvailabilityZone": "us-east-1b",
+            },
+        },
+        "MountTargetSubnetThree": {
+            "Type": "AWS::EC2::Subnet",
+            "Properties": {
+                "CidrBlock": "172.31.3.0/24",
+                "VpcId": {"Ref": "MountTargetVPC"},
+                "AvailabilityZone": "us-east-1c",
+            },
+        },
+        "FileSystemResource": {
+            "Type": "AWS::EFS::FileSystem",
+            "Properties": {
+                "PerformanceMode": "maxIO",
+                "LifecyclePolicies": [
+                    {"TransitionToIA": "AFTER_30_DAYS"},
+                    {"TransitionToPrimaryStorageClass": "AFTER_1_ACCESS"},
+                ],
+                "Encrypted": True,
+                "FileSystemTags": [{"Key": "Name", "Value": "TestFileSystem"}],
+                "FileSystemPolicy": {
+                    "Version": "2012-10-17",
+                    "Statement": [
+                        {
+                            "Effect": "Allow",
+                            "Action": ["elasticfilesystem:ClientMount"],
+                            "Principal": {
+                                "AWS": "arn:aws:iam::111122223333:role/EfsReadOnly"
+                            },
+                        }
+                    ],
+                },
+                "BackupPolicy": {"Status": "ENABLED"},
+                "KmsKeyId": {"Fn::GetAtt": ["key", "Arn"]},
+            },
+        },
+        "key": {
+            "Type": "AWS::KMS::Key",
+            "Properties": {
+                "KeyPolicy": {
+                    "Version": "2012-10-17",
+                    "Id": "key-default-1",
+                    "Statement": [
+                        {
+                            "Sid": "Allow administration of the key",
+                            "Effect": "Allow",
+                            "Principal": {
+                                "AWS": {
+                                    "Fn::Join": [
+                                        "",
+                                        [
+                                            "arn:aws:iam::",
+                                            {"Ref": "AWS::AccountId"},
+                                            ":root",
+                                        ],
+                                    ]
+                                }
+                            },
+                            "Action": ["kms:*"],
+                            "Resource": "*",
+                        }
+                    ],
+                }
+            },
+        },
+        "MountTargetResource1": {
+            "Type": "AWS::EFS::MountTarget",
+            "Properties": {
+                "FileSystemId": {"Ref": "FileSystemResource"},
+                "SubnetId": {"Ref": "MountTargetSubnetOne"},
+                "SecurityGroups": [
+                    {"Fn::GetAtt": ["MountTargetVPC", "DefaultSecurityGroup"]}
+                ],
+            },
+        },
+        "MountTargetResource2": {
+            "Type": "AWS::EFS::MountTarget",
+            "Properties": {
+                "FileSystemId": {"Ref": "FileSystemResource"},
+                "SubnetId": {"Ref": "MountTargetSubnetTwo"},
+                "SecurityGroups": [
+                    {"Fn::GetAtt": ["MountTargetVPC", "DefaultSecurityGroup"]}
+                ],
+            },
+        },
+        "MountTargetResource3": {
+            "Type": "AWS::EFS::MountTarget",
+            "Properties": {
+                "FileSystemId": {"Ref": "FileSystemResource"},
+                "SubnetId": {"Ref": "MountTargetSubnetThree"},
+                "SecurityGroups": [
+                    {"Fn::GetAtt": ["MountTargetVPC", "DefaultSecurityGroup"]}
+                ],
+            },
+        },
+        "AccessPointResource": {
+            "Type": "AWS::EFS::AccessPoint",
+            "Properties": {
+                "FileSystemId": {"Ref": "FileSystemResource"},
+                "PosixUser": {
+                    "Uid": "13234",
+                    "Gid": "1322",
+                    "SecondaryGids": ["1344", "1452"],
+                },
+                "RootDirectory": {
+                    "CreationInfo": {
+                        "OwnerGid": "708798",
+                        "OwnerUid": "7987987",
+                        "Permissions": "0755",
+                    },
+                    "Path": "/testcfn/abc",
+                },
+            },
+        },
+    },
+}
+
+
+@mock_aws
+def test_simple_template():
+    region = "us-east-1"
+    cf = boto3.client("cloudformation", region_name=region)
+    cf.create_stack(StackName="teststack", TemplateBody=json.dumps(template_fs_simple))
+
+    efs = boto3.client("efs", region)
+    fs = efs.describe_file_systems()["FileSystems"][0]
+    assert fs["PerformanceMode"] == "generalPurpose"
+    assert fs["Encrypted"] is False
+    assert fs["ThroughputMode"] == "bursting"
+
+
+@mock_aws
+def test_full_template():
+    region = "us-east-1"
+    cf = boto3.client("cloudformation", region_name=region)
+    cf.create_stack(StackName="teststack", TemplateBody=json.dumps(template_complete))
+
+    efs = boto3.client("efs", region)
+    fs = efs.describe_file_systems()["FileSystems"][0]
+    fs_id = fs["FileSystemId"]
+    assert fs["Name"] == "TestFileSystem"
+    assert fs["KmsKeyId"]
+
+    lc = efs.describe_lifecycle_configuration(FileSystemId=fs_id)["LifecyclePolicies"]
+    assert {"TransitionToIA": "AFTER_30_DAYS"} in lc
+    assert {"TransitionToPrimaryStorageClass": "AFTER_1_ACCESS"} in lc
+
+    aps = efs.describe_access_points()["AccessPoints"][0]
+    assert aps["FileSystemId"] == fs_id
+
+    cf.delete_stack(StackName="teststack")
+
+    assert efs.describe_file_systems()["FileSystems"] == []
+    assert efs.describe_access_points()["AccessPoints"] == []
diff --git a/tests/test_emr/test_emr_cloudformation.py b/tests/test_emr/test_emr_cloudformation.py
index 3bebfd22a..84bef4b25 100644
--- a/tests/test_emr/test_emr_cloudformation.py
+++ b/tests/test_emr/test_emr_cloudformation.py
@@ -1,6 +1,8 @@
 import json
 
 import boto3
+import pytest
+from botocore.exceptions import ClientError
 
 from moto import mock_aws
 from tests import EXAMPLE_AMI_ID
@@ -496,6 +498,7 @@ template_with_kerberos_attrs = {
         "securityConfiguration": {
             "Type": "AWS::EMR::SecurityConfiguration",
             "Properties": {
+                "Name": "mysecconfig",
                 "SecurityConfiguration": {
                     "AuthenticationConfiguration": {
                         "KerberosConfiguration": {
@@ -511,7 +514,7 @@ template_with_kerberos_attrs = {
                             },
                         }
                     }
-                }
+                },
             },
         },
         "emrRole": {
@@ -618,3 +621,14 @@ def test_create_cluster_with_kerberos_attrs():
         "KdcAdminPassword": "adminp2ss",
         "CrossRealmTrustPrincipalPassword": "p2ss",
     }
+
+    # Verify everything can be deleted
+    cf.delete_stack(StackName="teststack")
+
+    cl = emr.describe_cluster(ClusterId=cluster_id)["Cluster"]
+    assert cl["Status"]["State"] == "TERMINATED"
+
+    with pytest.raises(ClientError) as exc:
+        emr.describe_security_configuration(Name="mysecconfig")
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidRequestException"
diff --git a/tests/test_iam/__init__.py b/tests/test_iam/__init__.py
index 08a1c1568..466103fb0 100644
--- a/tests/test_iam/__init__.py
+++ b/tests/test_iam/__init__.py
@@ -1,1 +1,28 @@
-# This file is intentionally left blank.
+import os
+from functools import wraps
+
+from moto import mock_aws
+
+
+def iam_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_aws` context.
+    """
+
+    @wraps(func)
+    def pagination_wrapper():
+        allow_aws_request = (
+            os.environ.get("MOTO_TEST_ALLOW_AWS_REQUEST", "false").lower() == "true"
+        )
+
+        if allow_aws_request:
+            return func()
+        else:
+            with mock_aws():
+                return func()
+
+    return pagination_wrapper
diff --git a/tests/test_iam/test_iam_cloudformation.py b/tests/test_iam/test_iam_cloudformation.py
index aae856d18..2ac97ff82 100644
--- a/tests/test_iam/test_iam_cloudformation.py
+++ b/tests/test_iam/test_iam_cloudformation.py
@@ -1,4 +1,5 @@
 import json
+from uuid import uuid4
 
 import boto3
 import pytest
@@ -8,6 +9,8 @@ from botocore.exceptions import ClientError
 from moto import mock_aws
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from tests import EXAMPLE_AMI_ID
+from tests.test_iam import iam_aws_verified
+from tests.test_iam.test_iam import MOCK_STS_EC2_POLICY_DOCUMENT
 
 TEMPLATE_MINIMAL_ROLE = """
 AWSTemplateFormatVersion: 2010-09-09
@@ -1609,3 +1612,46 @@ def test_iam_roles():
         if resource["ResourceType"] == "AWS::IAM::Role"
     ]
     assert {r["PhysicalResourceId"] for r in role_resources} == set(role_names)
+
+
+template_with_instance_profile = """
+Parameters:
+    InputRole:
+        Type: String
+        Default: "test-emr-role"
+
+Resources:
+    emrEc2InstanceProfile:
+        Type: 'AWS::IAM::InstanceProfile'
+        Properties:
+            Path: /
+            Roles:
+              - !Ref InputRole
+"""
+
+
+@iam_aws_verified
+def test_delete_instance_profile_with_existing_role():
+    region = "us-east-1"
+    iam = boto3.client("iam", region_name=region)
+    iam_role_name = f"moto_{str(uuid4())[0:6]}"
+    iam.create_role(
+        RoleName=iam_role_name, AssumeRolePolicyDocument=MOCK_STS_EC2_POLICY_DOCUMENT
+    )
+
+    try:
+        cf = boto3.client("cloudformation", region_name=region)
+        cf.create_stack(
+            StackName="teststack",
+            TemplateBody=template_with_instance_profile,
+            Parameters=[{"ParameterKey": "InputRole", "ParameterValue": iam_role_name}],
+            Capabilities=["CAPABILITY_NAMED_IAM"],
+        )
+
+        # Just verify that we can delete the InstanceProfile
+        cf.delete_stack(StackName="teststack")
+
+        # The role still exists at this point
+        iam.get_role(RoleName=iam_role_name)
+    finally:
+        iam.delete_role(RoleName=iam_role_name)
diff --git a/tests/test_route53domains/__init__.py b/tests/test_route53domains/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_route53domains/test_route53domains_domain.py b/tests/test_route53domains/test_route53domains_domain.py
new file mode 100644
index 000000000..891329b32
--- /dev/null
+++ b/tests/test_route53domains/test_route53domains_domain.py
@@ -0,0 +1,562 @@
+from datetime import datetime, timedelta, timezone
+from typing import Dict, List
+
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+
+from moto import mock_aws
+
+
+@pytest.fixture(name="domain_parameters")
+def generate_domain_parameters() -> Dict:
+    return {
+        "DomainName": "domain.com",
+        "DurationInYears": 3,
+        "AutoRenew": True,
+        "AdminContact": {
+            "FirstName": "First",
+            "LastName": "Last",
+            "ContactType": "PERSON",
+            "AddressLine1": "address 1",
+            "AddressLine2": "address 2",
+            "City": "New York City",
+            "CountryCode": "US",
+            "ZipCode": "123123123",
+            "Email": "email@gmail.com",
+            "Fax": "+1.1234567890",
+        },
+        "RegistrantContact": {
+            "FirstName": "First",
+            "LastName": "Last",
+            "ContactType": "PERSON",
+            "AddressLine1": "address 1",
+            "AddressLine2": "address 2",
+            "City": "New York City",
+            "CountryCode": "US",
+            "ZipCode": "123123123",
+            "Email": "email@gmail.com",
+            "Fax": "+1.1234567890",
+        },
+        "TechContact": {
+            "FirstName": "First",
+            "LastName": "Last",
+            "ContactType": "PERSON",
+            "AddressLine1": "address 1",
+            "AddressLine2": "address 2",
+            "City": "New York City",
+            "CountryCode": "US",
+            "ZipCode": "123123123",
+            "Email": "email@gmail.com",
+            "Fax": "+1.1234567890",
+        },
+        "PrivacyProtectAdminContact": True,
+        "PrivacyProtectRegistrantContact": True,
+        "PrivacyProtectTechContact": True,
+    }
+
+
+@pytest.fixture(name="invalid_domain_parameters")
+def generate_invalid_domain_parameters(domain_parameters: Dict) -> Dict:
+    domain_parameters["DomainName"] = "a"
+    domain_parameters["DurationInYears"] = 500
+    return domain_parameters
+
+
+@mock_aws
+def test_register_domain(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    res = route53domains_client.register_domain(**domain_parameters)
+
+    operation_id = res["OperationId"]
+
+    operations = route53domains_client.list_operations(Type=["REGISTER_DOMAIN"])[
+        "Operations"
+    ]
+    for operation in operations:
+        if operation["OperationId"] == operation_id:
+            return
+
+    assert operation_id in [
+        operation["OperationId"] for operation in operations
+    ], "Could not find expected operation id returned from `register_domain` in operation list"
+
+
+@mock_aws
+def test_register_domain_creates_hosted_zone(
+    domain_parameters: Dict,
+):
+    """Test good register domain API calls."""
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53_client = boto3.client("route53", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+
+    res = route53_client.list_hosted_zones()
+    assert "domain.com" in [
+        zone["Name"] for zone in res["HostedZones"]
+    ], "`register_domain` did not create a new hosted zone with the same name"
+
+
+@mock_aws
+def test_register_domain_fails_on_invalid_input(
+    invalid_domain_parameters: Dict,
+):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53_client = boto3.client("route53", region_name="global")
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.register_domain(**invalid_domain_parameters)
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+    res = route53_client.list_hosted_zones()
+    assert len(res["HostedZones"]) == 0
+
+
+@mock_aws
+def test_register_domain_fails_on_invalid_tld(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53_client = boto3.client("route53", region_name="global")
+
+    params = domain_parameters.copy()
+    params["DomainName"] = "test.non-existing-tld"
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.register_domain(**params)
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "UnsupportedTLD"
+
+    res = route53_client.list_hosted_zones()
+    assert len(res["HostedZones"]) == 0
+
+
+@mock_aws
+def test_list_operations(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    operations = route53domains_client.list_operations()["Operations"]
+    assert len(operations) == 1
+
+    future_time = datetime.now(timezone.utc) + timedelta(minutes=1)
+    operations = route53domains_client.list_operations(
+        SubmittedSince=future_time.timestamp()
+    )["Operations"]
+    assert len(operations) == 0
+
+    operations = route53domains_client.list_operations(Status=["SUCCESSFUL"])[
+        "Operations"
+    ]
+    assert len(operations) == 1
+
+    operations = route53domains_client.list_operations(Status=["IN_PROGRESS"])[
+        "Operations"
+    ]
+    assert len(operations) == 0
+
+    operations = route53domains_client.list_operations(Type=["REGISTER_DOMAIN"])[
+        "Operations"
+    ]
+    assert len(operations) == 1
+
+    operations = route53domains_client.list_operations(Type=["DELETE_DOMAIN"])[
+        "Operations"
+    ]
+    assert len(operations) == 0
+
+
+@mock_aws
+def test_list_operations_invalid_input():
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    with pytest.raises(ClientError) as exc:
+        _ = route53domains_client.list_operations(Type=["INVALID_TYPE"])["Operations"]
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+    with pytest.raises(ClientError) as exc:
+        _ = route53domains_client.list_operations(Status=["INVALID_STATUS"])[
+            "Operations"
+        ]
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+
+@mock_aws
+def test_get_operation_detail(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    res = route53domains_client.register_domain(**domain_parameters)
+    expected_operation_id = res["OperationId"]
+    operation = route53domains_client.get_operation_detail(
+        OperationId=expected_operation_id
+    )
+    assert operation["OperationId"] == expected_operation_id
+    assert operation["Status"] == "SUCCESSFUL"
+    assert operation["Type"] == "REGISTER_DOMAIN"
+
+
+@mock_aws
+def test_get_nonexistent_operation_detail():
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.get_operation_detail(
+            OperationId="non-exiting-operation-id"
+        )
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+
+@mock_aws
+def test_duplicate_requests(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.register_domain(**domain_parameters)
+    err = exc.value.response["Error"]
+    assert err["Code"] == "DuplicateRequest"
+
+
+@mock_aws
+def test_domain_limit(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    params = domain_parameters.copy()
+    for i in range(20):
+        params["DomainName"] = f"domain-{i}.com"
+        route53domains_client.register_domain(**params)
+
+    params["DomainName"] = "domain-20.com"
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.register_domain(**params)
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "DomainLimitExceeded"
+
+
+@mock_aws
+def test_get_domain_detail(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    res = route53domains_client.get_domain_detail(
+        DomainName=domain_parameters["DomainName"]
+    )
+    assert res["DomainName"] == domain_parameters["DomainName"]
+
+
+@mock_aws
+def test_get_invalid_domain_detail(domain_parameters):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.get_domain_detail(DomainName="not-a-domain")
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.get_domain_detail(DomainName="test.non-existing-tld")
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "UnsupportedTLD"
+
+
+@mock_aws
+def test_list_domains(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    res = route53domains_client.list_domains()
+
+    assert len(res["Domains"]) == 1
+    params = domain_parameters.copy()
+    params["DomainName"] = "new-domain.com"
+    route53domains_client.register_domain(**params)
+    res = route53domains_client.list_domains()
+    assert len(res["Domains"]) == 2
+
+
+@mock_aws
+@pytest.mark.parametrize(
+    "filters,expected_domains_len",
+    [
+        (
+            [
+                {
+                    "Name": "DomainName",
+                    "Operator": "BEGINS_WITH",
+                    "Values": ["some-non-registered-domain.com"],
+                }
+            ],
+            0,  # expected_domains_len
+        ),
+        (
+            [
+                {
+                    "Name": "DomainName",
+                    "Operator": "BEGINS_WITH",
+                    "Values": ["domain.com"],
+                }
+            ],
+            1,  # expected_domains_len
+        ),
+        (
+            [
+                {
+                    "Name": "Expiry",
+                    "Operator": "GE",
+                    "Values": [
+                        str(
+                            datetime.fromisocalendar(
+                                year=2012, week=20, day=3
+                            ).timestamp()
+                        )
+                    ],
+                }
+            ],
+            1,  # expected_domains_len
+        ),
+        (
+            [
+                {
+                    "Name": "Expiry",
+                    "Operator": "GE",
+                    "Values": [
+                        str(
+                            datetime.fromisocalendar(
+                                year=2050, week=20, day=3
+                            ).timestamp()
+                        )
+                    ],
+                }
+            ],
+            0,  # expected_domains_len
+        ),
+    ],
+)
+def test_list_domains_filters(
+    domain_parameters: Dict, filters: List[Dict], expected_domains_len: int
+):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    res = route53domains_client.list_domains(FilterConditions=filters)
+    assert len(res["Domains"]) == expected_domains_len
+
+
+@mock_aws
+def test_list_domains_sort_condition(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    params = domain_parameters.copy()
+    params["DomainName"] = "adomain.com"
+    route53domains_client.register_domain(**params)
+    params["DomainName"] = "bdomain.com"
+    route53domains_client.register_domain(**params)
+    sort = {"Name": "DomainName", "SortOrder": "DES"}
+    res = route53domains_client.list_domains(SortCondition=sort)
+    domains = res["Domains"]
+    assert domains[0]["DomainName"] == "bdomain.com"
+    assert domains[1]["DomainName"] == "adomain.com"
+
+    sort = {"Name": "Expiry", "SortOrder": "ASC"}
+    res = route53domains_client.list_domains(SortCondition=sort)
+    domains = res["Domains"]
+    assert domains[0]["DomainName"] == "adomain.com"
+    assert domains[1]["DomainName"] == "bdomain.com"
+
+
+@mock_aws
+def test_list_domains_invalid_filter(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    filters = [
+        {
+            "Name": "InvalidField",
+            "Operator": "InvalidOperator",
+            "Values": ["value-1", "value-2"],  # multiple values isn't supported
+        }
+    ]
+
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.list_domains(FilterConditions=filters)
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+
+@mock_aws
+def test_list_domains_invalid_sort_condition(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    sort = {
+        "Name": "InvalidField",
+        "SortOrder": "InvalidOrder",
+    }
+
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.list_domains(SortCondition=sort)
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+
+@mock_aws
+def test_list_domains_sort_condition_not_the_same_as_filter_condition(
+    domain_parameters: Dict,
+):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    sort = {
+        "Name": "Expiry",
+        "SortOrder": "ASC",
+    }
+    filters = [
+        {
+            "Name": "DomainName",
+            "Operator": "BEGINS_WITH",
+            "Values": ["domain.com"],
+        }
+    ]
+
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.list_domains(FilterConditions=filters, SortCondition=sort)
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+
+@mock_aws
+def test_delete_domain(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    domains = route53domains_client.list_domains()["Domains"]
+    assert len(domains) == 1
+    route53domains_client.delete_domain(DomainName=domain_parameters["DomainName"])
+    domains = route53domains_client.list_domains()["Domains"]
+    assert len(domains) == 0
+    operations = route53domains_client.list_operations(Type=["DELETE_DOMAIN"])[
+        "Operations"
+    ]
+    assert len(operations) == 1
+
+
+@mock_aws
+def test_delete_invalid_domain(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    domains = route53domains_client.list_domains()["Domains"]
+    assert len(domains) == 0
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.delete_domain(DomainName=domain_parameters["DomainName"])
+
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+
+@mock_aws
+@pytest.mark.parametrize(
+    "nameservers",
+    [
+        [{"Name": "1-nameserver.net"}, {"Name": "2-nameserver.net"}],
+        [
+            {"Name": "3-nameserver.net", "GlueIps": ["1.1.1.2"]},
+            {"Name": "4-nameserver.net", "GlueIps": ["1.1.1.1"]},
+        ],
+    ],
+)
+def test_update_domain_nameservers(domain_parameters: Dict, nameservers: List[Dict]):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    operation_id = route53domains_client.update_domain_nameservers(
+        DomainName=domain_parameters["DomainName"], Nameservers=nameservers
+    )["OperationId"]
+    domain = route53domains_client.get_domain_detail(
+        DomainName=domain_parameters["DomainName"]
+    )
+    assert domain["Nameservers"] == nameservers
+    operation = route53domains_client.get_operation_detail(OperationId=operation_id)
+    assert operation["Type"] == "UPDATE_NAMESERVER"
+    assert operation["Status"] == "SUCCESSFUL"
+
+
+@mock_aws
+@pytest.mark.parametrize(
+    "nameservers",
+    [
+        [{"Name": "1-nameserver.net", "GlueIps": ["1.1.1.1", "1.1.1.2"]}],
+        [
+            {
+                "Name": "1-nameserver.net",
+                "GlueIps": [
+                    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+                    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+                ],
+            }
+        ],
+        [
+            {
+                "Name": "1-nameserver.net",
+                "GlueIps": [
+                    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+                    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+                    "1.1.1.1",
+                ],
+            }
+        ],
+        [
+            {
+                "Name": "1-nameserver.net",
+                "GlueIps": [
+                    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+                    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+                    "1.1.1.1",
+                    "1.1.1.2",
+                ],
+            }
+        ],
+        [
+            {
+                "Name": "1-nameserver.net",
+                "GlueIps": [
+                    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+                    "1.1.1.1",
+                    "not-an-ip-address",
+                ],
+            }
+        ],
+    ],
+)
+def test_update_domain_nameservers_with_multiple_glue_ips(
+    domain_parameters: Dict, nameservers: List[Dict]
+):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.update_domain_nameservers(
+            DomainName=domain_parameters["DomainName"], Nameservers=nameservers
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+
+@mock_aws
+def test_update_domain_nameservers_requires_glue_ips(domain_parameters: Dict):
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    route53domains_client.register_domain(**domain_parameters)
+    domain_name = domain_parameters["DomainName"]
+    nameservers = [{"Name": f"subdomain.{domain_name}"}]
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.update_domain_nameservers(
+            DomainName=domain_parameters["DomainName"], Nameservers=nameservers
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
+
+
+@mock_aws
+def test_update_domain_nameservers_for_nonexistent_domain():
+    route53domains_client = boto3.client("route53domains", region_name="global")
+    nameservers = [{"Name": "1-nameserver.net"}, {"Name": "2-nameserver.net"}]
+
+    with pytest.raises(ClientError) as exc:
+        route53domains_client.update_domain_nameservers(
+            DomainName="non-existent-domain.com", Nameservers=nameservers
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "InvalidInput"
diff --git a/tests/test_s3/__init__.py b/tests/test_s3/__init__.py
index 2a177eb21..e89b1490c 100644
--- a/tests/test_s3/__init__.py
+++ b/tests/test_s3/__init__.py
@@ -51,22 +51,24 @@ def s3_aws_verified(func):
         finally:
             ### CLEANUP ###
 
-            versions = client.list_object_versions(Bucket=bucket_name).get(
-                "Versions", []
-            )
-            for key in versions:
-                client.delete_object(
-                    Bucket=bucket_name, Key=key["Key"], VersionId=key.get("VersionId")
-                )
-            delete_markers = client.list_object_versions(Bucket=bucket_name).get(
-                "DeleteMarkers", []
-            )
-            for key in delete_markers:
-                client.delete_object(
-                    Bucket=bucket_name, Key=key["Key"], VersionId=key.get("VersionId")
-                )
+            empty_bucket(client, bucket_name)
             client.delete_bucket(Bucket=bucket_name)
 
         return resp
 
     return pagination_wrapper
+
+
+def empty_bucket(client, bucket_name):
+    versions = client.list_object_versions(Bucket=bucket_name).get("Versions", [])
+    for key in versions:
+        client.delete_object(
+            Bucket=bucket_name, Key=key["Key"], VersionId=key.get("VersionId")
+        )
+    delete_markers = client.list_object_versions(Bucket=bucket_name).get(
+        "DeleteMarkers", []
+    )
+    for key in delete_markers:
+        client.delete_object(
+            Bucket=bucket_name, Key=key["Key"], VersionId=key.get("VersionId")
+        )
diff --git a/tests/test_s3/test_s3_eventbridge_integration.py b/tests/test_s3/test_s3_eventbridge_integration.py
index d84235020..dda1f2c04 100644
--- a/tests/test_s3/test_s3_eventbridge_integration.py
+++ b/tests/test_s3/test_s3_eventbridge_integration.py
@@ -142,3 +142,26 @@ def test_copy_object_notification():
     assert event_message["detail"]["bucket"]["name"] == bucket_name
     assert event_message["detail"]["object"]["key"] == object_key
     assert event_message["detail"]["reason"] == "ObjectCreated"
+
+
+@mock_aws
+def test_delete_object_notification():
+    resource_names = _seteup_bucket_notification_eventbridge()
+    bucket_name = resource_names["bucket_name"]
+    s3_client = boto3.client("s3", region_name=REGION_NAME)
+
+    # Put Object
+    s3_client.put_object(Bucket=bucket_name, Key="keyname", Body="bodyofnewobject")
+
+    # Delete Object
+    s3_client.delete_object(Bucket=bucket_name, Key="keyname")
+
+    events = _get_send_events()
+    assert len(events) == 2
+    event_message = json.loads(events[1]["message"])
+    assert event_message["detail-type"] == "Object Deleted"
+    assert event_message["source"] == "aws.s3"
+    assert event_message["account"] == ACCOUNT_ID
+    assert event_message["region"] == REGION_NAME
+    assert event_message["detail"]["bucket"]["name"] == bucket_name
+    assert event_message["detail"]["reason"] == "ObjectRemoved"
diff --git a/tests/test_s3/test_s3_logging.py b/tests/test_s3/test_s3_logging.py
index 152b6fdec..a891b4a14 100644
--- a/tests/test_s3/test_s3_logging.py
+++ b/tests/test_s3/test_s3_logging.py
@@ -1,6 +1,7 @@
 import json
 from unittest import SkipTest
 from unittest.mock import patch
+from uuid import uuid4
 
 import boto3
 import pytest
@@ -11,6 +12,7 @@ from moto.core import DEFAULT_ACCOUNT_ID
 from moto.s3 import s3_backends
 from moto.s3.models import FakeBucket
 from moto.s3.responses import DEFAULT_REGION_NAME
+from tests.test_s3 import empty_bucket, s3_aws_verified
 
 
 @mock_aws
@@ -587,3 +589,76 @@ def test_bucket_policy_resource():
     assert FakeBucket._log_permissions_enabled_policy(
         target_bucket=log_bucket_obj, target_prefix="prefix"
     )
+
+
+@s3_aws_verified
+@pytest.mark.aws_verified
+def test_put_logging_w_bucket_policy_no_prefix(bucket_name=None):
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+
+    log_bucket_name = f"{uuid4()}"
+    s3_client.create_bucket(Bucket=log_bucket_name)
+    bucket_policy = {
+        "Version": "2012-10-17",
+        "Statement": [
+            {
+                "Sid": "S3ServerAccessLogsPolicy",
+                "Effect": "Allow",
+                "Principal": {"Service": "logging.s3.amazonaws.com"},
+                "Action": ["s3:PutObject"],
+                "Resource": f"arn:aws:s3:::{log_bucket_name}/*",
+            }
+        ],
+    }
+    s3_client.put_bucket_policy(
+        Bucket=log_bucket_name, Policy=json.dumps(bucket_policy)
+    )
+    s3_client.put_bucket_logging(
+        Bucket=bucket_name,
+        BucketLoggingStatus={
+            "LoggingEnabled": {"TargetBucket": log_bucket_name, "TargetPrefix": ""}
+        },
+    )
+    result = s3_client.get_bucket_logging(Bucket=bucket_name)
+    assert result["LoggingEnabled"]["TargetBucket"] == log_bucket_name
+    assert result["LoggingEnabled"]["TargetPrefix"] == ""
+
+    empty_bucket(s3_client, log_bucket_name)
+    s3_client.delete_bucket(Bucket=log_bucket_name)
+
+
+@s3_aws_verified
+@pytest.mark.aws_verified
+def test_put_logging_w_bucket_policy_w_prefix(bucket_name=None):
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+
+    log_bucket_name = f"{uuid4()}"
+    s3_client.create_bucket(Bucket=log_bucket_name)
+    prefix = "some-prefix"
+    bucket_policy = {
+        "Version": "2012-10-17",
+        "Statement": [
+            {
+                "Sid": "S3ServerAccessLogsPolicy",
+                "Effect": "Allow",
+                "Principal": {"Service": "logging.s3.amazonaws.com"},
+                "Action": ["s3:PutObject"],
+                "Resource": f"arn:aws:s3:::{log_bucket_name}/{prefix}*",
+            }
+        ],
+    }
+    s3_client.put_bucket_policy(
+        Bucket=log_bucket_name, Policy=json.dumps(bucket_policy)
+    )
+    s3_client.put_bucket_logging(
+        Bucket=bucket_name,
+        BucketLoggingStatus={
+            "LoggingEnabled": {"TargetBucket": log_bucket_name, "TargetPrefix": prefix}
+        },
+    )
+    result = s3_client.get_bucket_logging(Bucket=bucket_name)
+    assert result["LoggingEnabled"]["TargetBucket"] == log_bucket_name
+    assert result["LoggingEnabled"]["TargetPrefix"] == prefix
+
+    empty_bucket(s3_client, log_bucket_name)
+    s3_client.delete_bucket(Bucket=log_bucket_name)