Repository: getmoto/moto
Issue: dynamodb2.put_item exception clarity on missing HASH/RANGE keys on Input body

The errors thrown from dynamodb2.put_item can be misleading due to the unclear exception catch at https://github.com/spulec/moto/blob/f6fc9c7d54307f77d0cbbf698bab0c3be55ec153/moto/dynamodb2/responses.py#L207-L212, which catches most things within the put_item function call.

This issue covers when a HASH (or RANGE) key is not present in the input Item body which someone wishes to persist to DynamoDB, as shown below. The ideal scenario is a clearer Exception (ideally what would be thrown by boto3) to guide the developer to a resolution quickly.

## Producing the issue
```python
import os
import boto3
from moto import mock_dynamodb2

os.environ["AWS_DEFAULT_REGION"] = "eu-west-1"

class Tests:

    @mock_dynamodb2
    def test_one(self, client):

        dynamodb = boto3.resource('dynamodb')
        table = dynamodb.Table("test")

        dynamodb.create_table(
            AttributeDefinitions=[
                {
                    'AttributeName': 'structure_id',
                    'AttributeType': 'S'
                },
            ],
            TableName='test',
            KeySchema=[
                {
                    'AttributeName': 'structure_id',
                    'KeyType': 'HASH'
                },
            ],
            ProvisionedThroughput={
                'ReadCapacityUnits': 123,
                'WriteCapacityUnits': 123
            }
        )

        # Put an item missing the hash key
        table.put_item(Item={
            'a_terribly_misguided_id_attribute': 'abcdef'
        })
```
## Expected Output
An Exception such as `An error occurred (ValidationException) when calling the PutItem operation: One or more parameter values were invalid: Missing the key structure_id in the item`

## Actual Output
The Exception ` An error occurred (ConditionalCheckFailedException) when calling the PutItem operation`

```
broken_test.py F

==================================================================== FAILURES =====================================================================
_________________________________________________________________ Tests.test_one __________________________________________________________________

self = <broken_test.Tests object at 0x7f8f7fb9b8d0>, client = <FlaskClient <Flask 'app'>>

    @mock_dynamodb2
    def test_one(self, client):
    
        dynamodb = boto3.resource('dynamodb')
        table = dynamodb.Table("test")
    
        dynamodb.create_table(
            AttributeDefinitions=[
                {
                    'AttributeName': 'structure_id',
                    'AttributeType': 'S'
                },
            ],
            TableName='test',
            KeySchema=[
                {
                    'AttributeName': 'structure_id',
                    'KeyType': 'HASH'
                },
            ],
            ProvisionedThroughput={
                'ReadCapacityUnits': 123,
                'WriteCapacityUnits': 123
            }
        )
    
        # Put an item missing the hash key
        table.put_item(Item={
>           'a_terribly_misguided_id_attribute': 'abcdef'
        })

broken_test.py:37: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/home/marty/development/redacted/virtualenv/lib/python3.6/site-packages/boto3/resources/factory.py:520: in do_action
    response = action(self, *args, **kwargs)
/home/marty/development/redacted/virtualenv/lib/python3.6/site-packages/boto3/resources/action.py:83: in __call__
    response = getattr(parent.meta.client, operation_name)(**params)
/home/marty/development/redacted/virtualenv/lib/python3.6/site-packages/botocore/client.py:312: in _api_call
    return self._make_api_call(operation_name, kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <botocore.client.DynamoDB object at 0x7f8f7fae2748>, operation_name = 'PutItem'
api_params = {'Item': {'a_terribly_misguided_id_attribute': 'abcdef'}, 'TableName': 'test'}

    def _make_api_call(self, operation_name, api_params):
        operation_model = self._service_model.operation_model(operation_name)
        request_context = {
            'client_region': self.meta.region_name,
            'client_config': self.meta.config,
            'has_streaming_input': operation_model.has_streaming_input,
            'auth_type': operation_model.auth_type,
        }
        request_dict = self._convert_to_request_dict(
            api_params, operation_model, context=request_context)
    
        handler, event_response = self.meta.events.emit_until_response(
            'before-call.{endpoint_prefix}.{operation_name}'.format(
                endpoint_prefix=self._service_model.endpoint_prefix,
                operation_name=operation_name),
            model=operation_model, params=request_dict,
            request_signer=self._request_signer, context=request_context)
    
        if event_response is not None:
            http, parsed_response = event_response
        else:
            http, parsed_response = self._endpoint.make_request(
                operation_model, request_dict)
    
        self.meta.events.emit(
            'after-call.{endpoint_prefix}.{operation_name}'.format(
                endpoint_prefix=self._service_model.endpoint_prefix,
                operation_name=operation_name),
            http_response=http, parsed=parsed_response,
            model=operation_model, context=request_context
        )
    
        if http.status_code >= 300:
            error_code = parsed_response.get("Error", {}).get("Code")
            error_class = self.exceptions.from_code(error_code)
>           raise error_class(parsed_response, operation_name)
E           botocore.errorfactory.ConditionalCheckFailedException: An error occurred (ConditionalCheckFailedException) when calling the PutItem operation:

/home/marty/development/redacted/virtualenv/lib/python3.6/site-packages/botocore/client.py:601: ConditionalCheckFailedException
============================================================ 1 failed in 0.34 seconds =============================================================
```

