Repository: getmoto/moto
Pull request #7987 (2024-08-16 08:11:02)

AppMesh
Implements 

- create_mesh
- update_mesh
- describe_mesh
- delete_mesh
- list_meshes
- list_tags_for_resource
- tag_resource


Patch:
diff --git a/moto/appmesh/__init__.py b/moto/appmesh/__init__.py
new file mode 100644
index 000000000..46f3ef537
--- /dev/null
+++ b/moto/appmesh/__init__.py
@@ -0,0 +1,1 @@
+from .models import appmesh_backends  # noqa: F401
diff --git a/moto/appmesh/exceptions.py b/moto/appmesh/exceptions.py
new file mode 100644
index 000000000..44c2164f0
--- /dev/null
+++ b/moto/appmesh/exceptions.py
@@ -0,0 +1,23 @@
+"""Exceptions raised by the appmesh service."""
+
+from moto.core.exceptions import JsonRESTError
+
+
+class MeshError(JsonRESTError):
+    code = 400
+
+
+class MeshNotFoundError(MeshError):
+    def __init__(self, mesh_name: str) -> None:
+        super().__init__(
+            "MeshNotFound",
+            f"There are no meshes with the name {mesh_name}.",
+        )
+
+
+class ResourceNotFoundError(MeshError):
+    def __init__(self, resource_arn: str) -> None:
+        super().__init__(
+            "ResourceNotFound",
+            f"There are no mesh resources with the arn {resource_arn}.",
+        )
diff --git a/moto/appmesh/models.py b/moto/appmesh/models.py
new file mode 100644
index 000000000..76cf52b3f
--- /dev/null
+++ b/moto/appmesh/models.py
@@ -0,0 +1,191 @@
+"""AppMeshBackend class with methods for supported APIs."""
+
+from dataclasses import dataclass
+from datetime import datetime
+from typing import Any, Dict, List, Literal, Optional
+from uuid import uuid4
+
+from moto.appmesh.exceptions import MeshNotFoundError, ResourceNotFoundError
+from moto.core.base_backend import BackendDict, BaseBackend
+from moto.utilities.paginator import paginate
+
+
+@dataclass
+class Metadata:
+    arn: str
+    mesh_owner: str
+    resource_owner: str
+    created_at: datetime = datetime.now()
+    last_updated_at: datetime = datetime.now()
+    uid: str = uuid4().hex
+    version: int = 1
+
+
+@dataclass
+class Spec:
+    egress_filter: Dict[Literal["type"], Optional[str]]
+    service_discovery: Dict[Literal["ip_preference"], Optional[str]]
+
+
+@dataclass
+class Mesh:
+    mesh_name: str
+    metadata: Metadata
+    spec: Spec
+    status: Dict[Literal["status"], str]
+    tags: List[Dict[str, str]]
+
+    def to_dict(self) -> Dict[str, Any]:  # type ignore[misc]
+        return {
+            "meshName": self.mesh_name,
+            "metadata": {
+                "arn": self.metadata.arn,
+                "createdAt": self.metadata.created_at.strftime("%d/%m/%Y, %H:%M:%S"),
+                "lastUpdatedAt": self.metadata.last_updated_at.strftime(
+                    "%d/%m/%Y, %H:%M:%S"
+                ),
+                "meshOwner": self.metadata.mesh_owner,
+                "resourceOwner": self.metadata.resource_owner,
+                "uid": self.metadata.uid,
+                "version": self.metadata.version,
+            },
+            "spec": {
+                "egressFilter": self.spec.egress_filter,
+                "serviceDiscovery": {
+                    "ipPreference": self.spec.service_discovery.get("ip_preference")
+                },
+            },
+            "status": self.status,
+        }
+
+
+PAGINATION_MODEL = {
+    "list_meshes": {
+        "input_token": "next_token",
+        "limit_key": "max_results",
+        "limit_default": 100,
+        "unique_attribute": "meshName",
+    },
+    "list_tags_for_resource": {
+        "input_token": "next_token",
+        "limit_key": "limit",
+        "limit_default": 100,
+        "unique_attribute": ["key", "value"],
+    },
+}
+
+
+class AppMeshBackend(BaseBackend):
+    """Implementation of AppMesh APIs."""
+
+    def __init__(self, region_name: str, account_id: str) -> None:
+        super().__init__(region_name, account_id)
+        self.meshes: Dict[str, Mesh] = dict()
+
+    def create_mesh(
+        self,
+        client_token: Optional[str],
+        mesh_name: str,
+        egress_filter_type: Optional[str],
+        ip_preference: Optional[str],
+        tags: List[Dict[str, str]],
+    ) -> Mesh:
+        from moto.sts import sts_backends
+
+        sts_backend = sts_backends[self.account_id]["global"]
+        user_id, _, _ = sts_backend.get_caller_identity(
+            self.account_id, region=self.region_name
+        )
+
+        metadata = Metadata(
+            arn=f"arn:aws:appmesh:{self.region_name}:{self.account_id}:{mesh_name}",
+            mesh_owner=user_id,
+            resource_owner=user_id,
+        )
+        spec = Spec(
+            egress_filter={"type": egress_filter_type},
+            service_discovery={"ip_preference": ip_preference},
+        )
+        mesh = Mesh(
+            mesh_name=mesh_name,
+            spec=spec,
+            status={"status": "ACTIVE"},
+            metadata=metadata,
+            tags=tags,
+        )
+        self.meshes[mesh_name] = mesh
+        return mesh
+
+    def update_mesh(
+        self,
+        client_token: Optional[str],
+        mesh_name: str,
+        egress_filter_type: Optional[str],
+        ip_preference: Optional[str],
+    ) -> Mesh:
+        if mesh_name not in self.meshes:
+            raise MeshNotFoundError(mesh_name)
+        updated = False
+        if egress_filter_type is not None:
+            self.meshes[mesh_name].spec.egress_filter["type"] = egress_filter_type
+            updated = True
+
+        if ip_preference is not None:
+            self.meshes[mesh_name].spec.service_discovery["ip_preference"] = (
+                ip_preference
+            )
+            updated = True
+
+        if updated:
+            self.meshes[mesh_name].metadata.last_updated_at = datetime.now()
+            self.meshes[mesh_name].metadata.version += 1
+        return self.meshes[mesh_name]
+
+    def describe_mesh(self, mesh_name: str, mesh_owner: Optional[str]) -> Mesh:
+        if mesh_name not in self.meshes:
+            raise MeshNotFoundError(mesh_name)
+        return self.meshes[mesh_name]
+
+    def delete_mesh(self, mesh_name: str) -> Mesh:
+        if mesh_name not in self.meshes:
+            raise MeshNotFoundError(mesh_name)
+        self.meshes[mesh_name].status["status"] = "DELETED"
+        mesh = self.meshes[mesh_name]
+        del self.meshes[mesh_name]
+        return mesh
+
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore
+    def list_meshes(self, limit: Optional[int], next_token: Optional[str]):
+        return [
+            {
+                "arn": mesh.metadata.arn,
+                "createdAt": mesh.metadata.created_at.strftime("%d/%m/%Y, %H:%M:%S"),
+                "lastUpdatedAt": mesh.metadata.last_updated_at.strftime(
+                    "%d/%m/%Y, %H:%M:%S"
+                ),
+                "meshName": mesh.mesh_name,
+                "meshOwner": mesh.metadata.mesh_owner,
+                "resourceOwner": mesh.metadata.resource_owner,
+                "version": mesh.metadata.version,
+            }
+            for mesh in self.meshes.values()
+        ]
+
+    def _get_resource_with_arn(self, resource_arn: str) -> Mesh:
+        for mesh in self.meshes.values():
+            if mesh.metadata.arn == resource_arn:
+                return mesh
+        raise ResourceNotFoundError(resource_arn)
+
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore
+    def list_tags_for_resource(self, limit: int, next_token: str, resource_arn: str):
+        return self._get_resource_with_arn(resource_arn=resource_arn).tags
+
+    def tag_resource(self, resource_arn: str, tags: List[Dict[str, str]]) -> None:
+        if len(tags) > 0:
+            mesh = self._get_resource_with_arn(resource_arn=resource_arn)
+            mesh.tags.extend(tags)
+        return
+
+
+appmesh_backends = BackendDict(AppMeshBackend, "appmesh")
diff --git a/moto/appmesh/responses.py b/moto/appmesh/responses.py
new file mode 100644
index 000000000..aa2dd06a4
--- /dev/null
+++ b/moto/appmesh/responses.py
@@ -0,0 +1,99 @@
+"""Handles incoming appmesh requests, invokes methods, returns responses."""
+
+import json
+
+from moto.core.responses import BaseResponse
+
+from .models import AppMeshBackend, appmesh_backends
+
+
+class AppMeshResponse(BaseResponse):
+    """Handler for AppMesh requests and responses."""
+
+    def __init__(self) -> None:
+        super().__init__(service_name="appmesh")
+
+    @property
+    def appmesh_backend(self) -> AppMeshBackend:
+        """Return backend instance specific for this region."""
+        return appmesh_backends[self.current_account][self.region]
+
+    def create_mesh(self) -> str:
+        params = json.loads(self.body)
+        client_token = params.get("clientToken")
+        mesh_name = params.get("meshName")
+        spec = params.get("spec") or {}
+        egress_filter_type = (spec.get("egressFilter") or {}).get("type")
+        ip_preference = (spec.get("serviceDiscovery") or {}).get("ipPreference")
+        tags = params.get("tags")
+        mesh = self.appmesh_backend.create_mesh(
+            client_token=client_token,
+            mesh_name=mesh_name,
+            egress_filter_type=egress_filter_type,
+            ip_preference=ip_preference,
+            tags=tags,
+        )
+        return json.dumps(mesh.to_dict())
+
+    def update_mesh(self) -> str:
+        params = json.loads(self.body)
+        client_token = params.get("clientToken")
+        mesh_name = self._get_param("meshName")
+        spec = params.get("spec") or {}
+        egress_filter_type = (spec.get("egressFilter") or {}).get("type")
+        ip_preference = (spec.get("serviceDiscovery") or {}).get("ipPreference")
+        mesh = self.appmesh_backend.update_mesh(
+            client_token=client_token,
+            mesh_name=mesh_name,
+            egress_filter_type=egress_filter_type,
+            ip_preference=ip_preference,
+        )
+        return json.dumps(mesh.to_dict())
+
+    def describe_mesh(self) -> str:
+        mesh_name = self._get_param(param_name="meshName", if_none="")
+        mesh_owner = self._get_param("meshOwner")
+        mesh = self.appmesh_backend.describe_mesh(
+            mesh_name=mesh_name,
+            mesh_owner=mesh_owner,
+        )
+        return json.dumps(mesh.to_dict())
+
+    def delete_mesh(self) -> str:
+        mesh_name = self._get_param("meshName")
+        mesh = self.appmesh_backend.delete_mesh(
+            mesh_name=mesh_name,
+        )
+        return json.dumps(mesh.to_dict())
+
+    def list_meshes(self) -> str:
+        params = self._get_params()
+        limit = params.get("limit")
+        next_token = params.get("nextToken")
+        meshes, next_token = self.appmesh_backend.list_meshes(
+            limit=limit,
+            next_token=next_token,
+        )
+        return json.dumps(dict(meshes=meshes, nextToken=next_token))
+
+    def list_tags_for_resource(self) -> str:
+        params = self._get_params()
+        limit = params.get("limit")
+        next_token = params.get("nextToken")
+        resource_arn = params.get("resourceArn")
+        tags, next_token = self.appmesh_backend.list_tags_for_resource(
+            limit=limit,
+            next_token=next_token,
+            resource_arn=resource_arn,
+        )
+        return json.dumps(dict(nextToken=next_token, tags=tags))
+
+    def tag_resource(self) -> str:
+        params = json.loads(self.body)
+        resource_arn = self._get_param("resourceArn")
+        tags = params.get("tags")
+        self.appmesh_backend.tag_resource(
+            resource_arn=resource_arn,
+            tags=tags,
+        )
+        return json.dumps(dict())
diff --git a/moto/appmesh/urls.py b/moto/appmesh/urls.py
new file mode 100644
index 000000000..6d21691ec
--- /dev/null
+++ b/moto/appmesh/urls.py
@@ -0,0 +1,14 @@
+"""appmesh base URL and path."""
+
+from .responses import AppMeshResponse
+
+url_bases = [
+    r"https?://appmesh\.(.+)\.amazonaws\.com",
+]
+
+url_paths = {
+    "{0}/v20190125/meshes$": AppMeshResponse.dispatch,
+    "{0}/v20190125/meshes/(?P<meshName>[^/]+)$": AppMeshResponse.dispatch,
+    "{0}/v20190125/tags$": AppMeshResponse.dispatch,
+    "{0}/v20190125/tag$": AppMeshResponse.dispatch,
+}
diff --git a/moto/backend_index.py b/moto/backend_index.py
index 0b7a0c59c..91f12fa51 100644
--- a/moto/backend_index.py
+++ b/moto/backend_index.py
@@ -15,6 +15,7 @@ backend_url_patterns = [
         "applicationautoscaling",
         re.compile("https?://application-autoscaling\\.(.+)\\.amazonaws.com"),
     ),
