Repository: getmoto/moto
Pull request #8139 (2024-09-22 17:14:46)

DynamoDB: get/delete/update_item() now validates all provided keys exist

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8139?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.50%. Comparing base [(`b95bfe9`)](https://app.codecov.io/gh/getmoto/moto/commit/b95bfe95cc0fec17a3ca13dd8c420211826cb380?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`be79920`)](https://app.codecov.io/gh/getmoto/moto/commit/be79920eca8cd9132c0845056ae76fb0728ef53a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 7 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##           master    #8139    +/-   ##
========================================
  Coverage   94.49%   94.50%            
========================================
  Files        1150     1150            
  Lines       99025    99191   +166     
========================================
+ Hits        93571    93736   +165     
- Misses       5454     5455     +1     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8139/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8139/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.78% <0.00%> (-0.03%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8139/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.47% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8139?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py
index fb1f9d57b..8ef022c63 100644
--- a/moto/dynamodb/responses.py
+++ b/moto/dynamodb/responses.py
@@ -16,6 +16,7 @@ from moto.utilities.aws_headers import amz_crc32
 from .exceptions import (
     KeyIsEmptyStringException,
     MockValidationException,
+    ProvidedKeyDoesNotExist,
     ResourceNotFoundException,
     UnknownKeyType,
 )
@@ -565,7 +566,7 @@ class DynamoHandler(BaseResponse):
     @include_consumed_capacity(0.5)
     def get_item(self) -> str:
         name = self.body["TableName"]
-        self.dynamodb_backend.get_table(name)
+        table = self.dynamodb_backend.get_table(name)
         key = self.body["Key"]
         empty_keys = [k for k, v in key.items() if not next(iter(v.values()))]
         if empty_keys:
@@ -589,6 +590,9 @@ class DynamoHandler(BaseResponse):
                     "ExpressionAttributeNames must not be empty"
                 )
 
+        if not all([k in table.attribute_keys for k in key]):
+            raise ProvidedKeyDoesNotExist
+
         expression_attribute_names = expression_attribute_names or {}
         projection_expressions = self._adjust_projection_expression(
             projection_expression, expression_attribute_names
@@ -857,7 +861,9 @@ class DynamoHandler(BaseResponse):
         if return_values not in ("ALL_OLD", "NONE"):
             raise MockValidationException("Return values set to invalid value")
 
-        self.dynamodb_backend.get_table(name)
+        table = self.dynamodb_backend.get_table(name)
+        if not all([k in table.attribute_keys for k in key]):
+            raise ProvidedKeyDoesNotExist
 
         # Attempt to parse simple ConditionExpressions into an Expected
         # expression
@@ -891,6 +897,10 @@ class DynamoHandler(BaseResponse):
                 "Can not use both expression and non-expression parameters in the same request: Non-expression parameters: {AttributeUpdates} Expression parameters: {UpdateExpression}"
             )
 
+        table = self.dynamodb_backend.get_table(name)
+        if not all([k in table.attribute_keys for k in key]):
+            raise ProvidedKeyDoesNotExist
+
         if update_expression is not None:
             update_expression = update_expression.strip()
             if update_expression == "":


Test patch:
diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
index 93385c708..d2db55a4a 100644
--- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
+++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
@@ -122,14 +122,14 @@ def test_query_table_with_wrong_key_attribute_names_throws_exception():
     assert err["Message"] == "Query condition missed key schema element: partitionKey"
 
 
-@mock_aws
-def test_empty_expressionattributenames():
+@pytest.mark.aws_verified
+@dynamodb_aws_verified()
+def test_empty_expressionattributenames(table_name=None):
     ddb = boto3.resource("dynamodb", region_name="us-east-1")
-    ddb.create_table(
-        TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
-    )
-    table = ddb.Table("test-table")
+    table = ddb.Table(table_name)
     with pytest.raises(ClientError) as exc:
+        # Note: provided key is wrong
+        # Empty ExpressionAttributeName is verified earlier, so that's the error we get
         table.get_item(Key={"id": "my_id"}, ExpressionAttributeNames={})
     err = exc.value.response["Error"]
     assert err["Code"] == "ValidationException"
@@ -777,7 +777,7 @@ def test_update_expression_with_trailing_comma():
 
     with pytest.raises(ClientError) as exc:
         table.update_item(
-            Key={"pk": "key", "sk": "sk"},
+            Key={"pk": "key"},
             # Trailing comma should be invalid
             UpdateExpression="SET #attr1 = :val1, #attr2 = :val2,",
             ExpressionAttributeNames={"#attr1": "attr1", "#attr2": "attr2"},
@@ -1462,3 +1462,38 @@ def test_delete_table():
         err.value.response["Error"]["Message"]
         == "1 validation error detected: Table 'test1' can't be deleted while DeletionProtectionEnabled is set to True"
     )
+
+
+@pytest.mark.aws_verified
+@dynamodb_aws_verified(add_range=False)
+def test_provide_range_key_against_table_without_range_key(table_name=None):
+    ddb_client = boto3.client("dynamodb", "us-east-1")
+    with pytest.raises(ClientError) as exc:
+        ddb_client.get_item(
+            TableName=table_name, Key={"pk": {"S": "pk"}, "sk": {"S": "sk"}}
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert err["Message"] == "The provided key element does not match the schema"
+
+    with pytest.raises(ClientError) as exc:
+        ddb_client.delete_item(
+            TableName=table_name, Key={"pk": {"S": "pk"}, "sk": {"S": "sk"}}
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert err["Message"] == "The provided key element does not match the schema"
+
+    with pytest.raises(ClientError) as exc:
+        ddb_client.update_item(
+            TableName=table_name,
+            Key={
+                "pk": {"S": "x"},
+                "ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
+            },
+            UpdateExpression="set body=:New",
+            ExpressionAttributeValues={":New": {"S": "hello"}},
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert err["Message"] == "The provided key element does not match the schema"
diff --git a/tests/test_dynamodb/exceptions/test_key_length_exceptions.py b/tests/test_dynamodb/exceptions/test_key_length_exceptions.py
index f499d0355..2a7488873 100644
--- a/tests/test_dynamodb/exceptions/test_key_length_exceptions.py
+++ b/tests/test_dynamodb/exceptions/test_key_length_exceptions.py
@@ -5,6 +5,7 @@ from botocore.exceptions import ClientError
 
 from moto import mock_aws
 from moto.dynamodb.limits import HASH_KEY_MAX_LENGTH, RANGE_KEY_MAX_LENGTH
+from tests.test_dynamodb import dynamodb_aws_verified
 
 
 @mock_aws
@@ -215,34 +216,22 @@ def test_put_long_string_gsi_range_key_exception():
     )
 
 
-@mock_aws
-def test_update_item_with_long_string_hash_key_exception():
-    name = "TestTable"
-    conn = boto3.client("dynamodb", region_name="us-west-2")
-    conn.create_table(
-        TableName=name,
-        KeySchema=[{"AttributeName": "forum_name", "KeyType": "HASH"}],
-        AttributeDefinitions=[{"AttributeName": "forum_name", "AttributeType": "S"}],
-        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
-    )
+@pytest.mark.aws_verified
+@dynamodb_aws_verified()
+def test_update_item_with_long_string_hash_key_exception(table_name=None):
+    conn = boto3.client("dynamodb", region_name="us-east-1")
 
     conn.update_item(
-        TableName=name,
-        Key={
-            "forum_name": {"S": "x" * HASH_KEY_MAX_LENGTH},
-            "ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
-        },
+        TableName=table_name,
+        Key={"pk": {"S": "x" * HASH_KEY_MAX_LENGTH}},
         UpdateExpression="set body=:New",
         ExpressionAttributeValues={":New": {"S": "hello"}},
     )
 
     with pytest.raises(ClientError) as ex:
         conn.update_item(
-            TableName=name,
-            Key={
-                "forum_name": {"S": "x" * (HASH_KEY_MAX_LENGTH + 1)},
-                "ReceivedTime": {"S": "12/9/2011 11:36:03 PM"},
-            },
+            TableName=table_name,
+            Key={"pk": {"S": "x" * (HASH_KEY_MAX_LENGTH + 1)}},
             UpdateExpression="set body=:New",
             ExpressionAttributeValues={":New": {"S": "hello"}},
         )
diff --git a/tests/test_dynamodb/test_dynamodb_condition_expressions.py b/tests/test_dynamodb/test_dynamodb_condition_expressions.py
index d512f1411..6cd6c40b4 100644
--- a/tests/test_dynamodb/test_dynamodb_condition_expressions.py
+++ b/tests/test_dynamodb/test_dynamodb_condition_expressions.py
@@ -297,7 +297,7 @@ def test_condition_expression__attr_doesnt_exist():
         # Test nonexistent top-level attribute.
         client.update_item(
             TableName="test",
-            Key={"forum_name": {"S": "the-key"}, "subject": {"S": "the-subject"}},
+            Key={"forum_name": {"S": "the-key"}},
             UpdateExpression="set #new_state=:new_state, #ttl=:ttl",
             ConditionExpression="attribute_not_exists(#new_state)",
             ExpressionAttributeNames={"#new_state": "foobar", "#ttl": "ttl"},
diff --git a/tests/test_dynamodb/test_dynamodb_consumedcapacity.py b/tests/test_dynamodb/test_dynamodb_consumedcapacity.py
index af71f555c..b978fd3ee 100644
--- a/tests/test_dynamodb/test_dynamodb_consumedcapacity.py
+++ b/tests/test_dynamodb/test_dynamodb_consumedcapacity.py
@@ -95,7 +95,7 @@ def test_only_return_consumed_capacity_when_required(
     validate_response(response, should_have_capacity, should_have_table)
 
     # GET_ITEM
-    args = {"Key": item}
+    args = {"Key": {"job_id": item["job_id"]}}
     if capacity:
         args["ReturnConsumedCapacity"] = capacity
     response = table.get_item(**args)
diff --git a/tests/test_dynamodb/test_dynamodb_update_expressions.py b/tests/test_dynamodb/test_dynamodb_update_expressions.py
index 3d071d901..a579c75c7 100644
--- a/tests/test_dynamodb/test_dynamodb_update_expressions.py
+++ b/tests/test_dynamodb/test_dynamodb_update_expressions.py
@@ -296,9 +296,9 @@ def test_update_expression_remove_list_and_attribute(table_name=None):
         UpdateExpression="REMOVE #ulist[0], some_param",
         ExpressionAttributeNames={"#ulist": "user_list"},
     )
-    item = ddb_client.get_item(
-        TableName=table_name, Key={"pk": {"S": "primary_key"}, "sk": {"S": "sort_key"}}
-    )["Item"]
+    item = ddb_client.get_item(TableName=table_name, Key={"pk": {"S": "primary_key"}})[
+        "Item"
+    ]
     assert item == {
         "user_list": {"L": [{"M": {"name": {"S": "Jane"}, "surname": {"S": "Smith"}}}]},
         "pk": {"S": "primary_key"},