Repository: getmoto/moto
Pull request #7557 (2024-04-02 20:28:34)

Events: put_events should check if detail type, detail or source are empty
As discussed on #7532 



rafcio19: LGTM! The tests could be simplified by using pytest's [parametrize](https://docs.pytest.org/en/7.1.x/how-to/parametrize.html) - a single test with the  payload dict passed to `Entries` as parameter. 

See a straight forward example:

https://github.com/getmoto/moto/blob/fc5421a78d51711ed6b0dfb5f0b27f40a845fa60/tests/test_batch/test_batch.py#L7
fernando-alves: @rafcio19 I've updated the test, is this what you had in mind?
rafcio19: Looks great, but needs to be approved by the boss @bblommers 

Patch:
diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py
index 20dd2d314..e8e8d1569 100644
--- a/moto/dynamodb/responses.py
+++ b/moto/dynamodb/responses.py
@@ -526,7 +526,12 @@ class DynamoHandler(BaseResponse):
                 elif request_type == "DeleteRequest":
                     keys = request["Key"]
                     delete_requests.append((table_name, keys))
-
+        if self._contains_duplicates(
+            [json.dumps(k[1]) for k in delete_requests]
+        ) or self._contains_duplicates([json.dumps(k[1]) for k in put_requests]):
+            raise MockValidationException(
+                "Provided list of item keys contains duplicates"
+            )
         for table_name, item in put_requests:
             self.dynamodb_backend.put_item(table_name, item)
         for table_name, keys in delete_requests:
diff --git a/moto/events/models.py b/moto/events/models.py
index 05261a5a6..72fbd859b 100644
--- a/moto/events/models.py
+++ b/moto/events/models.py
@@ -1301,21 +1301,21 @@ class EventsBackend(BaseBackend):
 
         entries = []
         for event in events:
-            if "Source" not in event:
+            if not event.get("Source"):
                 entries.append(
                     {
                         "ErrorCode": "InvalidArgument",
                         "ErrorMessage": "Parameter Source is not valid. Reason: Source is a required argument.",
                     }
                 )
-            elif "DetailType" not in event:
+            elif not event.get("DetailType"):
                 entries.append(
                     {
                         "ErrorCode": "InvalidArgument",
                         "ErrorMessage": "Parameter DetailType is not valid. Reason: DetailType is a required argument.",
                     }
                 )