+    ("appmesh", re.compile("https?://appmesh\\.(.+)\\.amazonaws\\.com")),
     ("appsync", re.compile("https?://appsync\\.(.+)\\.amazonaws\\.com")),
     ("athena", re.compile("https?://athena\\.(.+)\\.amazonaws\\.com")),
     ("autoscaling", re.compile("https?://autoscaling\\.(.+)\\.amazonaws\\.com")),
diff --git a/moto/backends.py b/moto/backends.py
index d66121c36..2db397485 100644
--- a/moto/backends.py
+++ b/moto/backends.py
@@ -15,6 +15,7 @@ if TYPE_CHECKING:
     from moto.apigatewayv2.models import ApiGatewayV2Backend
     from moto.appconfig.models import AppConfigBackend
     from moto.applicationautoscaling.models import ApplicationAutoscalingBackend
+    from moto.appmesh.models import AppMeshBackend
     from moto.appsync.models import AppSyncBackend
     from moto.athena.models import AthenaBackend
     from moto.autoscaling.models import AutoScalingBackend
@@ -192,6 +193,7 @@ SERVICE_NAMES = Union[
     "Literal['apigatewayv2']",
     "Literal['appconfig']",
     "Literal['applicationautoscaling']",
+    "Literal['appmesh']",
     "Literal['appsync']",
     "Literal['athena']",
     "Literal['autoscaling']",
@@ -358,6 +360,8 @@ def get_backend(
     name: "Literal['applicationautoscaling']",
 ) -> "BackendDict[ApplicationAutoscalingBackend]": ...
 @overload
+def get_backend(name: "Literal['appmesh']") -> "BackendDict[AppMeshBackend]": ...
+@overload
 def get_backend(name: "Literal['appsync']") -> "BackendDict[AppSyncBackend]": ...
 @overload
 def get_backend(name: "Literal['athena']") -> "BackendDict[AthenaBackend]": ...
diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py
index b2e97d822..dd2e3c36e 100644
--- a/moto/dynamodb/responses.py
+++ b/moto/dynamodb/responses.py
@@ -889,12 +889,22 @@ class DynamoHandler(BaseResponse):
         name = self.body["TableName"]
         key = self.body["Key"]
         return_values = self.body.get("ReturnValues", "NONE")
-        update_expression = self.body.get("UpdateExpression", "").strip()
+        update_expression = self.body.get("UpdateExpression", None)
         attribute_updates = self.body.get("AttributeUpdates")
-        if update_expression and attribute_updates:
+        if update_expression is not None and attribute_updates:
             raise MockValidationException(
                 "Can not use both expression and non-expression parameters in the same request: Non-expression parameters: {AttributeUpdates} Expression parameters: {UpdateExpression}"
             )
+
+        if update_expression is not None:
+            update_expression = update_expression.strip()
+            if update_expression == "":
+                raise MockValidationException(
+                    "Invalid UpdateExpression: The expression can not be empty;"
+                )
+        else:
+            update_expression = ""
+
         return_values_on_condition_check_failure = self.body.get(
             "ReturnValuesOnConditionCheckFailure"
         )
diff --git a/moto/events/models.py b/moto/events/models.py
index 9b43ba579..dc54f52e8 100644
--- a/moto/events/models.py
+++ b/moto/events/models.py
@@ -38,7 +38,12 @@ from moto.utilities.utils import (
     get_partition,
 )
 
-from .utils import _BASE_EVENT_MESSAGE, PAGINATION_MODEL, EventMessageType
+from .utils import (
+    _BASE_EVENT_MESSAGE,
+    PAGINATION_MODEL,
+    EventMessageType,
+    EventTemplateParser,
+)
 
 if TYPE_CHECKING:
     from moto.secretsmanager.models import SecretsManagerBackend
@@ -110,7 +115,8 @@ class Rule(CloudFormationModel):
         self.state = "DISABLED"
 
     def delete(self, account_id: str, region_name: str) -> None:
-        event_backend = events_backends[account_id][region_name]
+        event_backend: EventsBackend = events_backends[account_id][region_name]
+        self.remove_targets([t["Id"] for t in self.targets])
         event_backend.delete_rule(name=self.name, event_bus_arn=self.event_bus_name)
 
     def put_targets(self, targets: List[Dict[str, Any]]) -> None:
@@ -128,8 +134,10 @@ class Rule(CloudFormationModel):
             if index is not None:
                 self.targets.pop(index)
 
-    def send_to_targets(self, event: EventMessageType) -> None:
-        if not self.event_pattern.matches_event(event):
+    def send_to_targets(
+        self, original_event: EventMessageType, transform_input: bool = True
+    ) -> None:
+        if not self.event_pattern.matches_event(original_event):
             return
 
         # supported targets
@@ -140,13 +148,22 @@ class Rule(CloudFormationModel):
         for target in self.targets:
             arn = parse_arn(target["Arn"])
 
+            if transform_input:
+                input_transformer = target.get("InputTransformer", {})
+                event = EventTemplateParser.parse(
+                    input_template=input_transformer.get("InputTemplate"),
+                    input_paths_map=input_transformer.get("InputPathsMap", {}),
+                    event=original_event,
+                )
+            else:
+                event = original_event.copy()  # type: ignore[assignment]
+
             if arn.service == "logs" and arn.resource_type == "log-group":
                 self._send_to_cw_log_group(arn.resource_id, event)
             elif arn.service == "events" and not arn.resource_type:
-                input_template = json.loads(target["InputTransformer"]["InputTemplate"])
-                archive_arn = parse_arn(input_template["archive-arn"])
+                archive_arn = parse_arn(event["archive-arn"])
 
-                self._send_to_events_archive(archive_arn.resource_id, event)
+                self._send_to_events_archive(archive_arn.resource_id, original_event)
             elif arn.service == "sqs":
                 group_id = target.get("SqsParameters", {}).get("MessageGroupId")
                 self._send_to_sqs_queue(arn.resource_id, event, group_id)
@@ -181,18 +198,15 @@ class Rule(CloudFormationModel):
             else:
                 raise NotImplementedError(f"Expr not defined for {type(self)}")
 
-    def _send_to_cw_log_group(self, name: str, event: EventMessageType) -> None:
+    def _send_to_cw_log_group(self, name: str, event: Dict[str, Any]) -> None:
         from moto.logs import logs_backends
 
-        event_copy = copy.deepcopy(event)
-        event_copy["time"] = iso_8601_datetime_without_milliseconds(
-            utcfromtimestamp(event_copy["time"])  # type: ignore[arg-type]
+        event["time"] = iso_8601_datetime_without_milliseconds(
+            utcfromtimestamp(event["time"])  # type: ignore[arg-type]
         )
 
         log_stream_name = str(random.uuid4())
-        log_events = [
-            {"timestamp": unix_time_millis(), "message": json.dumps(event_copy)}
-        ]
+        log_events = [{"timestamp": unix_time_millis(), "message": json.dumps(event)}]
 
         log_backend = logs_backends[self.account_id][self.region_name]
         log_backend.create_log_stream(name, log_stream_name)
@@ -214,13 +228,12 @@ class Rule(CloudFormationModel):
         return backend.destinations[destination_name]
 
     def _send_to_sqs_queue(
-        self, resource_id: str, event: EventMessageType, group_id: Optional[str] = None
+        self, resource_id: str, event: Dict[str, Any], group_id: Optional[str] = None
     ) -> None:
         from moto.sqs import sqs_backends
 
-        event_copy = copy.deepcopy(event)
-        event_copy["time"] = iso_8601_datetime_without_milliseconds(
-            utcfromtimestamp(event_copy["time"])  # type: ignore[arg-type]
+        event["time"] = iso_8601_datetime_without_milliseconds(
+            utcfromtimestamp(float(event["time"]))  # type: ignore[arg-type]
         )
 
         if group_id:
@@ -238,7 +251,7 @@ class Rule(CloudFormationModel):
 
         sqs_backends[self.account_id][self.region_name].send_message(
             queue_name=resource_id,
-            message_body=json.dumps(event_copy),
+            message_body=json.dumps(event),
             group_id=group_id,
         )
 
@@ -288,8 +301,8 @@ class Rule(CloudFormationModel):
         event_bus_arn = properties.get("EventBusName")
         tags = properties.get("Tags")
 
-        backend = events_backends[account_id][region_name]
-        return backend.put_rule(
+        backend: "EventsBackend" = events_backends[account_id][region_name]
+        rule = backend.put_rule(
             event_name,
             scheduled_expression=scheduled_expression,
             event_pattern=event_pattern,
@@ -300,6 +313,16 @@ class Rule(CloudFormationModel):
             tags=tags,
         )
 
+        targets = properties.get("Targets", [])
+        if targets:
+            backend.put_targets(
+                name=rule.name,
+                event_bus_arn=event_bus_arn,
+                targets=targets,
+            )
+
+        return rule
+
     @classmethod
     def update_from_cloudformation_json(  # type: ignore[misc]
         cls,
@@ -322,9 +345,10 @@ class Rule(CloudFormationModel):
         account_id: str,
         region_name: str,
     ) -> None:
-        event_backend = events_backends[account_id][region_name]
+        event_backend: EventsBackend = events_backends[account_id][region_name]
         properties = cloudformation_json["Properties"]
         event_bus_arn = properties.get("EventBusName")
+
         event_backend.delete_rule(resource_name, event_bus_arn)
 
     def describe(self) -> Dict[str, Any]:
@@ -378,6 +402,8 @@ class EventBus(CloudFormationModel):
 
     def delete(self, account_id: str, region_name: str) -> None:
         event_backend = events_backends[account_id][region_name]
+        for rule in self.rules.values():
+            rule.delete(account_id, region_name)
         event_backend.delete_event_bus(name=self.name)
 
     @classmethod
@@ -576,6 +602,7 @@ class Archive(CloudFormationModel):
 
         self.events: List[EventMessageType] = []
         self.event_bus_name = source_arn.split("/")[-1]
+        self.rule: Optional[Rule] = None
 
     def describe_short(self) -> Dict[str, Any]:
         return {
@@ -612,7 +639,11 @@ class Archive(CloudFormationModel):
             self.retention = retention
 
     def delete(self, account_id: str, region_name: str) -> None:
-        event_backend = events_backends[account_id][region_name]
+        event_backend: EventsBackend = events_backends[account_id][region_name]
+
+        if self.rule:
+            self.rule.delete(account_id, region_name)
+
         event_backend.archives.pop(self.name)
 
     @classmethod
@@ -756,6 +787,7 @@ class Replay(BaseModel):
                         event,
                         **{"id": str(random.uuid4()), "replay-name": self.name},  # type: ignore
                     ),
+                    transform_input=False,
                 )
 
         self.state = ReplayState.COMPLETED
@@ -1148,7 +1180,11 @@ class EventsBackend(BaseBackend):
 
     def delete_rule(self, name: str, event_bus_arn: Optional[str]) -> None:
         event_bus_name = self._normalize_event_bus_arn(event_bus_arn)
-        event_bus = self._get_event_bus(event_bus_name)
+        try:
+            event_bus = self._get_event_bus(event_bus_name)
+        except ResourceNotFoundException:
+            # If the EventBus is deleted, the Rule is also gone
+            return
         rule = event_bus.rules.get(name)
         if not rule:
             return
@@ -1350,6 +1386,7 @@ class EventsBackend(BaseBackend):
                     event_msg["region"] = self.region_name
                     event_msg["resources"] = event.get("Resources", [])
                     event_msg["detail"] = json.loads(event["Detail"])
+                    event_msg["ingestion-time"] = unix_time()
                     rule.send_to_targets(event_msg)
 
         return entries
@@ -1610,6 +1647,7 @@ class EventsBackend(BaseBackend):
             event_bus_arn=event_bus.name,
             managed_by="prod.vhs.events.aws.internal",
         )
+        archive.rule = rule
         self.put_targets(
             rule.name,
             rule.event_bus_name,
@@ -1619,13 +1657,8 @@ class EventsBackend(BaseBackend):
                     "Arn": f"arn:{get_partition(self.region_name)}:events:{self.region_name}:::",
                     "InputTransformer": {
                         "InputPathsMap": {},
-                        "InputTemplate": json.dumps(
-                            {
-                                "archive-arn": f"{archive.arn}:{archive.uuid}",
-                                "event": "<aws.events.event.json>",
-                                "ingestion-time": "<aws.events.event.ingestion-time>",
-                            }
-                        ),
+                        # Template is not valid JSON, so we can't json.dump it
+                        "InputTemplate": f'{{"archive-arn": "{archive.arn}:{archive.uuid}", "event": <aws.events.event.json>, "ingestion-time": <aws.events.event.ingestion-time>}}',
                     },
                 }
             ],
diff --git a/moto/events/utils.py b/moto/events/utils.py
index e84c68548..6c4640f29 100644
--- a/moto/events/utils.py
+++ b/moto/events/utils.py
@@ -1,4 +1,5 @@
-from typing import TYPE_CHECKING, List, TypedDict
+import json
+from typing import TYPE_CHECKING, Any, Dict, List, TypedDict
 
 if TYPE_CHECKING:
     from typing_extensions import Any, Dict, Required, Union
@@ -19,6 +20,7 @@ EventMessageType = TypedDict(
         "region": str,
         "resources": List[str],
         "detail": "Required[Dict[str, Any]]",
+        "ingestion-time": float,
     },
     total=False,
 )