## Suggested changes
At lines https://github.com/spulec/moto/blob/f6fc9c7d54307f77d0cbbf698bab0c3be55ec153/moto/dynamodb2/models.py#L263-L265 perform error checking to ensure the key specified by `self.hash_key_attr` and `self.range_key_attr` are within the `item_attrs` dict before accessing them. Raise a validation exception if they are not present, catching this at https://github.com/spulec/moto/blob/f6fc9c7d54307f77d0cbbf698bab0c3be55ec153/moto/dynamodb2/responses.py#L207-L212 and raising the appropriate AWS error with debug information.

If you like this idea, I'm happy to do a PR :+1: 

## Moto Version
1.0.1

## Other stuff
This bug report is related to #873 where the HASH key was not defined properly, resulting in a misleading 'ConditionalException', I believe this was a similar issue, but in that case `self.hash_key_attr` was undefined/None and thus a ValueError was thrown similarly.
https://github.com/spulec/moto/issues/873

Changed files:
moto/dynamodb2/models.py, moto/dynamodb2/responses.py, tests/test_dynamodb2/test_dynamodb.py

diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py
index 8e5a61755..1527821ed 100644
--- a/moto/dynamodb2/models.py
+++ b/moto/dynamodb2/models.py
@@ -792,6 +792,12 @@ class Table(BaseModel):
         expression_attribute_values=None,
         overwrite=False,
     ):
+        if self.hash_key_attr not in item_attrs.keys():
+            raise ValueError(
+                "One or more parameter values were invalid: Missing the key "
+                + self.hash_key_attr
+                + " in the item"
+            )
         hash_value = DynamoType(item_attrs.get(self.hash_key_attr))
         if self.has_range_key:
             range_value = DynamoType(item_attrs.get(self.range_key_attr))
@@ -808,7 +814,6 @@ class Table(BaseModel):
             else:
                 lookup_range_value = DynamoType(expected_range_value)
         current = self.get_item(hash_value, lookup_range_value)
-
         item = Item(
             hash_value, self.hash_key_type, range_value, self.range_key_type, item_attrs
         )
diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py
index d3767c3fd..9bcb0d541 100644
--- a/moto/dynamodb2/responses.py
+++ b/moto/dynamodb2/responses.py
@@ -293,11 +293,9 @@ class DynamoHandler(BaseResponse):
         except ItemSizeTooLarge:
             er = "com.amazonaws.dynamodb.v20111205#ValidationException"
             return self.error(er, ItemSizeTooLarge.message)
-        except ValueError:
+        except ValueError as ve:
             er = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException"
-            return self.error(
-                er, "A condition specified in the operation could not be evaluated."
-            )
+            return self.error(er, str(ve))
 
         if result:
             item_dict = result.to_json()
diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py
index 428b58f81..24e0626c9 100644
--- a/tests/test_dynamodb2/test_dynamodb.py
+++ b/tests/test_dynamodb2/test_dynamodb.py
@@ -1344,6 +1344,24 @@ def test_get_item_returns_consumed_capacity():
     assert "TableName" in response["ConsumedCapacity"]
 
 
+@mock_dynamodb2
+def test_put_item_nonexisting_hash_key():
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+    dynamodb.create_table(
+        AttributeDefinitions=[{"AttributeName": "structure_id", "AttributeType": "S"},],
+        TableName="test",
+        KeySchema=[{"AttributeName": "structure_id", "KeyType": "HASH"},],
+        ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
+    )
+    table = dynamodb.Table("test")
+
+    with assert_raises(ClientError) as ex:
+        table.put_item(Item={"a_terribly_misguided_id_attribute": "abcdef"})
+    ex.exception.response["Error"]["Message"].should.equal(
+        "One or more parameter values were invalid: Missing the key structure_id in the item"
+    )
+
+
 def test_filter_expression():
     row1 = moto.dynamodb2.models.Item(
         None,