Repository: getmoto/moto
Pull request #7991 (2024-08-16 15:18:13)

RDS Fix bug that causes second PIT restore to fail
Previous code broke if you tried to create more than one PIT restore due to a KeyError on 'db_subnet_group' when trying to delete the key.  This just checks for existence before deleting the key

Added a test to the existing `test_restore_db_instance_to_point_in_time()`.  Don't know if it's wanted or if I should ass it to a separate function




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/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")


Test patch:
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_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