@@ -58,3 +60,58 @@ _BASE_EVENT_MESSAGE: EventMessageType = {
     "resources": [],
     "detail": {},
 }
+
+
+class EventTemplateParser:
+    DEFAULT_EVENT_INPUT_TEMPLATE = '{"id": "<id>", "time": <time>, "version": "0", "detail-type": "<detail-type>", "source": "<source>", "region": "<region>", "resources": <resources>, "detail": <detail>}'
+    DEFAULT_EVENT_INPUT_PATHS_MAP = {
+        "account": "$.account",
+        "detail": "$.detail",
+        "detail-type": "$.detail-type",
+        "source": "$.source",
+        "id": "$.id",
+        "region": "$.region",
+        "resources": "$.resources",
+        "time": "$.time",
+    }
+
+    @staticmethod
+    def _stringify(result: Any) -> str:  # type: ignore[misc]
+        if isinstance(result, dict):
+            result = json.dumps(result)
+        elif isinstance(result, list):
+            result = json.dumps([EventTemplateParser._stringify(x) for x in result])
+        elif isinstance(result, (int, float)):
+            result = str(result)
+        return result
+
+    @staticmethod
+    def parse(  # type: ignore[misc]
+        input_template: str, input_paths_map: Dict[str, Any], event: EventMessageType
+    ) -> Dict[str, Any]:
+        from jsonpath_ng.ext import parse
+
+        template_to_use = (
+            input_template or EventTemplateParser.DEFAULT_EVENT_INPUT_TEMPLATE
+        )
+        input_paths_map = (
+            input_paths_map or EventTemplateParser.DEFAULT_EVENT_INPUT_PATHS_MAP
+        )
+        for input_path in input_paths_map:
+            input_expr = parse(input_paths_map[input_path])
+            matches = input_expr.find(event)
+            result = (
+                EventTemplateParser._stringify(matches[0].value) if matches else None
+            )
+            if result:
+                template_to_use = template_to_use.replace(f"<{input_path}>", result)
+
+        default_inputs_map = {
+            "aws.events.event.json": event,
+            "aws.events.event.ingestion-time": event["ingestion-time"],
+        }
+        for input_path in default_inputs_map:
+            result = EventTemplateParser._stringify(default_inputs_map[input_path])
+            template_to_use = template_to_use.replace(f"<{input_path}>", result)
+
+        return json.loads(template_to_use)
diff --git a/moto/rds/models.py b/moto/rds/models.py
index af34de951..a03630690 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -999,6 +999,13 @@ class DBInstance(CloudFormationModel, RDSBaseModel):
             if value is not None:
                 setattr(self, key, value)
 
