Repository: getmoto/moto (python)

Issue:
DynamoDB Scan does not support segments
I am making a Lambda which splits a scan into multiple segments for "threaded" processing, and hoped to test the logic with Moto before trying it for real. It seems that Moto doesn't understand the concept, however.

AWS documentation:
https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Scan.html#Scan.ParallelScan

Moto implementation:
https://github.com/getmoto/moto/blob/12cd1868b5e725b8201714f30d5ddfd0324fd141/moto/dynamodb/responses.py#L833

Pull request:
DynamoDB: scan() now supports parallelization using the Segment/TotalSegments parameters
Fixes #8171 

Patch:
diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py
index b6c823cd9..d3d1a71e8 100644
--- a/moto/dynamodb/models/__init__.py
+++ b/moto/dynamodb/models/__init__.py
@@ -401,6 +401,7 @@ class DynamoDBBackend(BaseBackend):
         index_name: str,
         consistent_read: bool,
         projection_expression: Optional[List[List[str]]],
+        segments: Union[Tuple[None, None], Tuple[int, int]],
     ) -> Tuple[List[Item], int, Optional[Dict[str, Any]]]:
         table = self.get_table(table_name)
 
@@ -421,6 +422,7 @@ class DynamoDBBackend(BaseBackend):
             index_name,
             consistent_read,
             projection_expression,
+            segments=segments,
         )
 
     def update_item(
diff --git a/moto/dynamodb/models/dynamo_type.py b/moto/dynamodb/models/dynamo_type.py
index 62f87356f..3aa92fd53 100644
--- a/moto/dynamodb/models/dynamo_type.py
+++ b/moto/dynamodb/models/dynamo_type.py
@@ -1,7 +1,7 @@
 import base64
 import copy
 from decimal import Decimal
-from typing import Any, Dict, List, Optional, Union
+from typing import Any, Dict, List, Optional, Tuple, Union
 
 from boto3.dynamodb.types import TypeDeserializer, TypeSerializer
 from botocore.utils import merge_dicts
@@ -12,6 +12,7 @@ from moto.dynamodb.exceptions import (
     IncorrectDataType,
     ItemSizeTooLarge,
 )
+from moto.utilities.utils import md5_hash
 
 from .utilities import bytesize, find_nested_key
 
@@ -455,3 +456,31 @@ class Item(BaseModel):
             # We need to convert that into DynamoDB dictionary ({'M': {'key': {'S': 'value'}}})
             attrs=serializer.serialize(result)["M"],
         )
+
+    def is_within_segment(
+        self, segments: Union[Tuple[None, None], Tuple[int, int]]
+    ) -> bool:
+        """
+        Segments can be either (x, y) or (None, None)
+        None, None => the user requested the entire table, so the item always falls within that
+        x, y       => the user requested segment x out of y
+
+        Segment membership is computed based on the value of the hash key
+        """
+        if segments == (None, None):
+            return True
+
+        segment, total_segments = segments
+        # Creates a reproducible hash number for this item (between 0 and 256)
+        # Note that we can't use the builtin hash() method, as that is not deterministic between executions
+        #
+        # Using a hash based on the hash key ensures parity with how AWS seems to behave:
+        #  - Items are not divided equally between segment
+        #  - Items always fall in the same segment, regardless of how often you call `scan()`
+        #  - Items with the same hash key but different range keys always fall in the same segment
+        #  - Items with different hash keys may be part of different segments
+        #
+        item_hash = md5_hash(self.hash_key.value.encode("utf8")).digest()[0]
+        # Modulo ensures that we always get a number between 0 and (total_segments)
+        item_segment = item_hash % total_segments
+        return segment == item_segment
diff --git a/moto/dynamodb/models/table.py b/moto/dynamodb/models/table.py
index bd074fe05..73d382609 100644
--- a/moto/dynamodb/models/table.py
+++ b/moto/dynamodb/models/table.py
@@ -1,6 +1,6 @@
 import copy
 from collections import defaultdict
-from typing import Any, Dict, List, Optional, Sequence, Tuple
+from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
 
 from moto.core.common_models import BaseModel, CloudFormationModel
 from moto.core.utils import unix_time, unix_time_millis, utcnow