-            elif "Detail" not in event:
+            elif not event.get("Detail"):
                 entries.append(
                     {
                         "ErrorCode": "InvalidArgument",


Test patch:
diff --git a/tests/test_dynamodb/conftest.py b/tests/test_dynamodb/conftest.py
index 04d975c2b..8c4915154 100644
--- a/tests/test_dynamodb/conftest.py
+++ b/tests/test_dynamodb/conftest.py
@@ -1,9 +1,61 @@
+import os
+
+import boto3
 import pytest
 
+from moto import mock_aws
 from moto.core import DEFAULT_ACCOUNT_ID
 from moto.dynamodb.models import Table
 
 
+# pylint: disable=redefined-outer-name
+@pytest.fixture(scope="function")
+def aws_credentials():
+    """Mocked AWS Credentials for moto."""
+    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"
+    os.environ["AWS_DEFAULT_REGION"] = "us-east-1"
+
+
+@pytest.fixture(scope="function")
+def ddb_client(aws_credentials):
+    with mock_aws():
+        yield boto3.client("dynamodb")
+
+
+@pytest.fixture(scope="function")
+def ddb_resource(aws_credentials):
+    with mock_aws():
+        yield boto3.resource("dynamodb")
+
+
+@pytest.fixture
+def users_table_name():
+    return "users"
+
+
+@pytest.fixture
+def create_user_table(ddb_client, users_table_name):
+    ddb_client.create_table(
+        TableName=users_table_name,
+        KeySchema=[{"AttributeName": "username", "KeyType": "HASH"}],
+        AttributeDefinitions=[{"AttributeName": "username", "AttributeType": "S"}],
+        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
+    )
+    ddb_client.put_item(
+        TableName="users", Item={"username": {"S": "user1"}, "binaryfoo": {"B": b"bar"}}
+    )
+    ddb_client.put_item(
+        TableName="users", Item={"username": {"S": "user2"}, "foo": {"S": "bar"}}
+    )
+    ddb_client.put_item(
+        TableName="users", Item={"username": {"S": "user3"}, "foo": {"S": "bar"}}
+    )
+    return ddb_client
+
+
 @pytest.fixture
 def table():
     return Table(
diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
index a29f15e09..a8ecd2224 100644
--- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
+++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
@@ -746,6 +746,35 @@ def test_update_expression_with_trailing_comma():
     )
 
 
+@mock_aws
+def test_batch_items_should_throw_exception_for_duplicate_request(
+    ddb_resource, create_user_table, users_table_name
+):
+    # Setup
+    table = ddb_resource.Table(users_table_name)
+    test_item = {"forum_name": "test_dupe", "subject": "test1"}
+
+    # Execute
+    with pytest.raises(ClientError) as ex:
+        with table.batch_writer() as batch:
+            for i in range(0, 5):
+                batch.put_item(Item=test_item)
+
+    with pytest.raises(ClientError) as ex2:
+        with table.batch_writer() as batch:
+            for i in range(0, 5):
+                batch.delete_item(Key=test_item)
+
+    # Verify
+    err = ex.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert err["Message"] == "Provided list of item keys contains duplicates"
+
+    err2 = ex2.value.response["Error"]
+    assert err2["Code"] == "ValidationException"
+    assert err2["Message"] == "Provided list of item keys contains duplicates"
+
+
 @mock_aws
 def test_batch_put_item_with_empty_value():
     ddb = boto3.resource("dynamodb", region_name="us-east-1")
diff --git a/tests/test_dynamodb/test_dynamodb_batch_get_item.py b/tests/test_dynamodb/test_dynamodb_batch_get_item.py
index 06d639c6f..f4afadcf4 100644
--- a/tests/test_dynamodb/test_dynamodb_batch_get_item.py
+++ b/tests/test_dynamodb/test_dynamodb_batch_get_item.py
@@ -1,33 +1,12 @@
-import boto3
 import pytest
 from botocore.exceptions import ClientError
 
 from moto import mock_aws
 
 
-def _create_user_table():
-    client = boto3.client("dynamodb", region_name="us-east-1")
-    client.create_table(
-        TableName="users",
-        KeySchema=[{"AttributeName": "username", "KeyType": "HASH"}],
-        AttributeDefinitions=[{"AttributeName": "username", "AttributeType": "S"}],
-        ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
-    )
-    client.put_item(
-        TableName="users", Item={"username": {"S": "user1"}, "binaryfoo": {"B": b"bar"}}
-    )
-    client.put_item(
-        TableName="users", Item={"username": {"S": "user2"}, "foo": {"S": "bar"}}
-    )
-    client.put_item(
-        TableName="users", Item={"username": {"S": "user3"}, "foo": {"S": "bar"}}
-    )
-    return client
-
-
 @mock_aws
-def test_batch_items_returns_all():
-    dynamodb = _create_user_table()
+def test_batch_items_returns_all(create_user_table):
+    dynamodb = create_user_table
     returned_items = dynamodb.batch_get_item(
         RequestItems={
             "users": {
@@ -48,8 +27,10 @@ def test_batch_items_returns_all():
 
 
 @mock_aws
-def test_batch_items_throws_exception_when_requesting_100_items_for_single_table():
-    dynamodb = _create_user_table()
+def test_batch_items_throws_exception_when_requesting_100_items_for_single_table(
+    create_user_table,
+):
+    dynamodb = create_user_table
     with pytest.raises(ClientError) as ex:
         dynamodb.batch_get_item(
             RequestItems={
@@ -70,8 +51,10 @@ def test_batch_items_throws_exception_when_requesting_100_items_for_single_table
 
 
 @mock_aws
-def test_batch_items_throws_exception_when_requesting_100_items_across_all_tables():
-    dynamodb = _create_user_table()
+def test_batch_items_throws_exception_when_requesting_100_items_across_all_tables(
+    create_user_table,
+):
+    dynamodb = create_user_table
     with pytest.raises(ClientError) as ex:
         dynamodb.batch_get_item(
             RequestItems={
@@ -95,8 +78,8 @@ def test_batch_items_throws_exception_when_requesting_100_items_across_all_table
 
 
 @mock_aws
-def test_batch_items_with_basic_projection_expression():
-    dynamodb = _create_user_table()
+def test_batch_items_with_basic_projection_expression(create_user_table):
+    dynamodb = create_user_table
     returned_items = dynamodb.batch_get_item(
         RequestItems={
             "users": {
@@ -142,8 +125,10 @@ def test_batch_items_with_basic_projection_expression():
 
 
 @mock_aws
-def test_batch_items_with_basic_projection_expression_and_attr_expression_names():
-    dynamodb = _create_user_table()
+def test_batch_items_with_basic_projection_expression_and_attr_expression_names(
+    create_user_table,
+):
+    dynamodb = create_user_table
     returned_items = dynamodb.batch_get_item(
         RequestItems={
             "users": {
@@ -167,8 +152,8 @@ def test_batch_items_with_basic_projection_expression_and_attr_expression_names(
 
 
 @mock_aws
-def test_batch_items_should_throw_exception_for_duplicate_request():
-    client = _create_user_table()
+def test_batch_items_should_throw_exception_for_duplicate_request(create_user_table):
+    client = create_user_table
     with pytest.raises(ClientError) as ex:
         client.batch_get_item(
             RequestItems={
@@ -187,7 +172,7 @@ def test_batch_items_should_throw_exception_for_duplicate_request():
 
 
 @mock_aws
-def test_batch_items_should_return_16mb_max():
+def test_batch_items_should_return_16mb_max(create_user_table):
     """
     A single operation can retrieve up to 16 MB of data [...]. BatchGetItem returns a partial result if the response size limit is exceeded [..].
 
@@ -197,7 +182,7 @@ def test_batch_items_should_return_16mb_max():
     It also returns an appropriate UnprocessedKeys value so you can get the next page of results.
     If desired, your application can include its own logic to assemble the pages of results into one dataset.
     """
-    client = _create_user_table()
+    client = create_user_table
     # Fill table with all the data
     for i in range(100):
         client.put_item(
diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py
index 9c8d2d422..2e28caff4 100644
--- a/tests/test_events/test_events.py
+++ b/tests/test_events/test_events.py
@@ -720,53 +720,30 @@ def test_put_events_error_too_many_entries():
 
 
 @mock_aws
-def test_put_events_error_missing_argument_source():
-    # given
-    client = boto3.client("events", "eu-central-1")
-
-    # when
-    response = client.put_events(Entries=[{}])
-
-    # then
-    assert response["FailedEntryCount"] == 1
-    assert len(response["Entries"]) == 1
-    assert response["Entries"][0] == {
-        "ErrorCode": "InvalidArgument",
-        "ErrorMessage": "Parameter Source is not valid. Reason: Source is a required argument.",
-    }
-
-
-@mock_aws
-def test_put_events_error_missing_argument_detail_type():
-    # given
-    client = boto3.client("events", "eu-central-1")
-
-    # when
-    response = client.put_events(Entries=[{"Source": "source"}])
-
-    # then
-    assert response["FailedEntryCount"] == 1
-    assert len(response["Entries"]) == 1
-    assert response["Entries"][0] == {
-        "ErrorCode": "InvalidArgument",
-        "ErrorMessage": "Parameter DetailType is not valid. Reason: DetailType is a required argument.",
-    }
-
-
-@mock_aws
-def test_put_events_error_missing_argument_detail():
+@pytest.mark.parametrize(
+    "argument,entries",
+    [
+        ("Source", [{}]),
+        ("Source", [{"Source": ""}]),
+        ("DetailType", [{"Source": "source"}]),
+        ("DetailType", [{"Source": "source", "DetailType": ""}]),
+        ("Detail", [{"Source": "source", "DetailType": "type"}]),
+        ("Detail", [{"Source": "source", "DetailType": "typee", "Detail": ""}]),
+    ],
+)
+def test_put_events_error_missing_or_empty_required_argument(argument, entries):
     # given
     client = boto3.client("events", "eu-central-1")
 
     # when
-    response = client.put_events(Entries=[{"DetailType": "type", "Source": "source"}])
+    response = client.put_events(Entries=entries)
 
     # then
     assert response["FailedEntryCount"] == 1
     assert len(response["Entries"]) == 1
     assert response["Entries"][0] == {
         "ErrorCode": "InvalidArgument",
-        "ErrorMessage": "Parameter Detail is not valid. Reason: Detail is a required argument.",
+        "ErrorMessage": f"Parameter {argument} is not valid. Reason: {argument} is a required argument.",
     }