Repository: getmoto/moto
Pull request #7378 (2024-02-21 09:33:27)

DynamoDB: transact_write_items() should raise ValidationException when putting and deleting the same item
DynamoDB: transact_write_items() should raise ValidationException when putting and deleting the same item.

### How to reproduce

Given

`main.py`

```python
import boto3


def main():
    client = boto3.client("dynamodb", region_name="us-east-1")

    client.transact_write_items(
        TransactItems=[
            {
                "Put": {
                    "TableName": "test-table",
                    "Item": {
                        "pk": {"S": "test-pk"},
                        "sk": {"S": "test-sk"},
                        "field": {"S": "test-field"}
                    }
                }
            },
            {
                "Delete": {
                    "TableName": "test-table",
                    "Key": {
                        "pk": {"S": "test-pk"},
                        "sk": {"S": "test-sk"},
                    }
                }
            }
        ]
    )


if __name__ == "__main__":
    main()
```

and `test_main.py`

```python
import boto3
from moto import mock_aws


@mock_aws
def test_main():
    client = boto3.client("dynamodb", region_name="us-east-1")

    client.create_table(
        TableName="test-table",
        KeySchema=[
            {"AttributeName": "pk", "KeyType": "HASH"},
            {"AttributeName": "sk", "KeyType": "RANGE"},
        ],
        AttributeDefinitions=[
            {"AttributeName": "pk", "AttributeType": "S"},
            {"AttributeName": "sk", "AttributeType": "S"}
        ],
        BillingMode="PAY_PER_REQUEST",
    )

    from main import main

    main()
```

Running `main.py` will raise a `ValidationError` when connecting to an actual DynamoDB

```console
Traceback (most recent call last):
  File "/Users/khanh.le/moto-test/moto-test/main.py", line 33, in <module>
    main()
  File "/Users/khanh.le/moto-test/moto-test/main.py", line 7, in main
    client.transact_write_items(
  File "/Users/khanh.le/moto-test/moto-test/.venv/lib/python3.12/site-packages/botocore/client.py", line 553, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/khanh.le/moto-test/moto-test/.venv/lib/python3.12/site-packages/botocore/client.py", line 1009, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the TransactWriteItems operation: Transaction request cannot include multiple operations on one item
```

But running `test_main.py` will not (PASSED)

```
boto3==1.34.46
botocore==1.34.46
moto==5.0.2
```

### Checklist

- [x] Feature is added
- [x] The linter is happy
- [x] Tests are added
- [x] All tests pass, existing and new


Patch:
diff --git a/moto/core/responses.py b/moto/core/responses.py
index 73f2b37c2..54b575912 100644
--- a/moto/core/responses.py
+++ b/moto/core/responses.py
@@ -343,8 +343,6 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
         if hasattr(self.body, "read"):
             self.body = self.body.read()
 
-        raw_body = self.body
-
         # https://github.com/getmoto/moto/issues/6692
         # Content coming from SDK's can be GZipped for performance reasons
         if (
@@ -379,7 +377,7 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin):
                         OrderedDict(
                             (key, [value])
                             for key, value in parse_qsl(
-                                raw_body, keep_blank_values=True
+                                self.body, keep_blank_values=True
                             )
                         )
                     )
diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py
index c3be0ff5d..0627a238e 100644
--- a/moto/dynamodb/models/__init__.py
+++ b/moto/dynamodb/models/__init__.py
@@ -579,7 +579,11 @@ class DynamoDBBackend(BaseBackend):
                     item = item["Put"]
                     attrs = item["Item"]
                     table_name = item["TableName"]