@@ -897,6 +897,7 @@ class Table(CloudFormationModel):
         index_name: Optional[str] = None,
         consistent_read: bool = False,
         projection_expression: Optional[List[List[str]]] = None,
+        segments: Union[Tuple[None, None], Tuple[int, int]] = (None, None),
     ) -> Tuple[List[Item], int, Optional[Dict[str, Any]]]:
         results: List[Item] = []
         result_size = 0
@@ -942,6 +943,9 @@ class Table(CloudFormationModel):
         last_evaluated_key = None
         processing_previous_page = exclusive_start_key is not None
         for item in items:
+            if not item.is_within_segment(segments):
+                continue
+
             # Cycle through the previous page of results
             # When we encounter our start key, we know we've reached the end of the previous page
             if processing_previous_page:
diff --git a/moto/dynamodb/responses.py b/moto/dynamodb/responses.py
index cb1f777f2..c79331afc 100644
--- a/moto/dynamodb/responses.py
+++ b/moto/dynamodb/responses.py
@@ -829,6 +829,24 @@ class DynamoHandler(BaseResponse):
         limit = self.body.get("Limit")
         index_name = self.body.get("IndexName")
         consistent_read = self.body.get("ConsistentRead", False)
+        segment = self.body.get("Segment")
+        total_segments = self.body.get("TotalSegments")
+        if segment is not None and total_segments is None:
+            raise MockValidationException(
+                "The TotalSegments parameter is required but was not present in the request when Segment parameter is present"
+            )
+        if total_segments is not None and segment is None:
+            raise MockValidationException(
+                "The Segment parameter is required but was not present in the request when parameter TotalSegments is present"
+            )
+        if (
+            segment is not None
+            and total_segments is not None
+            and segment >= total_segments
+        ):
+            raise MockValidationException(
+                f"The Segment parameter is zero-based and must be less than parameter TotalSegments: Segment: {segment} is not less than TotalSegments: {total_segments}"
+            )
 
         projection_expressions = self._adjust_projection_expression(
             projection_expression, expression_attribute_names
@@ -840,12 +858,13 @@ class DynamoHandler(BaseResponse):
                 filters,
                 limit,
                 exclusive_start_key,
-                filter_expression,
-                expression_attribute_names,
-                expression_attribute_values,
-                index_name,
-                consistent_read,
-                projection_expressions,
+                filter_expression=filter_expression,
+                expr_names=expression_attribute_names,
+                expr_values=expression_attribute_values,
+                index_name=index_name,
+                consistent_read=consistent_read,
+                projection_expression=projection_expressions,
+                segments=(segment, total_segments),
             )
         except ValueError as err:
             raise MockValidationException(f"Bad Filter Expression: {err}")
