Repository: getmoto/moto

Issue:
DynamoDB put_item doesn't raise ValidationException correctly
```
from moto import mock_dynamodb2
import boto3

@mock_dynamodb2
def test():
    os.environ['AWS_DEFAULT_REGION'] = 'eu-west-1'
    os.environ['AWS_ACCESS_KEY_ID'] = 'testing'
    os.environ['AWS_SECRET_ACCESS_KEY'] = 'testing'
    os.environ['AWS_SECURITY_TOKEN'] = 'testing'
    os.environ['AWS_SESSION_TOKEN'] = 'testing'

    client = boto3.client('dynamodb')
    resource = boto3.resource('dynamodb')

    response = client.create_table(
        AttributeDefinitions=[
            {'AttributeName': 'job_id', 'AttributeType': 'S'},
            {'AttributeName': 'started_at', 'AttributeType': 'N'}
        ],
        TableName='ec2_job_status',
        KeySchema=[
            {'AttributeName': 'job_id', 'KeyType': 'HASH'},
            {'AttributeName': 'started_at', 'KeyType': 'RANGE'}
        ],
        BillingMode='PAY_PER_REQUEST',
        StreamSpecification={'StreamEnabled': False}
    )
    
    item = {
        'job_id': str(uuid4()),
    }

    table = resource.Table('ec2_job_status')
    table.put_item(Item=item)

def test()
```
The code above should raise `ValidationException` because I am not passing the range key attribute but instead raises a `TypeError: 'NoneType' object is not subscriptable`.

Likewise, using the same DynamoDB table as above but inserting this
```
item = {}

table = resource.Table('ec2_job_status')
table.put_item(Item=item)
```
or this
```
item = {
  'started_at': 123
}

table = resource.Table('ec2_job_status')
table.put_item(Item=item)
```
Should also raise `ValidationException` but instead a `ConditionalCheckFailedException` is risen in both cases.

I think I have an idea of where the issue is, would a PR be welcome?


Discussion:
Thanks for raising this, @dreamorosi! PR's are always welcome

Resolving patch:
diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py
--- a/moto/dynamodb2/models.py
+++ b/moto/dynamodb2/models.py
@@ -800,13 +800,19 @@ def put_item(
         overwrite=False,
     ):
         if self.hash_key_attr not in item_attrs.keys():
-            raise ValueError(
+            raise KeyError(
                 "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:
+            if self.range_key_attr not in item_attrs.keys():
+                raise KeyError(
+                    "One or more parameter values were invalid: Missing the key "
+                    + self.range_key_attr
+                    + " in the item"
+                )
             range_value = DynamoType(item_attrs.get(self.range_key_attr))
         else:
             range_value = None
diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py
--- a/moto/dynamodb2/responses.py
+++ b/moto/dynamodb2/responses.py
@@ -299,6 +299,9 @@ def put_item(self):
         except ItemSizeTooLarge:
             er = "com.amazonaws.dynamodb.v20111205#ValidationException"
             return self.error(er, ItemSizeTooLarge.message)
+        except KeyError as ke:
+            er = "com.amazonaws.dynamodb.v20111205#ValidationException"
+            return self.error(er, ke.args[0])
         except ValueError as ve:
             er = "com.amazonaws.dynamodb.v20111205#ConditionalCheckFailedException"
             return self.error(er, str(ve))


Test patch:
diff --git a/tests/test_dynamodb2/test_dynamodb.py b/tests/test_dynamodb2/test_dynamodb.py
--- a/tests/test_dynamodb2/test_dynamodb.py
+++ b/tests/test_dynamodb2/test_dynamodb.py
@@ -1345,6 +1345,25 @@ def test_get_item_returns_consumed_capacity():
     assert "TableName" in response["ConsumedCapacity"]
 
 
+@mock_dynamodb2
+def test_put_empty_item():
+    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={})
+    ex.exception.response["Error"]["Message"].should.equal(
+        "One or more parameter values were invalid: Missing the key structure_id in the item"
+    )
+    ex.exception.response["Error"]["Code"].should.equal("ValidationException")
+
+
 @mock_dynamodb2
 def test_put_item_nonexisting_hash_key():
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
@@ -1361,6 +1380,32 @@ def test_put_item_nonexisting_hash_key():
     ex.exception.response["Error"]["Message"].should.equal(
         "One or more parameter values were invalid: Missing the key structure_id in the item"
     )
+    ex.exception.response["Error"]["Code"].should.equal("ValidationException")
+
+
+@mock_dynamodb2
+def test_put_item_nonexisting_range_key():
+    dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
+    dynamodb.create_table(
+        AttributeDefinitions=[
+            {"AttributeName": "structure_id", "AttributeType": "S"},
+            {"AttributeName": "added_at", "AttributeType": "N"},
+        ],
+        TableName="test",
+        KeySchema=[
+            {"AttributeName": "structure_id", "KeyType": "HASH"},
+            {"AttributeName": "added_at", "KeyType": "RANGE"},
+        ],
+        ProvisionedThroughput={"ReadCapacityUnits": 123, "WriteCapacityUnits": 123},
+    )
+    table = dynamodb.Table("test")
+
+    with assert_raises(ClientError) as ex:
+        table.put_item(Item={"structure_id": "abcdef"})
+    ex.exception.response["Error"]["Message"].should.equal(
+        "One or more parameter values were invalid: Missing the key added_at in the item"
+    )
+    ex.exception.response["Error"]["Code"].should.equal("ValidationException")
 
 
 def test_filter_expression():