+        cwl_exports = db_kwargs.get("cloudwatch_logs_exports_config") or {}
+        for exp in cwl_exports.get("DisableLogTypes", []):
+            self.enabled_cloudwatch_logs_exports.remove(exp)
+        self.enabled_cloudwatch_logs_exports.extend(
+            cwl_exports.get("EnableLogTypes", [])
+        )
+
     @classmethod
     def has_cfn_attr(cls, attr: str) -> bool:
         return attr in ["Endpoint.Address", "Endpoint.Port"]
@@ -1881,7 +1888,8 @@ class RDSBackend(BaseBackend):
         # remove the db subnet group as it cannot be copied
         # and is not used in the restored instance
         source_dict = db_instance.__dict__
-        del source_dict["db_subnet_group"]
+        if "db_subnet_group" in source_dict:
+            del source_dict["db_subnet_group"]
 
         new_instance_props = copy.deepcopy(source_dict)
         new_instance_props.pop("backend")
diff --git a/moto/rds/responses.py b/moto/rds/responses.py
index 632d2327e..53a7bcbf9 100644
--- a/moto/rds/responses.py
+++ b/moto/rds/responses.py
@@ -37,6 +37,9 @@ class RDSResponse(BaseResponse):
             "enable_cloudwatch_logs_exports": self._get_params().get(
                 "EnableCloudwatchLogsExports"
             ),