diff --git a/moto/ssm/resources/regions.json b/moto/ssm/resources/regions.json
index 16c511b94..ba206eab9 100644
--- a/moto/ssm/resources/regions.json
+++ b/moto/ssm/resources/regions.json
@@ -5342,15 +5342,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "opsworkscm": {
-                "Value": "opsworkscm",
-                "endpoint": {
-                  "Value": "opsworks-cm.ap-northeast-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
               "opsworkspuppetenterprise": {
                 "Value": "opsworkspuppetenterprise",
                 "endpoint": {
@@ -5699,15 +5690,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "sagemaker-edge": {
-                "Value": "sagemaker-edge",
-                "endpoint": {
-                  "Value": "edge.sagemaker.ap-northeast-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
               "sagemaker-featurestore-runtime": {
                 "Value": "sagemaker-featurestore-runtime",
                 "endpoint": {
@@ -22518,6 +22500,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "ds": {
+                "Value": "ds",
+                "endpoint": {
+                  "Value": "ds.ap-southeast-5.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "dynamodb": {
                 "Value": "dynamodb",
                 "endpoint": {
@@ -22665,6 +22656,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "fms": {
+                "Value": "fms",
+                "endpoint": {
+                  "Value": "fms.ap-southeast-5.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "iam": {
                 "Value": "iam",
                 "endpoint": {
@@ -27680,9 +27680,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "sagemaker-metrics": {
-                "Value": "sagemaker-metrics"
-              },
               "sagemaker-runtime": {
                 "Value": "sagemaker-runtime",
                 "endpoint": {
@@ -29067,9 +29064,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "sagemaker-metrics": {
-                "Value": "sagemaker-metrics"
-              },
               "sagemaker-runtime": {
                 "Value": "sagemaker-runtime",
                 "endpoint": {
@@ -29232,7 +29226,7 @@
               "support": {
                 "Value": "support",
                 "endpoint": {
-                  "Value": "support.cn-north-1.amazonaws.com.cn"
+                  "Value": "support.cn-northwest-1.amazonaws.com.cn"
                 },
                 "protocols": {
                   "Value": "HTTPS"
@@ -31635,15 +31629,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "sagemaker-edge": {
-                "Value": "sagemaker-edge",
-                "endpoint": {
-                  "Value": "edge.sagemaker.eu-central-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
               "sagemaker-featurestore-runtime": {
                 "Value": "sagemaker-featurestore-runtime",
                 "endpoint": {
@@ -32364,6 +32349,24 @@
                   "Value": "HTTPS"
                 }
               },
+              "bedrock": {
+                "Value": "bedrock",
+                "endpoint": {
+                  "Value": "bedrock.eu-central-2.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
+              "bedrock-runtime": {
+                "Value": "bedrock-runtime",
+                "endpoint": {
+                  "Value": "bedrock-runtime.eu-central-2.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "chatbot": {
                 "Value": "chatbot"
               },
@@ -33228,6 +33231,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "sagemaker-featurestore-runtime": {
+                "Value": "sagemaker-featurestore-runtime",
+                "endpoint": {
+                  "Value": "featurestore-runtime.sagemaker.eu-central-2.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "sagemaker-metrics": {
                 "Value": "sagemaker-metrics",
                 "endpoint": {
@@ -36041,6 +36053,12 @@
                   "Value": "HTTPS"
                 }
               },
+              "controlcatalog": {
+                "Value": "controlcatalog",
+                "endpoint": {
+                  "Value": "controlcatalog.eu-south-1.amazonaws.com"
+                }
+              },
               "controltower": {
                 "Value": "controltower",
                 "endpoint": {
@@ -40975,15 +40993,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "sagemaker-edge": {
-                "Value": "sagemaker-edge",
-                "endpoint": {
-                  "Value": "edge.sagemaker.eu-west-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
               "sagemaker-featurestore-runtime": {
                 "Value": "sagemaker-featurestore-runtime",
                 "endpoint": {
@@ -48715,15 +48724,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "sagemaker-metrics": {
-                "Value": "sagemaker-metrics",
-                "endpoint": {
-                  "Value": "metrics.sagemaker.me-central-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
               "sagemaker-runtime": {
                 "Value": "sagemaker-runtime",
                 "endpoint": {
@@ -55470,15 +55470,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "sagemaker-edge": {
-                "Value": "sagemaker-edge",
-                "endpoint": {
-                  "Value": "edge.sagemaker.us-east-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
               "sagemaker-featurestore-runtime": {
                 "Value": "sagemaker-featurestore-runtime",
                 "endpoint": {
@@ -58203,15 +58194,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "sagemaker-edge": {
-                "Value": "sagemaker-edge",
-                "endpoint": {
-                  "Value": "edge.sagemaker.us-east-2.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
               "sagemaker-featurestore-runtime": {
                 "Value": "sagemaker-featurestore-runtime",
                 "endpoint": {
@@ -58873,6 +58855,24 @@
                   "Value": "HTTPS"
                 }
               },
+              "bedrock": {
+                "Value": "bedrock",
+                "endpoint": {
+                  "Value": "bedrock.us-gov-east-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
+              "bedrock-runtime": {
+                "Value": "bedrock-runtime",
+                "endpoint": {
+                  "Value": "bedrock-runtime.us-gov-east-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "chime": {
                 "Value": "chime"
               },
@@ -66528,15 +66528,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "sagemaker-edge": {
-                "Value": "sagemaker-edge",
-                "endpoint": {
-                  "Value": "edge.sagemaker.us-west-2.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
               "sagemaker-featurestore-runtime": {
                 "Value": "sagemaker-featurestore-runtime",
                 "endpoint": {
diff --git a/moto/ssm/resources/services.json b/moto/ssm/resources/services.json
index 3d5d5cd5d..fcccde9d1 100644
--- a/moto/ssm/resources/services.json
+++ b/moto/ssm/resources/services.json
@@ -7929,6 +7929,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "eu-central-2": {
+                "Value": "eu-central-2",
+                "endpoint": {
+                  "Value": "bedrock.eu-central-2.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "eu-west-1": {
                 "Value": "eu-west-1",
                 "endpoint": {
@@ -7983,6 +7992,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "us-gov-east-1": {
+                "Value": "us-gov-east-1",
+                "endpoint": {
+                  "Value": "bedrock.us-gov-east-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "us-gov-west-1": {
                 "Value": "us-gov-west-1",
                 "endpoint": {
@@ -8072,6 +8090,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "eu-central-2": {
+                "Value": "eu-central-2",
+                "endpoint": {
+                  "Value": "bedrock-runtime.eu-central-2.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "eu-west-1": {
                 "Value": "eu-west-1",
                 "endpoint": {
@@ -8126,6 +8153,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "us-gov-east-1": {
+                "Value": "us-gov-east-1",
+                "endpoint": {
+                  "Value": "bedrock-runtime.us-gov-east-1.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "us-gov-west-1": {
                 "Value": "us-gov-west-1",
                 "endpoint": {
@@ -15893,6 +15929,12 @@
                 "endpoint": {
                   "Value": "controlcatalog.ap-southeast-5.amazonaws.com"
                 }
+              },
+              "eu-south-1": {
+                "Value": "eu-south-1",
+                "endpoint": {
+                  "Value": "controlcatalog.eu-south-1.amazonaws.com"
+                }
               }
             }
           },
@@ -19449,6 +19491,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "ap-southeast-5": {
+                "Value": "ap-southeast-5",
+                "endpoint": {
+                  "Value": "ds.ap-southeast-5.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "ca-central-1": {
                 "Value": "ca-central-1",
                 "endpoint": {
@@ -26535,6 +26586,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "ap-southeast-5": {
+                "Value": "ap-southeast-5",
+                "endpoint": {
+                  "Value": "fms.ap-southeast-5.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "ca-central-1": {
                 "Value": "ca-central-1",
                 "endpoint": {
@@ -46001,15 +46061,6 @@
               "Value": "AWS OpsWorks for Chef Automate"
             },
             "regions": {
-              "ap-northeast-1": {
-                "Value": "ap-northeast-1",
-                "endpoint": {
-                  "Value": "opsworks-cm.ap-northeast-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
               "ap-southeast-1": {
                 "Value": "ap-southeast-1",
                 "endpoint": {
@@ -55643,64 +55694,6 @@
               }
             }
           },
-          "sagemaker-edge": {
-            "regions": {
-              "ap-northeast-1": {
-                "Value": "ap-northeast-1",
-                "endpoint": {
-                  "Value": "edge.sagemaker.ap-northeast-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
-              "eu-central-1": {
-                "Value": "eu-central-1",
-                "endpoint": {
-                  "Value": "edge.sagemaker.eu-central-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
-              "eu-west-1": {
-                "Value": "eu-west-1",
-                "endpoint": {
-                  "Value": "edge.sagemaker.eu-west-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
-              "us-east-1": {
-                "Value": "us-east-1",
-                "endpoint": {
-                  "Value": "edge.sagemaker.us-east-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
-              "us-east-2": {
-                "Value": "us-east-2",
-                "endpoint": {
-                  "Value": "edge.sagemaker.us-east-2.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
-              "us-west-2": {
-                "Value": "us-west-2",
-                "endpoint": {
-                  "Value": "edge.sagemaker.us-west-2.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              }
-            }
-          },
           "sagemaker-featurestore-runtime": {
             "Value": "sagemaker-featurestore-runtime",
             "longName": {
@@ -55824,6 +55817,15 @@
                   "Value": "HTTPS"
                 }
               },
+              "eu-central-2": {
+                "Value": "eu-central-2",
+                "endpoint": {
+                  "Value": "featurestore-runtime.sagemaker.eu-central-2.amazonaws.com"
+                },
+                "protocols": {
+                  "Value": "HTTPS"
+                }
+              },
               "eu-north-1": {
                 "Value": "eu-north-1",
                 "endpoint": {
@@ -56083,12 +56085,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "cn-north-1": {
-                "Value": "cn-north-1"
-              },
-              "cn-northwest-1": {
-                "Value": "cn-northwest-1"
-              },
               "eu-central-1": {
                 "Value": "eu-central-1",
                 "endpoint": {
@@ -56170,15 +56166,6 @@
                   "Value": "HTTPS"
                 }
               },
-              "me-central-1": {
-                "Value": "me-central-1",
-                "endpoint": {
-                  "Value": "metrics.sagemaker.me-central-1.amazonaws.com"
-                },
-                "protocols": {
-                  "Value": "HTTPS"
-                }
-              },
               "me-south-1": {
                 "Value": "me-south-1",
                 "endpoint": {
@@ -64156,7 +64143,7 @@
               "cn-northwest-1": {
                 "Value": "cn-northwest-1",
                 "endpoint": {
-                  "Value": "support.cn-north-1.amazonaws.com.cn"
+                  "Value": "support.cn-northwest-1.amazonaws.com.cn"
                 },
                 "protocols": {
                   "Value": "HTTPS"
diff --git a/moto/stepfunctions/parser/api.py b/moto/stepfunctions/parser/api.py
index 579b0b395..436912ed3 100644
--- a/moto/stepfunctions/parser/api.py
+++ b/moto/stepfunctions/parser/api.py
@@ -268,10 +268,10 @@ class InvalidOutput(ServiceException):
 
 class InvalidToken(ServiceException):
     code: str = "InvalidToken"
-    exception_type: str = "UnrecognizedClientException"
+    exception_type: str = "InvalidToken"
     sender_fault: bool = False
     status_code: int = 400
-    message: str = "The security token included in the request is invalid."
+    message: str = "Invalid Token: 'Invalid token'"
 
     def __init__(self):
         super().__init__(self.message, self.exception_type, self.status_code)


Test patch:
diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
index e0bb4c38d..8861f36e7 100644
--- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
+++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py
@@ -33,28 +33,40 @@ table_schema = {
 
 class BaseTest:
     @classmethod
-    def setup_class(cls):
+    def setup_class(cls, add_range=False):
         if not allow_aws_request():
             cls.mock = mock_aws()
             cls.mock.start()
         cls.client = boto3.client("dynamodb", region_name="us-east-1")
         cls.table_name = "T" + str(uuid4())[0:6]
+        cls.has_range_key = add_range
 
         dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
 
         # Create the DynamoDB table.
+        schema = [{"AttributeName": "pk", "KeyType": "HASH"}]
+        defs = [{"AttributeName": "pk", "AttributeType": "S"}]
+        if add_range:
+            schema.append({"AttributeName": "rk", "KeyType": "RANGE"})
+            defs.append({"AttributeName": "rk", "AttributeType": "S"})
         dynamodb.create_table(
             TableName=cls.table_name,
-            KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}],
-            AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}],
+            KeySchema=schema,
+            AttributeDefinitions=defs,
             ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5},
         )
         waiter = cls.client.get_waiter("table_exists")
         waiter.wait(TableName=cls.table_name)
         cls.table = dynamodb.Table(cls.table_name)
-        cls.table.put_item(
-            Item={"pk": "the-key", "subject": "123", "body": "some test msg"}
-        )
+
+    def setup_method(self):
+        # Empty table between runs
+        items = self.table.scan()["Items"]
+        for item in items:
+            if self.has_range_key:
+                self.table.delete_item(Key={"pk": item["pk"], "rk": item["rk"]})
+            else:
+                self.table.delete_item(Key={"pk": item["pk"]})
 
     @classmethod
     def teardown_class(cls):
@@ -1296,6 +1308,12 @@ def test_query_with_missing_expression_attribute():
 
 @pytest.mark.aws_verified
 class TestReturnValuesOnConditionCheckFailure(BaseTest):
+    def setup_method(self):
+        super().setup_method()
+        self.table.put_item(
+            Item={"pk": "the-key", "subject": "123", "body": "some test msg"}
+        )
+
     def test_put_item_does_not_return_old_item(self):
         with pytest.raises(ClientError) as exc:
             self.table.put_item(
diff --git a/tests/test_dynamodb/test_dynamodb_scan.py b/tests/test_dynamodb/test_dynamodb_scan.py
index 1f070a9b4..0e9e77663 100644
--- a/tests/test_dynamodb/test_dynamodb_scan.py
+++ b/tests/test_dynamodb/test_dynamodb_scan.py
@@ -7,6 +7,7 @@ from boto3.dynamodb.conditions import Attr
 from botocore.exceptions import ClientError
 
 from moto import mock_aws
+from tests.test_dynamodb.exceptions.test_dynamodb_exceptions import BaseTest
 
 from . import dynamodb_aws_verified
 
@@ -729,3 +730,127 @@ class TestFilterExpression:
             "Items"
         ]
         assert items == [{"partitionKey": "pk-1"}]
+
+
+@pytest.mark.aws_verified
+class TestParallelScan(BaseTest):
+    @staticmethod
+    def setup_class(cls):  # pylint: disable=arguments-renamed
+        super().setup_class(add_range=True)
+
+    def test_segment_only(self):
+        with pytest.raises(ClientError) as exc:
+            self.table.scan(Segment=1)
+        err = exc.value.response["Error"]
+        assert err["Code"] == "ValidationException"
+        assert (
+            err["Message"]
+            == "The TotalSegments parameter is required but was not present in the request when Segment parameter is present"
+        )
+
+    def test_total_segments_only(self):
+        with pytest.raises(ClientError) as exc:
+            self.table.scan(TotalSegments=1)
+        err = exc.value.response["Error"]
+        assert err["Code"] == "ValidationException"
+        assert (
+            err["Message"]
+            == "The Segment parameter is required but was not present in the request when parameter TotalSegments is present"
+        )
+
+    def test_parallelize_all_different_hash_keys(self):
+        for i in range(10):
+            self.table.put_item(Item={"pk": f"item{i}", "rk": "sth"})
+
+        resp1 = self.table.scan(Segment=0, TotalSegments=3)["Items"]
+        resp2 = self.table.scan(Segment=1, TotalSegments=3)["Items"]
+        resp3 = self.table.scan(Segment=2, TotalSegments=3)["Items"]
+
+        assert len(resp1) + len(resp2) + len(resp3) == 10
+
+    def test_parallelize_different_hash_key_per_segment(self):
+        for i in range(3):
+            for j in range(4):
+                self.table.put_item(Item={"pk": f"item{i}", "rk": f"rk{j}"})
+
+        resp1 = self.table.scan(Segment=0, TotalSegments=3)["Items"]
+        resp2 = self.table.scan(Segment=1, TotalSegments=3)["Items"]
+        resp3 = self.table.scan(Segment=2, TotalSegments=3)["Items"]
+
+        assert len(resp1) + len(resp2) + len(resp3) == 12
+
+    def test_scan_using_filter_expression(self):
+        # AWS seems to return all data in Segment 1
+        for i in range(10):
+            self.table.put_item(Item={"pk": "item", "rk": f"range{i}"})
+        for i in range(10):
+            self.table.put_item(Item={"pk": "n/a", "rk": f"range{i}"})
+        for i in range(20, 10, -1):
+            self.table.put_item(Item={"pk": "item", "rk": f"range{i}"})
+
+        resp1 = self.table.scan(
+            FilterExpression=Attr("pk").eq("item"), Segment=0, TotalSegments=3
+        )["Items"]
+        resp2 = self.table.scan(
+            FilterExpression=Attr("pk").eq("item"), Segment=1, TotalSegments=3
+        )["Items"]
+        resp3 = self.table.scan(
+            FilterExpression=Attr("pk").eq("item"), Segment=2, TotalSegments=3
+        )["Items"]
+
+        assert len(resp1) + len(resp2) + len(resp3) == 20
+
+    def test_scan_single_hash_key(self):
+        # AWS seems to return all data in Segment 1
+        for i in range(10):
+            self.table.put_item(Item={"pk": "item", "rk": f"range{i}"})
+        for i in range(20, 10, -1):
+            self.table.put_item(Item={"pk": "item", "rk": f"range{i}"})
+
+        resp1 = self.table.scan(Segment=0, TotalSegments=3)["Items"]
+        resp2 = self.table.scan(Segment=1, TotalSegments=3)["Items"]
+        resp3 = self.table.scan(Segment=2, TotalSegments=3)["Items"]
+
+        assert len(resp1) + len(resp2) + len(resp3) == 20
+
+    def test_pagination(self):
+        for i in range(50):
+            self.table.put_item(Item={"pk": "item", "rk": f"range{i}"})
+
+        resp1 = self.table.scan(Segment=0, TotalSegments=3, Limit=10)
+        resp2 = self.table.scan(Segment=1, TotalSegments=3, Limit=10)
+        resp3 = self.table.scan(Segment=2, TotalSegments=3, Limit=10)
+
+        first_pass = len(resp1["Items"]) + len(resp2["Items"]) + len(resp3["Items"])
+        assert first_pass <= 30
+
+        second_pass = 0
+        if "LastEvaluatedKey" in resp1:
+            resp = self.table.scan(
+                Segment=0, TotalSegments=3, ExclusiveStartKey=resp1["LastEvaluatedKey"]
+            )
+            second_pass += len(resp["Items"])
+
+        if "LastEvaluatedKey" in resp2:
+            resp = self.table.scan(
+                Segment=1, TotalSegments=3, ExclusiveStartKey=resp2["LastEvaluatedKey"]
+            )
+            second_pass += len(resp["Items"])
+
+        if "LastEvaluatedKey" in resp3:
+            resp = self.table.scan(
+                Segment=2, TotalSegments=3, ExclusiveStartKey=resp3["LastEvaluatedKey"]
+            )
+            second_pass += len(resp["Items"])
+
+        assert first_pass + second_pass == 50
+
+    def test_segment_larger_than_total_segments(self):
+        with pytest.raises(ClientError) as exc:
+            self.table.scan(Segment=3, TotalSegments=3)
+        err = exc.value.response["Error"]
+        assert err["Code"] == "ValidationException"
+        assert (
+            err["Message"]
+            == "The Segment parameter is zero-based and must be less than parameter TotalSegments: Segment: 3 is not less than TotalSegments: 3"
+        )
diff --git a/tests/test_ec2/test_security_groups.py b/tests/test_ec2/test_security_groups.py
index 2dc3c797b..30349464f 100644
--- a/tests/test_ec2/test_security_groups.py
+++ b/tests/test_ec2/test_security_groups.py
@@ -2130,6 +2130,7 @@ def test_authorize_security_group_rules_with_different_ipranges_or_prefixes(is_i
         for rule in created_rules:
             rule.pop("GroupOwnerId", None)
             rule.pop("Tags", None)
+            rule.pop("SecurityGroupRuleArn", None)
             rule.pop("SecurityGroupRuleId", None)
             del rule["GroupId"]
 
diff --git a/tests/test_iot/test_iot_integration.py b/tests/test_iot/test_iot_integration.py
new file mode 100644
index 000000000..ef007fef1
--- /dev/null
+++ b/tests/test_iot/test_iot_integration.py
@@ -0,0 +1,27 @@
+import json
+
+import boto3
+
+from moto import mock_aws
+
+
+@mock_aws
+def test_search_things_include_named_shadow():
+    iot_client = boto3.client("iot", region_name="ap-northeast-1")
+    iotdata_client = boto3.client("iot-data", region_name="ap-northeast-1")
+    raw_payload = b'{"state": {"desired": {"led": "on"}, "reported": {"led": "off"}}}'
+
+    thing_name = "test-thing-name"
+    iot_client.create_thing(thingName=thing_name)
+    iotdata_client.update_thing_shadow(
+        thingName=thing_name, shadowName="test_shadow", payload=raw_payload
+    )
+
+    resp = iot_client.search_index(queryString=f"thingName:{thing_name}")
+
+    assert len(resp["things"]) == 1
+    shadow = json.loads(resp["things"][0]["shadow"])
+
+    assert shadow["name"]["test_shadow"]["desired"] == {"led": "on"}
+    assert shadow["name"]["test_shadow"]["reported"] == {"led": "off"}
+    assert shadow["name"]["test_shadow"]["hasDelta"]
diff --git a/tests/test_iot/test_iot_search.py b/tests/test_iot/test_iot_search.py
index 5c0a501cf..ffb096118 100644
--- a/tests/test_iot/test_iot_search.py
+++ b/tests/test_iot/test_iot_search.py
@@ -1,5 +1,3 @@
-import json
-
 import boto3
 import pytest
 
@@ -55,28 +53,6 @@ def test_search_things_include_group_names():
     assert resp["things"][0]["thingGroupNames"] == ["TestGroup1", "AnotherGroup"]
 
 
-@mock_aws
-def test_search_things_include_named_shadow():
-    iot_client = boto3.client("iot", region_name="ap-northeast-1")
-    iotdata_client = boto3.client("iot-data", region_name="ap-northeast-1")
-    raw_payload = b'{"state": {"desired": {"led": "on"}, "reported": {"led": "off"}}}'
-
-    thing_name = "test-thing-name"
-    iot_client.create_thing(thingName=thing_name)
-    iotdata_client.update_thing_shadow(
-        thingName=thing_name, shadowName="test_shadow", payload=raw_payload
-    )
-
-    resp = iot_client.search_index(queryString=f"thingName:{thing_name}")
-
-    assert len(resp["things"]) == 1
-    shadow = json.loads(resp["things"][0]["shadow"])
-
-    assert shadow["name"]["test_shadow"]["desired"] == {"led": "on"}
-    assert shadow["name"]["test_shadow"]["reported"] == {"led": "off"}
-    assert shadow["name"]["test_shadow"]["hasDelta"]
-
-
 @mock_aws
 @pytest.mark.parametrize(
     "query_string,results",
diff --git a/tests/test_stepfunctions/parser/test_stepfunctions.py b/tests/test_stepfunctions/parser/test_stepfunctions.py
index e0c9adf11..c8af68d88 100644
--- a/tests/test_stepfunctions/parser/test_stepfunctions.py
+++ b/tests/test_stepfunctions/parser/test_stepfunctions.py
@@ -157,7 +157,6 @@ def test_version_is_only_available_when_published():
             publish=True,
             tracingConfiguration={"enabled": True},
             loggingConfiguration={"level": "OFF"},
-            encryptionConfiguration=encryption_config,
         )
         assert resp["stateMachineVersionArn"] == f"{arn2}:2"
     finally:
diff --git a/tests/test_stepfunctions/parser/test_stepfunctions_dynamodb_integration.py b/tests/test_stepfunctions/parser/test_stepfunctions_dynamodb_integration.py
index 7e772ca2f..a86449679 100644
--- a/tests/test_stepfunctions/parser/test_stepfunctions_dynamodb_integration.py
+++ b/tests/test_stepfunctions/parser/test_stepfunctions_dynamodb_integration.py
@@ -269,29 +269,21 @@ def test_send_task_failure_invalid_token(table_name=None, sleep_time=0):
                     client.send_task_failure(taskToken="bad_token", error="test error")
 
                 # Verify
-                assert (
-                    exc.value.response["Error"]["Code"] == "UnrecognizedClientException"
-                )
+                assert exc.value.response["Error"]["Code"] == "InvalidToken"
                 assert (
                     exc.value.response["Error"]["Message"]
-                    == "The security token included "
-                    "in the request is invalid."
+                    == "Invalid Token: 'Invalid token'"
                 )
 
                 # Execute
                 with pytest.raises(ClientError) as exc:
-                    client.send_task_success(
-                        taskToken="bad_token", output="test output"
-                    )
+                    client.send_task_success(taskToken="bad_token", output="output")
 
                 # Verify
-                assert (
-                    exc.value.response["Error"]["Code"] == "UnrecognizedClientException"
-                )
+                assert exc.value.response["Error"]["Code"] == "InvalidToken"
                 assert (
                     exc.value.response["Error"]["Message"]
-                    == "The security token included "
-                    "in the request is invalid."
+                    == "Invalid Token: 'Invalid token'"
                 )
 
     verify_execution_result(