-                    check_unicity(table_name, item)
+                    table = self.get_table(table_name)
+                    key = {table.hash_key_attr: attrs[table.hash_key_attr]}
+                    if table.range_key_attr is not None:
+                        key[table.range_key_attr] = attrs[table.range_key_attr]
+                    check_unicity(table_name, key)
                     condition_expression = item.get("ConditionExpression", None)
                     expression_attribute_names = item.get(
                         "ExpressionAttributeNames", None


Test patch:
diff --git a/tests/test_cloudwatch/test_cloudwatch_boto3.py b/tests/test_cloudwatch/test_cloudwatch_boto3.py
index a5d881ac5..7de4cb844 100644
--- a/tests/test_cloudwatch/test_cloudwatch_boto3.py
+++ b/tests/test_cloudwatch/test_cloudwatch_boto3.py
@@ -2,6 +2,7 @@ import copy
 from datetime import datetime, timedelta, timezone
 from decimal import Decimal
 from operator import itemgetter
+from unittest import SkipTest
 from uuid import uuid4
 
 import boto3
@@ -10,7 +11,7 @@ from botocore.exceptions import ClientError
 from dateutil.tz import tzutc
 from freezegun import freeze_time
 
-from moto import mock_aws
+from moto import mock_aws, settings
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
 from moto.core.utils import utcnow
 
@@ -27,6 +28,43 @@ def test_put_metric_data_no_dimensions():
     assert {"Namespace": "tester", "MetricName": "metric", "Dimensions": []} in metrics
 
 
+@mock_aws
+def test_put_a_ton_of_metric_data():
+    """
+    A sufficiently large call with metric data triggers request compression
+    Moto should decompress the request if this is the case, and the request should succeed
+    """
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Can't test large requests in ServerMode")
+    cloudwatch = boto3.client("cloudwatch", "us-east-1")
+
+    metrics = []
+    metric_dimensions = [{"Name": "Environment", "Value": "env1"}]
+    metric_args = {"Unit": "Seconds", "MetricName": "TestCWMetrics"}
+
+    for type, licences in {
+        k: v
+        for k, v in {
+            "userTypes": [{"id": f"UserType{x}"} for x in range(0, 50)]
+        }.items()
+        if k in ["userTypes", "appBundles", "apps", "extensions", "portalCapabilities"]
+    }.items():
+        for licence in licences:
+            metrics.append(
+                {
+                    **metric_args,
+                    "Dimensions": [
+                        *metric_dimensions,
+                        {"Name": "Type", "Value": f"{type}/{licence['id']}"},
+                    ],
+                    "Value": 1,
+                }
+            )
+
+    cloudwatch.put_metric_data(Namespace="acme", MetricData=metrics)
+    # We don't really need any assertions - we just need to know that the call succeeds
+
+
 @mock_aws
 def test_put_metric_data_can_not_have_nan():
     client = boto3.client("cloudwatch", region_name="us-west-2")
diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py b/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py
index 61f2c59dd..212739388 100644
--- a/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py
+++ b/tests/test_dynamodb/exceptions/test_dynamodb_transactions.py
@@ -46,6 +46,55 @@ def test_multiple_transactions_on_same_item():
     )
 
 