+            "cloudwatch_logs_exports_config": self._get_params().get(
+                "CloudwatchLogsExportConfiguration"
+            ),
             "enable_iam_database_authentication": self._get_bool_param(
                 "EnableIAMDatabaseAuthentication"
             ),
diff --git a/moto/s3/notifications.py b/moto/s3/notifications.py
index 9f935a8f2..6dcae0007 100644
--- a/moto/s3/notifications.py
+++ b/moto/s3/notifications.py
@@ -203,7 +203,7 @@ def _send_event_bridge_message(
         events_backend = events_backends[account_id][bucket.region_name]
         for event_bus in events_backend.event_buses.values():
             for rule in event_bus.rules.values():
-                rule.send_to_targets(event)
+                rule.send_to_targets(event, transform_input=False)
 
     except:  # noqa
         # This is an async action in AWS.
diff --git a/setup.cfg b/setup.cfg
index 03d5fdb5c..31a38c272 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -162,7 +162,7 @@ emr =
 emrcontainers =
 emrserverless =
 es =
-events =
+events = jsonpath_ng
 firehose =
 forecast =
 glacier =


Test patch:
diff --git a/.github/workflows/tests_real_aws.yml b/.github/workflows/tests_real_aws.yml
index 736b47a55..f3d204bbb 100644
--- a/.github/workflows/tests_real_aws.yml
+++ b/.github/workflows/tests_real_aws.yml
@@ -44,4 +44,4 @@ jobs:
       env:
         MOTO_TEST_ALLOW_AWS_REQUEST: ${{ true }}
       run: |
-        pytest -sv -n auto tests/test_applicationautoscaling/ tests/test_athena/ tests/test_cloudformation/ tests/test_dynamodb/ tests/test_ec2/ tests/test_iam/ tests/test_iot/ tests/test_lakeformation/ tests/test_logs/ tests/test_sqs/ tests/test_ses/ tests/test_s3* tests/test_stepfunctions/ tests/test_sns/ tests/test_timestreamwrite/ -m aws_verified
+        pytest -sv -n auto tests/test_applicationautoscaling/ tests/test_athena/ tests/test_cloudformation/ tests/test_dynamodb/ tests/test_ec2/ tests/test_events/ tests/test_iam/ tests/test_iot/ tests/test_lakeformation/ tests/test_logs/ tests/test_sqs/ tests/test_ses/ tests/test_s3* tests/test_stepfunctions/ tests/test_sns/ tests/test_timestreamwrite/ -m aws_verified
diff --git a/tests/test_appmesh/__init__.py b/tests/test_appmesh/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_appmesh/test_appmesh.py b/tests/test_appmesh/test_appmesh.py
new file mode 100644
index 000000000..b715ba514
--- /dev/null
+++ b/tests/test_appmesh/test_appmesh.py
@@ -0,0 +1,150 @@
+"""Unit tests for appmesh-supported APIs."""
+
+from collections import defaultdict
+from datetime import datetime
+
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+
+from moto import mock_aws
+
+# See our Development Tips on writing tests for hints on how to write good tests:
+# http://docs.getmoto.org/en/latest/docs/contributing/development_tips/tests.html
+
+
+@pytest.fixture(name="client")
+def fixture_transfer_client():
+    with mock_aws():
+        yield boto3.client("appmesh", region_name="us-east-1")
+
+
+@mock_aws
+def test_create_list_update_describe_delete_mesh(client):
+    # Create first mesh
+    connection = client.create_mesh(
+        meshName="mesh1",
+        spec={
+            "egressFilter": {"type": "DROP_ALL"},
+            "serviceDiscovery": {"ipPreference": "IPv4_ONLY"},
+        },
+        tags=[{"key": "owner", "value": "moto"}],
+    )
+    mesh = connection.get("mesh")
+    assert mesh is not None
+    assert mesh["meshName"] == "mesh1"
+    assert mesh["spec"]["egressFilter"]["type"] == "DROP_ALL"
+    assert mesh["spec"]["serviceDiscovery"]["ipPreference"] == "IPv4_ONLY"
+    assert mesh["status"]["status"] == "ACTIVE"
+    assert mesh["metadata"]["version"] == 1
+    assert isinstance(mesh["metadata"]["arn"], str)
+    assert isinstance(mesh["metadata"]["meshOwner"], str)
+    assert isinstance(mesh["metadata"]["resourceOwner"], str)
+    assert isinstance(mesh["metadata"]["createdAt"], datetime)
+    assert isinstance(mesh["metadata"]["lastUpdatedAt"], datetime)
+
+    # Create second mesh
+    connection = client.create_mesh(
+        meshName="mesh2",
+        spec={
+            "egressFilter": {"type": "ALLOW_ALL"},
+            "serviceDiscovery": {"ipPreference": "IPv4_PREFERRED"},
+        },
+        tags=[{"key": "owner", "value": "moto"}],
+    )
+    mesh = connection.get("mesh")
+    assert mesh is not None
+    assert mesh["meshName"] == "mesh2"
+    assert mesh["spec"]["egressFilter"]["type"] == "ALLOW_ALL"
+    assert mesh["spec"]["serviceDiscovery"]["ipPreference"] == "IPv4_PREFERRED"
+    assert mesh["status"]["status"] == "ACTIVE"
+    assert mesh["metadata"]["version"] == 1
+    assert isinstance(mesh["metadata"]["arn"], str)
+    assert isinstance(mesh["metadata"]["meshOwner"], str)
+    assert isinstance(mesh["metadata"]["resourceOwner"], str)
+    assert isinstance(mesh["metadata"]["createdAt"], datetime)
+    assert isinstance(mesh["metadata"]["lastUpdatedAt"], datetime)
+
+    # List all methods, expecting 2
+    connection = client.list_meshes()
+    meshes = connection.get("meshes")
+    assert meshes is not None
+    assert isinstance(meshes, list)
+    assert len(meshes) == 2
+    names_counted = defaultdict(int)
+    for mesh in meshes:
+        mesh_name = mesh.get("meshName")
+        if mesh_name:
+            names_counted[mesh_name] += 1
+    assert names_counted["mesh1"] == 1
+    assert names_counted["mesh2"] == 1
+
+    # Change spec for mesh 1
+    connection = client.update_mesh(
+        meshName="mesh1",
+        spec={
+            "egressFilter": {"type": "ALLOW_ALL"},
+            "serviceDiscovery": {"ipPreference": "IPv6_PREFERRED"},
+        },
+    )
+    mesh = connection.get("mesh")
+    assert mesh is not None
+    assert mesh["meshName"] == "mesh1"
+    assert mesh["spec"]["egressFilter"]["type"] == "ALLOW_ALL"
+    assert mesh["spec"]["serviceDiscovery"]["ipPreference"] == "IPv6_PREFERRED"
+    assert mesh["status"]["status"] == "ACTIVE"
+    assert mesh["metadata"]["version"] == 2
+
+    # Describe mesh 1, should reflect changes
+    connection = client.describe_mesh(meshName="mesh1")
+    mesh = connection.get("mesh")
+    assert mesh is not None
+    assert mesh["meshName"] == "mesh1"
+    assert mesh["spec"]["egressFilter"]["type"] == "ALLOW_ALL"
+    assert mesh["spec"]["serviceDiscovery"]["ipPreference"] == "IPv6_PREFERRED"
+    assert mesh["status"]["status"] == "ACTIVE"
+    assert mesh["metadata"]["version"] == 2
+
+    connection = client.delete_mesh(meshName="mesh2")
+    mesh = connection.get("mesh")
+    assert mesh is not None
+    assert mesh["meshName"] == "mesh2"
+    assert mesh["spec"]["egressFilter"]["type"] == "ALLOW_ALL"
+    assert mesh["spec"]["serviceDiscovery"]["ipPreference"] == "IPv4_PREFERRED"
+    assert mesh["status"]["status"] == "DELETED"
+
+    connection = client.list_meshes()
+    meshes = connection.get("meshes")
+    assert meshes is not None
+    assert isinstance(meshes, list)
+    assert len(meshes) == 1
+    assert meshes[0]["meshName"] == "mesh1"
+
+    with pytest.raises(ClientError) as e:
+        client.describe_mesh(meshName="mesh2")
+    err = e.value.response["Error"]
+    assert err["Code"] == "MeshNotFound"
+    assert err["Message"] == "There are no meshes with the name mesh2."
+
+
+@mock_aws
+def test_tag_and_list_tags_for_resource(client):
+    connection = client.create_mesh(
+        meshName="mesh1",
+        spec={
+            "egressFilter": {"type": "DROP_ALL"},
+            "serviceDiscovery": {"ipPreference": "IPv4_ONLY"},
+        },
+        tags=[{"key": "owner", "value": "moto"}],
+    )
+    mesh = connection.get("mesh")
+    assert mesh is not None
+    arn = mesh["metadata"]["arn"]
+
+    client.tag_resource(
+        resourceArn=arn, tags=[{"key": "organization", "value": "moto"}]
+    )
+    connection = client.list_tags_for_resource(resourceArn=arn)
+    tags = connection["tags"]
+    assert tags[0] == {"key": "owner", "value": "moto"}
+    assert tags[1] == {"key": "organization", "value": "moto"}
diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py
index 2be6f32f1..f8b930289 100644
--- a/tests/test_dynamodb/test_dynamodb.py
+++ b/tests/test_dynamodb/test_dynamodb.py
@@ -3864,6 +3864,31 @@ def test_error_when_providing_expression_and_nonexpression_params():
     )
 
 
