Repository: getmoto/moto (python)

Issue:
DynamoDB should return a ValidationError if you try to put an item with an empty string set
## Summary
I recently discovered a difference between moto and DynamoDB behavior - moto will accept an item with an empty String Set, while DynamoDB will return a ValidationError. Here's a quick TestCase to reproduce:

```python
from unittest import TestCase

import boto3
from botocore.exceptions import ClientError
from moto import mock_aws


@mock_aws
class TestEmptyStringSet(TestCase):
    """
    DynamoDB will return a ValidationError, if you try to put an empty string set. This can be confirmed by running this
    test against a real AWS account, without the @mock_aws decorator.
    """
    def test_client_error_on_empty_string_set(self):
        """
        Should raise Client error:
        ```
        botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the PutItem operation: One
        or more parameter values were invalid: An string set  may not be empty
        ```
        """
        with self.assertRaises(ClientError) as e:
            self._client.put_item(
                TableName=self._table_name,
                Item={
                    'pk': {'S': 'foo'},
                    'stringSet': {'SS': []}
                }
            )

        self.assertEqual(e.exception.response['Error']['Code'], 'ValidationException')

    def setUp(self):
        self._table_name = 'example-test-table'
        self._client = boto3.client('dynamodb')
        self._table = boto3.resource('dynamodb').create_table(
            AttributeDefinitions=[
                {
                    'AttributeName': 'pk',
                    'AttributeType': 'S'
                }
            ],
            TableName=self._table_name,
            KeySchema=[
                {
                    'AttributeName': 'pk',
                    'KeyType': 'HASH'
                }
            ],
            BillingMode='PAY_PER_REQUEST',
        )
        waiter = self._client.get_waiter('table_exists')
        waiter.wait(TableName=self._table_name)

    def tearDown(self):
        self._table.delete()
        waiter = self._client.get_waiter('table_not_exists')
        waiter.wait(TableName=self._table_name)
```

Pull request description:
DynamoDB: update_item() now validates empty string sets
Fixes #8022 

Patch:
diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py
index dd2e3c36e..b0141dfba 100644
--- a/moto/dynamodb/responses.py
+++ b/moto/dynamodb/responses.py
@@ -122,23 +122,24 @@ def validate_put_has_empty_keys(
             raise MockValidationException(msg.format(empty_key))
 
 
-def put_has_empty_attrs(field_updates: Dict[str, Any], table: Table) -> bool:
+def validate_put_has_empty_attrs(field_updates: Dict[str, Any], table: Table) -> None:
     # Example invalid attribute: [{'M': {'SS': {'NS': []}}}]
-    def _validate_attr(attr: Dict[str, Any]) -> bool:
-        if "NS" in attr and attr["NS"] == []:
-            return True
+    def _validate_attr(attr: Dict[str, Any]) -> None:
+        for set_type, error in [("NS", "number"), ("SS", "string")]:
+            if set_type in attr and attr[set_type] == []:
+                raise MockValidationException(
+                    f"One or more parameter values were invalid: An {error} set  may not be empty"
+                )
+
         else:
-            return any(
-                [_validate_attr(val) for val in attr.values() if isinstance(val, dict)]
-            )
+            for val in attr.values():
+                if isinstance(val, dict):
+                    _validate_attr(val)
 
     if table:
-        key_names = table.attribute_keys
-        attrs_to_check = [
-            val for attr, val in field_updates.items() if attr not in key_names
-        ]
-        return any([_validate_attr(attr) for attr in attrs_to_check])
-    return False
+        for attr, val in field_updates.items():
+            if attr not in table.attribute_keys:
+                _validate_attr(val)
 
 
 def validate_put_has_gsi_keys_set_to_none(item: Dict[str, Any], table: Table) -> None:
@@ -475,10 +476,7 @@ class DynamoHandler(BaseResponse):
 
         table = self.dynamodb_backend.get_table(name)
         validate_put_has_empty_keys(item, table)
-        if put_has_empty_attrs(item, table):
-            raise MockValidationException(
-                "One or more parameter values were invalid: An number set  may not be empty"
-            )
+        validate_put_has_empty_attrs(item, table)
         validate_put_has_gsi_keys_set_to_none(item, table)
 
         overwrite = "Expected" not in self.body


Test patch:
diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
index 64cca0b23..93385c708 100644
--- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
+++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
@@ -694,19 +694,15 @@ def test_put_item_wrong_datatype():
     assert err["Message"] == "NUMBER_VALUE cannot be converted to String"
 
 
-@mock_aws
-def test_put_item_empty_set():
+@dynamodb_aws_verified()
+@pytest.mark.aws_verified
+def test_put_item_empty_set(table_name=None):
     client = boto3.client("dynamodb", region_name="us-east-1")
     dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
-    client.create_table(
-        TableName="test-table",
-        KeySchema=[{"AttributeName": "Key", "KeyType": "HASH"}],
-        AttributeDefinitions=[{"AttributeName": "Key", "AttributeType": "S"}],
-        BillingMode="PAY_PER_REQUEST",
-    )
-    table = dynamodb.Table("test-table")
+
+    table = dynamodb.Table(table_name)
     with pytest.raises(ClientError) as exc:
-        table.put_item(Item={"Key": "some-irrelevant_key", "attr2": {"SS": set([])}})
+        table.put_item(Item={"pk": "some-irrelevant_key", "attr2": {"SS": set([])}})
     err = exc.value.response["Error"]
     assert err["Code"] == "ValidationException"
     assert (
@@ -714,6 +710,17 @@ def test_put_item_empty_set():
         == "One or more parameter values were invalid: An number set  may not be empty"
     )
 
+    with pytest.raises(ClientError) as exc:
+        client.put_item(
+            TableName=table_name, Item={"pk": {"S": "foo"}, "stringSet": {"SS": []}}
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationException"
+    assert (
+        err["Message"]
+        == "One or more parameter values were invalid: An string set  may not be empty"
+    )
+
 
 @mock_aws
 def test_put_item_returns_old_item():