Repository: getmoto/moto
Pull request: dynamodb2 - updated error message when UpdateItem with ConditionalExpression fails
Resolves #4352 

Problem statement:
Incorrect and misleading error message while performing conditional UpdateItem operation on nonexistent item in dynamodb2
Calling `update_item` on `Table` object obtained from `boto3.resource('dynamodb')` with an optional `ConditionalExpression` throws an exception with very misleading error message. The message differs from the one actually returned by the real API.

```python
import boto3
from moto import mock_dynamodb2

if __name__ == '__main__':
    with mock_dynamodb2():
        dynamodb = boto3.resource('dynamodb')
        dynamodb.create_table(
            TableName='test-table',
            AttributeDefinitions=[
                {
                    'AttributeName': 'id',
                    'AttributeType': 'S'
                }
            ],
            KeySchema=[
                {
                    'AttributeName': 'id',
                    'KeyType': 'HASH'
                }
            ]
        )
        table = dynamodb.Table('test-table')
        table.put_item(
            Item={
                'id': '1',
                'owner': 'ABC',
                'guests': set(['DEF', 'GHI', 'JKL']),
                'items': ['item0', 'item1', 'item2']
            }
        )

        test_user = 'DEF'
        new_item = 'item3'

        res = table.update_item(
            Key={
                'id': '2'
            },
            UpdateExpression='SET #i = list_append(#i, :vals)',
            ConditionExpression='#u = :userid Or contains(#g, :userid)',
            ExpressionAttributeNames={
                '#i': 'items',
                '#u': 'user_id',
                '#g': 'guests'
            },
            ExpressionAttributeValues={
                ':vals': [new_item],
                ':userid': test_user
            },
            ReturnValues='UPDATED_NEW'
        )

```

In `put_item()` function call item with PK `id=1` is being inserted to the table. Then, `update_item()` is being called for an item with PK `id=2` that obviously does not exist at this point. In such a case, boto3 throws this exception:
```
botocore.errorfactory.ConditionalCheckFailedException: An error occurred (ConditionalCheckFailedException) when calling the UpdateItem operation: The conditional request failed
```
which, in my opinion, is not perfect either, as it does not provide any hint for a reason behind request failure. However, the one returned with moto mocks is much more misleading:
```
botocore.errorfactory.ConditionalCheckFailedException: An error occurred (ConditionalCheckFailedException) when calling the UpdateItem operation: A condition specified in the operation could not be evaluated.
```

I know this is might be a detail, but to me those two messages have completely different meaning. It is not only misleading to inform user that it is an update expression that lead request to be failed, it completely fails to point at the core of the problem. While the real message is much more general and doesn't point directly at the issue, it doesn't cause any confusion.

In order to fix this issue, I'd expect `update_item()` function of `dynamodb` resource called with `ConditionExpression` on `Key` that cannot be resolved to an item to throw an exception with message saying `An error occurred (ConditionalCheckFailedException) when calling the UpdateItem operation: The conditional request failed`.

My moto version is 2.2.8

Hints from issue discussion:
Thanks for raising this @k0mmsussert0d - you're right, we should return the correct error message. Marking it as a bug.

Commit message: Added test case checking if ConditionalCheckFailedException is thrown on attempt to perform UpdateItem on non-existent item (#4352)

Patch to review:
diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py
index 486c000e5bab..b77c547aae80 100644
--- a/moto/dynamodb2/responses.py
+++ b/moto/dynamodb2/responses.py
@@ -856,7 +856,7 @@ def update_item(self):
         except ValueError:
             er = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException"
             return self.error(
-                er, "A condition specified in the operation could not be evaluated."
+                er, "An error occurred (ConditionalCheckFailedException) when calling the UpdateItem operation: The conditional request failed"
             )
         except TypeError:
             er = "com.amazonaws.dynamodb.v20111205#ValidationException"
diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py
index d7ed1252862f..ec27dfdf7560 100644
--- a/tests/test_dynamodb2/test_dynamodb.py
+++ b/tests/test_dynamodb2/test_dynamodb.py
@@ -2887,6 +2887,16 @@ def test_condition_expressions():
             ExpressionAttributeNames={"#existing": "existing", "#match": "match"},
         )
 
+    with pytest.raises(client.exceptions.ConditionalCheckFailedException):
+        client.update_item(
+            TableName="test1",
+            Key={"client": {"S": "client2"}, "app": {"S": "app1"}},
+            UpdateExpression="set #match=:match",
+            ConditionExpression="attribute_exists(#existing)",
+            ExpressionAttributeValues={":match": {"S": "match"}},
+            ExpressionAttributeNames={"#existing": "existing", "#match": "match"},
+        )
+
     with pytest.raises(client.exceptions.ConditionalCheckFailedException):
         client.delete_item(
             TableName="test1",


Code review comments:
diff_hunk: @@ -2887,6 +2887,16 @@ def test_condition_expressions():
             ExpressionAttributeNames={"#existing": "existing", "#match": "match"},
         )
 
+    with pytest.raises(client.exceptions.ConditionalCheckFailedException):
path: tests/test_dynamodb2/test_dynamodb.py
text: @user1:
```suggestion
    with pytest.raises(client.exceptions.ConditionalCheckFailedException) as exc:
```