+@mock_aws
+def test_error_when_providing_empty_update_expression():
+    client = boto3.client("dynamodb", "eu-central-1")
+    table_name = "testtable"
+    client.create_table(
+        TableName=table_name,
+        KeySchema=[{"AttributeName": "pkey", "KeyType": "HASH"}],
+        AttributeDefinitions=[{"AttributeName": "pkey", "AttributeType": "S"}],
+        BillingMode="PAY_PER_REQUEST",
+    )
+
+    with pytest.raises(ClientError) as ex:
+        client.update_item(
+            TableName=table_name,
+            Key={"pkey": {"S": "testrecord"}},
+            UpdateExpression="",
+            ExpressionAttributeValues={":order": {"SS": ["item"]}},
+        )
+    err = ex.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"] == "Invalid UpdateExpression: The expression can not be empty;"
+    )
+
+
 @mock_aws
 def test_attribute_item_delete():
     name = "TestTable"
diff --git a/tests/test_dynamodb/test_dynamodb_update_expressions.py b/tests/test_dynamodb/test_dynamodb_update_expressions.py
index 526b00fce..3add47cfd 100644
--- a/tests/test_dynamodb/test_dynamodb_update_expressions.py
+++ b/tests/test_dynamodb/test_dynamodb_update_expressions.py
@@ -206,3 +206,22 @@ def test_update_item_with_empty_values(table_name=None):
     err = exc.value.response["Error"]
     assert err["Code"] == "ValidationException"
     assert err["Message"] == "ExpressionAttributeValues must not be empty"
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified()
+def test_update_item_with_empty_expression(table_name=None):
+    dynamodb = boto3.client("dynamodb", "us-east-1")
+
+    dynamodb.put_item(TableName=table_name, Item={"pk": {"S": "foo"}})
+    with pytest.raises(ClientError) as exc:
+        dynamodb.update_item(
+            TableName=table_name,
+            Key={"pk": {"S": "foo"}},
+            UpdateExpression="",
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"] == "Invalid UpdateExpression: The expression can not be empty;"
+    )
diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py
index b2fe8dd78..babba5bee 100644
--- a/tests/test_events/test_events.py
+++ b/tests/test_events/test_events.py
@@ -13,7 +13,7 @@ from botocore.exceptions import ClientError
 from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from moto.core.utils import iso_8601_datetime_without_milliseconds