+@mock_aws
+def test_transact_write_items__put_and_delete_on_same_item():
+    schema = {
+        "KeySchema": [
+            {"AttributeName": "pk", "KeyType": "HASH"},
+            {"AttributeName": "sk", "KeyType": "RANGE"},
+        ],
+        "AttributeDefinitions": [
+            {"AttributeName": "pk", "AttributeType": "S"},
+            {"AttributeName": "sk", "AttributeType": "S"},
+        ],
+    }
+    dynamodb = boto3.client("dynamodb", region_name="us-east-1")
+    dynamodb.create_table(
+        TableName="test-table", BillingMode="PAY_PER_REQUEST", **schema
+    )
+
+    with pytest.raises(ClientError) as exc:
+        dynamodb.transact_write_items(
+            TransactItems=[
+                {
+                    "Put": {
+                        "TableName": "test-table",
+                        "Item": {
+                            "pk": {"S": "test-pk"},
+                            "sk": {"S": "test-sk"},
+                            "field": {"S": "test-field"},
+                        },
+                    }
+                },
+                {
+                    "Delete": {
+                        "TableName": "test-table",
+                        "Key": {
+                            "pk": {"S": "test-pk"},
+                            "sk": {"S": "test-sk"},
+                        },
+                    }
+                },
+            ]
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == "Transaction request cannot include multiple operations on one item"
+    )
+
+
 @mock_aws
 def test_transact_write_items__too_many_transactions():
     schema = {
diff --git a/tests/test_dynamodb/test_dynamodb.py b/tests/test_dynamodb/test_dynamodb.py
index 26d5e0545..808d48d2f 100644
--- a/tests/test_dynamodb/test_dynamodb.py
+++ b/tests/test_dynamodb/test_dynamodb.py
@@ -3856,24 +3856,27 @@ def test_transact_write_items_conditioncheck_passes():
     dynamodb.create_table(
         TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
     )
-    # Insert an item without email address
-    dynamodb.put_item(TableName="test-table", Item={"id": {"S": "foo"}})
-    # Put an email address, after verifying it doesn't exist yet
+    # Insert an item with email address
+    dynamodb.put_item(
+        TableName="test-table",
+        Item={"id": {"S": "foo"}, "email_address": {"S": "foo@moto.com"}},
+    )
+    # Put a new item, after verifying the exising item also has email address
     dynamodb.transact_write_items(
         TransactItems=[
             {
                 "ConditionCheck": {
                     "Key": {"id": {"S": "foo"}},
                     "TableName": "test-table",
-                    "ConditionExpression": "attribute_not_exists(#e)",
+                    "ConditionExpression": "attribute_exists(#e)",
                     "ExpressionAttributeNames": {"#e": "email_address"},
                 }
             },
             {
                 "Put": {
                     "Item": {
-                        "id": {"S": "foo"},
-                        "email_address": {"S": "test@moto.com"},
+                        "id": {"S": "bar"},
+                        "email_address": {"S": "bar@moto.com"},
                     },
                     "TableName": "test-table",
                 }
@@ -3882,8 +3885,9 @@ def test_transact_write_items_conditioncheck_passes():
     )
     # Assert all are present
     items = dynamodb.scan(TableName="test-table")["Items"]
-    assert len(items) == 1
-    assert items[0] == {"email_address": {"S": "test@moto.com"}, "id": {"S": "foo"}}
+    assert len(items) == 2
+    assert items[0] == {"email_address": {"S": "foo@moto.com"}, "id": {"S": "foo"}}
+    assert items[1] == {"email_address": {"S": "bar@moto.com"}, "id": {"S": "bar"}}
 
 
 @mock_aws
@@ -3896,12 +3900,12 @@ def test_transact_write_items_conditioncheck_fails():
     dynamodb.create_table(
         TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
     )
-    # Insert an item with email address
+    # Insert an item without email address
     dynamodb.put_item(
         TableName="test-table",
-        Item={"id": {"S": "foo"}, "email_address": {"S": "test@moto.com"}},
+        Item={"id": {"S": "foo"}},
     )
-    # Try to put an email address, but verify whether it exists
+    # Try putting a new item, after verifying the exising item also has email address
     # ConditionCheck should fail
     with pytest.raises(ClientError) as ex:
         dynamodb.transact_write_items(
@@ -3910,15 +3914,15 @@ def test_transact_write_items_conditioncheck_fails():
                     "ConditionCheck": {
                         "Key": {"id": {"S": "foo"}},
                         "TableName": "test-table",
-                        "ConditionExpression": "attribute_not_exists(#e)",
+                        "ConditionExpression": "attribute_exists(#e)",
                         "ExpressionAttributeNames": {"#e": "email_address"},
                     }
                 },
                 {
                     "Put": {
                         "Item": {
-                            "id": {"S": "foo"},
-                            "email_address": {"S": "update@moto.com"},
+                            "id": {"S": "bar"},
+                            "email_address": {"S": "bar@moto.com"},
                         },
                         "TableName": "test-table",
                     }
@@ -3929,10 +3933,10 @@ def test_transact_write_items_conditioncheck_fails():
     assert ex.value.response["Error"]["Code"] == "TransactionCanceledException"
     assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
-    # Assert the original email address is still present
+    # Assert the original item is still present
     items = dynamodb.scan(TableName="test-table")["Items"]
     assert len(items) == 1
-    assert items[0] == {"email_address": {"S": "test@moto.com"}, "id": {"S": "foo"}}
+    assert items[0] == {"id": {"S": "foo"}}
 
 
 @mock_aws