Repository: getmoto/moto (python)

Issue:
EventBridge targets are not identified by moto when parsing cloudformation file.  
Hi, 
I'm using a cloudformation file to create my test setup. The scenario is simple: put an event, use a rule to move that event to SQS, and use an InputTransformer to change the event a bit. 
When loading the CFT, all is good, except that the target is not identified, and therefore the event is not present in the queue. 
When I add a target using boto3 code (same target from the CFT), I can see the event in the queue (although it is not transformed - not sure if I did something wrong, or if it doesn't work as well)

using python 3.12.2
moto[all]==5.0.2
boto3==1.34.55
cfn-flip==1.3.0
pytest==8.0.2
(running on windows, I suspect it shouldn't matter, but just in case)

Full details below, but the important part is this:
Before I run `events_client.put_targets` 
the result of `events_client.list_targets_by_rule(EventBusName=event_bus_name,Rule=events_client.list_rules(EventBusName=event_bus_name)['Rules'][0]['Name'])`  the target list is empty (and a message is not put in SQS). 
After I run `events_client.put_targets`, using the same data parsed from the CFT (had to manually replace the ARN, as the function was not accepted by moto, which makes sense to me) - the target is present, and events are getting to SQS. 




I'm using the following CFT:
```
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>}"
```

my test:

```
from cfn_tools import load_yaml
import boto3
from moto import mock_aws
from assertpy import assert_that


def format_message(Detail, DetailType, EventBusName, Resources: list[str], Source):
    entry = {
PutEvents call is used.
        'Source': Source,
        'Detail': json.dumps(Detail),
        'DetailType': 'Malicious',
        'Resources': Resources,
        'EventBusName': EventBusName
    }
    return entry

#This is the important stuff - I expected to not need this
def workaround_missing_target(events_client, q_url, sqs_client, template):
    q_arn = sqs_client.get_queue_attributes(QueueUrl=q_url, AttributeNames=['All'])['Attributes']['QueueArn']
    event_bus_name = next(filter(lambda x: x['Name'] != 'default', events_client.list_event_buses()['EventBuses']))[
        'Name']
    targets = template['Resources']['MyRule']['Properties']['Targets']
    targets[0]['Arn'] = q_arn
    events_client.put_targets(
        Rule=events_client.list_rules(EventBusName=event_bus_name)['Rules'][0]['Name'],
        EventBusName=event_bus_name,
        Targets=targets,
    )


@mock_aws
def test_moto():
    path = os.path.join(os.path.dirname(__file__), 'my_template.yml')
    with open(path) as f:
        template_str = f.read()
    template = load_yaml(template_str)
    cf_client = boto3.client('cloudformation', region_name='eu-west-1')
    sqs_client = boto3.client('sqs', region_name='eu-west-1')
    events_client = boto3.client('events', region_name='eu-west-1')
    cf_client.create_stack(StackName='my-stack', TemplateBody=template_str)
    q_url = sqs_client.list_queues()['QueueUrls'][0]

    workaround_missing_target(events_client, q_url, sqs_client, template)

    entry = format_message(Detail={'a': 'b'}, DetailType=None, EventBusName='TestEventBusName',
                           Resources=["path/to/file"], Source="my.source")
    events_client.put_events(Entries=[entry])

    m = sqs_client.receive_message(QueueUrl=q_url)
    assert_that(m['Messages'][0]['Body']).contains('http_method') # assertion fails, not sure if I did something wrong, or if this is another bug
```





Pull request:
Events: now supports the InputTemplate-parameter
Fixes #7753 

- The CloudFormation `AWS::Events::Rule`-type now also creates/deletes Targets
- Events are now formatted according to the InputTemplate (if provided)

Patch:
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..e8d56b279 100644
--- a/moto/rds/models.py
+++ b/moto/rds/models.py
@@ -1881,7 +1881,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/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_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..7b95748a9 100644
--- a/tests/test_rds/test_rds.py
+++ b/tests/test_rds/test_rds.py
@@ -1262,6 +1262,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