From 7c26f9f8310947091df90e4ef68c8fdf79d89794 Mon Sep 17 00:00:00 2001 From: Khanh Le <49672380+khanh-alice@users.noreply.github.com> Date: Fri, 23 Feb 2024 02:06:51 +0700 Subject: [PATCH] DynamoDB: transact_write_items() should raise ValidationException when putting and deleting the same item (#7378) --- moto/dynamodb/models/__init__.py | 6 ++- .../exceptions/test_dynamodb_transactions.py | 49 +++++++++++++++++++ tests/test_dynamodb/test_dynamodb.py | 36 ++++++++------ 3 files changed, 74 insertions(+), 17 deletions(-) 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 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