-from tests import aws_verified
+from tests import allow_aws_request, aws_verified
 
 RULES = [
     {"Name": "test1", "ScheduleExpression": "rate(5 minutes)"},
@@ -1584,14 +1584,17 @@ def test_delete_archive_error_unknown_archive():
     assert ex.response["Error"]["Message"] == f"Archive {name} does not exist."
 
 
-@mock_aws
+@aws_verified
+@pytest.mark.aws_verified
 def test_archive_actual_events():
     # given
+    sts = boto3.client("sts", "us-east-1")
+    account_id = sts.get_caller_identity()["Account"]
     client = boto3.client("events", "eu-central-1")
     name = "test-archive"
     name_2 = "test-archive-no-match"
     name_3 = "test-archive-matches"
-    event_bus_arn = f"arn:aws:events:eu-central-1:{ACCOUNT_ID}:event-bus/default"
+    event_bus_arn = f"arn:aws:events:eu-central-1:{account_id}:event-bus/default"
     event = {
         "Source": "source",
         "DetailType": "type",
@@ -1609,24 +1612,72 @@ def test_archive_actual_events():
         EventPattern=json.dumps({"detail-type": ["type"], "source": ["source"]}),
     )
 
+    # then
+    rules = client.list_rules()["Rules"]
+    assert len(rules) >= 3
+    for rule in rules:
+        assert rule["Name"] in [
+            f"Events-Archive-{name}",
+            f"Events-Archive-{name_2}",
+            f"Events-Archive-{name_3}",
+        ]
+        assert rule["ManagedBy"] == "prod.vhs.events.aws.internal"
+        if rule["Name"] == f"Events-Archive-{name}":
+            assert json.loads(rule["EventPattern"]) == {
+                "replay-name": [{"exists": False}]
+            }
+        if rule["Name"] == f"Events-Archive-{name_2}":
+            assert json.loads(rule["EventPattern"]) == {
+                "detail-type": ["type"],
+                "replay-name": [{"exists": False}],
+                "source": ["test"],
+            }
+        if rule["Name"] == f"Events-Archive-{name_3}":
+            assert json.loads(rule["EventPattern"]) == {
+                "detail-type": ["type"],
+                "replay-name": [{"exists": False}],
+                "source": ["source"],
+            }
+
+        targets = client.list_targets_by_rule(Rule=rule["Name"])["Targets"]
+        assert len(targets) == 1
+        assert targets[0]["Arn"] == "arn:aws:events:eu-central-1:::"
+
+        transformer = targets[0]["InputTransformer"]
+        assert transformer["InputPathsMap"] == {}
+
+        template = transformer["InputTemplate"]
+        assert '"archive-arn"' in template
+        assert '"event": <aws.events.event.json>' in template
+        assert '"ingestion-time": <aws.events.event.ingestion-time>' in template
+
     # when
     response = client.put_events(Entries=[event])
-
-    # then
     assert response["FailedEntryCount"] == 0
     assert len(response["Entries"]) == 1
 
-    response = client.describe_archive(ArchiveName=name)
-    assert response["EventCount"] == 1
-    assert response["SizeBytes"] > 0
+    # then
+    if not allow_aws_request():
+        # AWS doesn't (immediately) update the EventCount
+        # Only test this against Moto
+        response = client.describe_archive(ArchiveName=name)
+        assert response["EventCount"] == 1
+        assert response["SizeBytes"] > 0
 
-    response = client.describe_archive(ArchiveName=name_2)
-    assert response["EventCount"] == 0
-    assert response["SizeBytes"] == 0
+        response = client.describe_archive(ArchiveName=name_2)
+        assert response["EventCount"] == 0
+        assert response["SizeBytes"] == 0
 
-    response = client.describe_archive(ArchiveName=name_3)
-    assert response["EventCount"] == 1
-    assert response["SizeBytes"] > 0
+        response = client.describe_archive(ArchiveName=name_3)
+        assert response["EventCount"] == 1
+        assert response["SizeBytes"] > 0
+
+    client.delete_archive(ArchiveName=name)
+    client.delete_archive(ArchiveName=name_2)
+    client.delete_archive(ArchiveName=name_3)
+
+    rules = client.list_rules()["Rules"]
+    assert rules == []
 
 
 @mock_aws
diff --git a/tests/test_events/test_events_cloudformation.py b/tests/test_events/test_events_cloudformation.py
index f9f808ea1..c643d9b63 100644
--- a/tests/test_events/test_events_cloudformation.py
+++ b/tests/test_events/test_events_cloudformation.py
@@ -1,6 +1,7 @@
 import copy
 import json
 from string import Template
+from uuid import uuid4
 
 import boto3
 import pytest
@@ -8,6 +9,7 @@ from botocore.exceptions import ClientError
 
 from moto import mock_aws
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from tests import aws_verified
 
 archive_template = Template(
     json.dumps(
@@ -69,6 +71,64 @@ empty = json.dumps(
     }
 )
 
+rule_with_targets = """---
+AWSTemplateFormatVersion: 2010-09-09
+Resources:
+  MyEventBus:
+    Type: AWS::Events::EventBus
+    Properties:
+      Name: "TestEventBusName"
+
+  DestQueue:
+    Type: AWS::SQS::Queue
+
+  MyQueuePolicy:
+    Type: AWS::SQS::QueuePolicy
+    Properties:
+      PolicyDocument:
+          Version: 2012-10-17
+          Statement:
+            - Effect: Allow
+              Principal:
+                Service: events.amazonaws.com
+              Action: sqs:SendMessage
+              Resource: !GetAtt DestQueue.Arn
+              Condition:
+                ArnEquals:
+                  aws:SourceArn: !GetAtt MyRule.Arn
+            - Effect: Allow
+              Principal:
+                Service: events.amazonaws.com
+              Action: sqs:SendMessage
+              Resource: !GetAtt DestQueue.Arn
+      Queues:
+        - !Ref DestQueue
+
+  MyRule:
+    Type: AWS::Events::Rule
+    Properties:
+      EventBusName: !GetAtt MyEventBus.Name
+      EventPattern:
+        source:
+          - my.source
+        detail-type:
+          - Malicious
+      State: ENABLED
+      Targets:
+        - Id: meh
+          Arn: !GetAtt DestQueue.Arn
+          InputTransformer:
+            InputPathsMap:
+              account: $.account
+              detail: $.detail
+              detail-type: $.detail-type
+              id: $.id
+              region: $.region
+              resources: $.resources
+              time: $.time
+            InputTemplate: '{\"id\": \"<id>\",\"detail-type\": \"<detail-type>\",\"time\":\"<time>\",\"region\": \"<region>\",\"account\": \"<account>\",\"resources\":<resources>,\"http_method\": \"POST\",\"http_endpoint\":\"/test/endpoint\",\"detail\":<detail>}'
+"""
+
 
 @mock_aws
 def test_create_archive():
@@ -183,3 +243,71 @@ def test_delete_rule():
     err = exc.value.response["Error"]
     assert err["Code"] == "ResourceNotFoundException"
     assert err["Message"] == "Rule test-rule does not exist."
+
+
+@aws_verified
+@pytest.mark.aws_verified
+def test_create_rule_with_targets():
+    # given
+    cfn_client = boto3.client("cloudformation", region_name="eu-central-1")
+    sqs_client = boto3.client("sqs", region_name="eu-central-1")
+    sts = boto3.client("sts", "us-east-1")
+
+    account_id = sts.get_caller_identity()["Account"]
+    stack_name = f"test-stack-{str(uuid4())[0:6]}"
+
+    # when
+    cfn_client.create_stack(StackName=stack_name, TemplateBody=rule_with_targets)
+    cfn_client.get_waiter("stack_create_complete").wait(StackName=stack_name)
+    try:
+        # then
+        events_client = boto3.client("events", region_name="eu-central-1")
+        rule = events_client.list_rules(EventBusName="TestEventBusName")["Rules"][0]
+
+        targets = events_client.list_targets_by_rule(
+            Rule=rule["Name"], EventBusName="TestEventBusName"
+        )["Targets"]
+        assert len(targets) == 1
+        assert targets[0]["Id"] == "meh"
+        assert targets[0]["InputTransformer"]["InputPathsMap"]["account"] == "$.account"
+
+        resources = cfn_client.list_stack_resources(StackName=stack_name)[
+            "StackResourceSummaries"
+        ]
+        queue = next((r for r in resources if r["LogicalResourceId"] == "DestQueue"))
+        q_url = queue["PhysicalResourceId"]
+
+        entry = dict(
+            Detail='{"a": "b"}',
+            DetailType="Malicious",
+            EventBusName="TestEventBusName",
+            Resources=["path/to/file"],
+            Source="my.source",
+        )
+        entry_id = events_client.put_events(Entries=[entry])["Entries"][0]["EventId"]
+
+        messages = sqs_client.receive_message(QueueUrl=q_url, WaitTimeSeconds=20)[
+            "Messages"
+        ]
+        msg_body = json.loads(messages[0]["Body"])
+
+        msg_body.pop("time")
+        assert msg_body == {
+            "id": entry_id,
+            "detail-type": "Malicious",
+            "region": "eu-central-1",
+            "account": account_id,
+            "resources": ["path/to/file"],
+            "http_method": "POST",
+            "http_endpoint": "/test/endpoint",
+            "detail": {"a": "b"},
+        }
+
+    finally:
+        cfn_client.delete_stack(StackName=stack_name)
+        cfn_client.get_waiter("stack_delete_complete").wait(StackName=stack_name)
+
+        with pytest.raises(ClientError) as exc:
+            events_client.list_rules(EventBusName="TestEventBusName")
+        err = exc.value.response["Error"]
+        assert err["Code"] == "ResourceNotFoundException"
diff --git a/tests/test_rds/test_rds.py b/tests/test_rds/test_rds.py
index ac70f9c57..6ed50fb0e 100644
--- a/tests/test_rds/test_rds.py
+++ b/tests/test_rds/test_rds.py
@@ -551,7 +551,7 @@ def test_describe_non_existent_database():
 def test_modify_db_instance():
     conn = boto3.client("rds", region_name=DEFAULT_REGION)
     conn.create_db_instance(
-        DBInstanceIdentifier="db-master-1",
+        DBInstanceIdentifier="db-id",
         AllocatedStorage=10,
         DBInstanceClass="postgres",
         Engine="postgres",
@@ -560,23 +560,22 @@ def test_modify_db_instance():
         Port=1234,
         DBSecurityGroups=["my_sg"],
     )
-    instances = conn.describe_db_instances(DBInstanceIdentifier="db-master-1")
-    assert instances["DBInstances"][0]["AllocatedStorage"] == 10
+    inst = conn.describe_db_instances(DBInstanceIdentifier="db-id")["DBInstances"][0]
+    assert inst["AllocatedStorage"] == 10
+    assert inst["EnabledCloudwatchLogsExports"] == []
+
     conn.modify_db_instance(
-        DBInstanceIdentifier="db-master-1",
+        DBInstanceIdentifier="db-id",
         AllocatedStorage=20,
         ApplyImmediately=True,
         VpcSecurityGroupIds=["sg-123456"],
+        CloudwatchLogsExportConfiguration={"EnableLogTypes": ["error"]},
     )
-    instances = conn.describe_db_instances(DBInstanceIdentifier="db-master-1")
-    assert instances["DBInstances"][0]["AllocatedStorage"] == 20
-    assert instances["DBInstances"][0]["PreferredMaintenanceWindow"] == (
-        "wed:06:38-wed:07:08"
-    )
-    assert (
-        instances["DBInstances"][0]["VpcSecurityGroups"][0]["VpcSecurityGroupId"]
-        == "sg-123456"
-    )
+    inst = conn.describe_db_instances(DBInstanceIdentifier="db-id")["DBInstances"][0]
+    assert inst["AllocatedStorage"] == 20
+    assert inst["PreferredMaintenanceWindow"] == "wed:06:38-wed:07:08"
+    assert inst["VpcSecurityGroups"][0]["VpcSecurityGroupId"] == "sg-123456"
+    assert inst["EnabledCloudwatchLogsExports"] == ["error"]
 
 
 @mock_aws
@@ -1262,6 +1261,34 @@ def test_restore_db_instance_to_point_in_time():
         )
         == 1
     )
+    # ensure another pit restore can be made
+    new_instance = conn.restore_db_instance_to_point_in_time(
+        SourceDBInstanceIdentifier="db-primary-1",
+        TargetDBInstanceIdentifier="db-restore-2",
+    )["DBInstance"]
+    assert new_instance["DBInstanceIdentifier"] == "db-restore-2"
+    assert new_instance["DBInstanceClass"] == "db.m1.small"
+    assert new_instance["StorageType"] == "gp2"
+    assert new_instance["Engine"] == "postgres"
+    assert new_instance["DBName"] == "staging-postgres"
+    assert new_instance["DBParameterGroups"][0]["DBParameterGroupName"] == (
+        "default.postgres9.3"
+    )
+    assert new_instance["DBSecurityGroups"] == [
+        {"DBSecurityGroupName": "my_sg", "Status": "active"}
+    ]
+    assert new_instance["Endpoint"]["Port"] == 5432
+
+    # Verify it exists
+    assert len(conn.describe_db_instances()["DBInstances"]) == 3
+    assert (
+        len(
+            conn.describe_db_instances(DBInstanceIdentifier="db-restore-2")[
+                "DBInstances"
+            ]
+        )
+        == 1
+    )
 
 
 @mock_aws