getmoto / moto

A library that allows you to easily mock out tests based on AWS infrastructure.
http://docs.getmoto.org/en/latest/
Apache License 2.0
7.63k stars 2.04k forks source link

DynamoDB should raise a ValidationError when ADD and DELETE statements include the same document path #8250

Open jusdino opened 1 day ago

jusdino commented 1 day ago

DynamoDB does not support ADD and DELETE actions on the same string set in the same UpdateItem call. Here's a test to demonstrate:

from unittest import TestCase

import boto3
from botocore.exceptions import ClientError
from moto import mock_aws

@mock_aws
class TestEmptyStringSet(TestCase):
    """
    DynamoDB will return a ValidationError, if you try to ADD and DELETE to the same String Set. This can be confirmed
    by running this test against a real AWS account, without the @mock_aws decorator.
    """

    def test_validation_error_on_delete_and_add(self):
        self._table.put_item(Item={'pk': 'foo', 'string_set': {'a', 'b'}})

        with self.assertRaises(ClientError) as e:
            self._table.update_item(
                Key={'pk': 'foo'},
                UpdateExpression='ADD #stringSet :addSet  DELETE #stringSet :deleteSet',
                ExpressionAttributeNames={'#stringSet': 'string_set'},
                ExpressionAttributeValues={':addSet': {'c'}, ':deleteSet': {'a'}},
            )
        self.assertEqual('ValidationException', e.exception.response['Error']['Code'])
        self.assertTrue(
            e.exception.response['Error']['Message'].startswith(
                'Invalid UpdateExpression: Two document paths overlap with each other;'
            )
        )

    def setUp(self):
        self._table_name = 'example-test-table'
        self._client = boto3.client('dynamodb')
        self._table = boto3.resource('dynamodb').create_table(
            AttributeDefinitions=[{'AttributeName': 'pk', 'AttributeType': 'S'}],
            TableName=self._table_name,
            KeySchema=[{'AttributeName': 'pk', 'KeyType': 'HASH'}],
            BillingMode='PAY_PER_REQUEST',
        )
        waiter = self._client.get_waiter('table_exists')
        waiter.wait(TableName=self._table_name)

    def tearDown(self):
        self._table.delete()
        waiter = self._client.get_waiter('table_not_exists')
        waiter.wait(TableName=self._table_name)
bblommers commented 1 day ago

Thanks for raising this @jusdino - I'll mark it as an enhancement to add validation for this scenario.