Repository: getmoto/moto
Pull request #7118 (2023-12-11 23:09:40)

S3: list_object_versions() now returns the correct NextToken
Follow-up to #6896 

The `NextVersionIdMarker` is now set to the last item returned
The pagination logic is also adjusted so that we skip everything up and including to the last item of the previous page

The existing solution using `itertools.dropwhile` was really clean, but it also resulted in a bug where the last item of the previous page would be re-added to the next page.

All relevant tests are verified against AWS (and extracted into it's own page to make the tests more manageable).



Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7a87c2e1a..5a83285ab 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
       - name: Get pip cache dir
@@ -45,7 +45,7 @@ jobs:
     steps:
     - uses: actions/checkout@v4
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
     # Retrieve the previously cached dependencies
@@ -111,7 +111,7 @@ jobs:
         with:
           fetch-depth: 0
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
       - name: Update & Build
diff --git a/.github/workflows/data-update_config-managed-rules.yml b/.github/workflows/data-update_config-managed-rules.yml
index aa91837f9..863a7824f 100644
--- a/.github/workflows/data-update_config-managed-rules.yml
+++ b/.github/workflows/data-update_config-managed-rules.yml
@@ -27,7 +27,7 @@ jobs:
       uses: actions/checkout@v4
 
     - name: Set up Python 3.9
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: 3.9
 
diff --git a/.github/workflows/data-update_ec2-instance-offerings.yml b/.github/workflows/data-update_ec2-instance-offerings.yml
index f59d8ebee..9c3ab832c 100644
--- a/.github/workflows/data-update_ec2-instance-offerings.yml
+++ b/.github/workflows/data-update_ec2-instance-offerings.yml
@@ -27,7 +27,7 @@ jobs:
       uses: actions/checkout@v4
 
     - name: Set up Python 3.9
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: 3.9
 
diff --git a/.github/workflows/data-update_ec2-instance-types.yml b/.github/workflows/data-update_ec2-instance-types.yml
index 5cc490eba..80e46d113 100644
--- a/.github/workflows/data-update_ec2-instance-types.yml
+++ b/.github/workflows/data-update_ec2-instance-types.yml
@@ -26,7 +26,7 @@ jobs:
       uses: actions/checkout@v4
 
     - name: Set up Python 3.9
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: 3.9
 
diff --git a/.github/workflows/data-update_ssm-default-amis.yml b/.github/workflows/data-update_ssm-default-amis.yml
index 3496021b0..e266e96b7 100644
--- a/.github/workflows/data-update_ssm-default-amis.yml
+++ b/.github/workflows/data-update_ssm-default-amis.yml
@@ -26,7 +26,7 @@ jobs:
       uses: actions/checkout@v4
 
     - name: Set up Python 3.9
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: 3.9
 
diff --git a/.github/workflows/data-update_ssm-default-parameters.yml b/.github/workflows/data-update_ssm-default-parameters.yml
index 3fd76960d..fdf9749e5 100644
--- a/.github/workflows/data-update_ssm-default-parameters.yml
+++ b/.github/workflows/data-update_ssm-default-parameters.yml
@@ -27,7 +27,7 @@ jobs:
       uses: actions/checkout@v4
 
     - name: Set up Python 3.9
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: 3.9
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4e3025e56..5033b5fee 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,7 +29,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python 3.8
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: 3.8
     - name: Install dependencies
diff --git a/moto/ec2/models/availability_zones_and_regions.py b/moto/ec2/models/availability_zones_and_regions.py
index eb46d01ab..77f05b6a1 100644
--- a/moto/ec2/models/availability_zones_and_regions.py
+++ b/moto/ec2/models/availability_zones_and_regions.py
@@ -24,6 +24,7 @@ class Zone:
         self.region_name = region_name
         self.zone_id = zone_id
         self.zone_type = zone_type
+        self.state = "available"
 
 
 class RegionsAndZonesBackend:
@@ -318,8 +319,15 @@ class RegionsAndZonesBackend:
         return ret
 
     def describe_availability_zones(
-        self, filters: Optional[List[Dict[str, Any]]] = None
+        self,
+        filters: Optional[List[Dict[str, Any]]] = None,
+        zone_names: Optional[List[str]] = None,
+        zone_ids: Optional[List[str]] = None,
     ) -> List[Zone]:
+        """
+        The following parameters are supported: ZoneIds, ZoneNames, Filters
+        The following filters are supported: zone-id, zone-type, zone-name, region-name, state
+        """
         # We might not have any zones for the current region, if it was introduced recently
         zones = self.zones.get(self.region_name, [])  # type: ignore[attr-defined]
         attr_pairs = (
@@ -327,10 +335,13 @@ class RegionsAndZonesBackend:
             ("zone-type", "zone_type"),
             ("zone-name", "name"),
             ("region-name", "region_name"),
+            ("state", "state"),
         )
         result = zones
         if filters:
             result = filter_resources(zones, filters, attr_pairs)
+        result = [r for r in result if not zone_ids or r.zone_id in zone_ids]
+        result = [r for r in result if not zone_names or r.name in zone_names]
         return result
 
     def get_zone_by_name(self, name: str) -> Optional[Zone]:
diff --git a/moto/ec2/responses/availability_zones_and_regions.py b/moto/ec2/responses/availability_zones_and_regions.py
index 0ef8238d0..15948c7cb 100644
--- a/moto/ec2/responses/availability_zones_and_regions.py
+++ b/moto/ec2/responses/availability_zones_and_regions.py
@@ -5,7 +5,11 @@ class AvailabilityZonesAndRegions(EC2BaseResponse):
     def describe_availability_zones(self) -> str:
         self.error_on_dryrun()
         filters = self._filters_from_querystring()
-        zones = self.ec2_backend.describe_availability_zones(filters)
+        zone_names = self._get_multi_param("ZoneName")
+        zone_ids = self._get_multi_param("ZoneId")
+        zones = self.ec2_backend.describe_availability_zones(
+            filters, zone_names=zone_names, zone_ids=zone_ids
+        )
         template = self.response_template(DESCRIBE_ZONES_RESPONSE)
         return template.render(zones=zones)
 
diff --git a/moto/s3/models.py b/moto/s3/models.py
index c3fb764d8..b55f79b5a 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -1845,19 +1845,32 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
         all_versions.sort(key=lambda r: (r.name, -unix_time_millis(r.last_modified)))
         last_name = None
 
-        def key_or_version_match(ver: Union[FakeKey, FakeDeleteMarker]) -> bool:
-            if key_marker is None:
-                return True
-
-            if version_id_marker is None or version_id_marker == "":
-                return ver.name >= key_marker
-
-            return ver.name == key_marker and ver.version_id == version_id_marker
+        skip_versions = True
+        last_item_added: Union[None, FakeKey, FakeDeleteMarker] = None
+        for version in all_versions:
+            # Pagination
+            if skip_versions:
+                if key_marker is None:
+                    # If KeyMarker is not supplied, we do not skip anything
+                    skip_versions = False
+                elif not version_id_marker:
+                    # Only KeyMarker is supplied, and it will be set to the last item of the previous page
+                    # We skip all versions with ``name < key_marker``
+                    # Because our list is ordered, we keep everything where ``name >= key_marker`` (i.e.: the next page)
+                    skip_versions = version.name < key_marker
+                    continue
+                elif (
+                    version.name == key_marker
+                    and version.version_id == version_id_marker
+                ):
+                    # KeyMarker and VersionIdMarker are set to the last item of the previous page
+                    # Which means we should still skip the current version
+                    # But continue processing all subsequent versions
+                    skip_versions = False
+                    continue
+                else:
+                    continue
 
-        for version in itertools.dropwhile(
-            lambda ver: not key_or_version_match(ver),
-            all_versions,
-        ):
             name = version.name
             # guaranteed to be sorted - so the first key with this name will be the latest
             version.is_latest = name != last_name
@@ -1882,6 +1895,8 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
                 common_prefixes.add(prefix_including_delimiter)
                 name = prefix_including_delimiter
                 is_common_prefix = True
+            elif last_item_added:
+                name = last_item_added.name
 
             # Only return max_keys items.
             if (
@@ -1889,19 +1904,26 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
                 and len(requested_versions) + len(delete_markers) + len(common_prefixes)
                 >= max_keys
             ):
+
                 next_key_marker = name
-                next_version_id_marker = (
-                    version.version_id if not is_common_prefix else None
-                )
+                if is_common_prefix:
+                    # No NextToken when returning common prefixes
+                    next_version_id_marker = None
+                elif last_item_added is not None:
+                    # NextToken is set to the (version of the) latest item
+                    next_version_id_marker = last_item_added.version_id
+                else:
+                    # Should only happen when max_keys == 0, so when we do not have a last item
+                    next_version_id_marker = None
                 break
 
             if not is_common_prefix:
+                last_item_added = version
                 # Differentiate between FakeKey and FakeDeleteMarkers
                 if not isinstance(version, FakeKey):
                     delete_markers.append(version)
-                    continue
-
-                requested_versions.append(version)
+                else:
+                    requested_versions.append(version)
 
         return (
             requested_versions,


Test patch:
diff --git a/.github/workflows/dependency_test.yml b/.github/workflows/dependency_test.yml
index cdcb033a8..d77b35bcd 100644
--- a/.github/workflows/dependency_test.yml
+++ b/.github/workflows/dependency_test.yml
@@ -28,7 +28,7 @@ jobs:
     - name: Checkout repo
       uses: actions/checkout@v4
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
     - name: Run test
diff --git a/.github/workflows/dockertests.yml b/.github/workflows/dockertests.yml
index 4a3b06200..390df94a3 100644
--- a/.github/workflows/dockertests.yml
+++ b/.github/workflows/dockertests.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: ${{ matrix.python-version }}
       - name: Get pip cache dir
@@ -48,7 +48,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
     - name: Start MotoServer on an unusual port
@@ -107,7 +107,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
     - name: Start MotoServer on a custom Docker network bridge
@@ -165,7 +165,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
     - name: Start MotoServer on an unusual port
diff --git a/.github/workflows/test_outdated_versions.yml b/.github/workflows/test_outdated_versions.yml
index e5959dac6..92e497fde 100644
--- a/.github/workflows/test_outdated_versions.yml
+++ b/.github/workflows/test_outdated_versions.yml
@@ -23,7 +23,7 @@ jobs:
       uses: actions/checkout@v4
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
 
diff --git a/.github/workflows/test_sqs.yml b/.github/workflows/test_sqs.yml
index 0599c6d0c..6061a8ba6 100644
--- a/.github/workflows/test_sqs.yml
+++ b/.github/workflows/test_sqs.yml
@@ -21,7 +21,7 @@ jobs:
       uses: actions/checkout@v4
 
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
 
diff --git a/.github/workflows/test_terraform.yml b/.github/workflows/test_terraform.yml
index fc4837c1c..618c7b291 100644
--- a/.github/workflows/test_terraform.yml
+++ b/.github/workflows/test_terraform.yml
@@ -28,7 +28,7 @@ jobs:
         go-version: ${{ matrix.go-version }}
         cache-dependency-path: tests/terraformtests/terraform-provider-aws/go.sum
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: "3.8"
     - name: Start MotoServer
diff --git a/.github/workflows/tests_decoratormode.yml b/.github/workflows/tests_decoratormode.yml
index 564345d17..f7cc869b5 100644
--- a/.github/workflows/tests_decoratormode.yml
+++ b/.github/workflows/tests_decoratormode.yml
@@ -14,7 +14,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
     - name: Get pip cache dir
diff --git a/.github/workflows/tests_proxymode.yml b/.github/workflows/tests_proxymode.yml
index 2b526d03f..b0f7ff2d0 100644
--- a/.github/workflows/tests_proxymode.yml
+++ b/.github/workflows/tests_proxymode.yml
@@ -14,7 +14,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
     - name: Get pip cache dir
diff --git a/.github/workflows/tests_real_aws.yml b/.github/workflows/tests_real_aws.yml
index b5a438499..9f4dba065 100644
--- a/.github/workflows/tests_real_aws.yml
+++ b/.github/workflows/tests_real_aws.yml
@@ -15,7 +15,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: "3.11"
     - name: Get pip cache dir
diff --git a/.github/workflows/tests_sdk_dotnet.yml b/.github/workflows/tests_sdk_dotnet.yml
index 2c10a8a1b..990a83571 100644
--- a/.github/workflows/tests_sdk_dotnet.yml
+++ b/.github/workflows/tests_sdk_dotnet.yml
@@ -10,7 +10,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python 3.8
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: "3.8"
     - name: Start MotoServer
diff --git a/.github/workflows/tests_sdk_java.yml b/.github/workflows/tests_sdk_java.yml
index b2b8a523f..aac136488 100644
--- a/.github/workflows/tests_sdk_java.yml
+++ b/.github/workflows/tests_sdk_java.yml
@@ -10,7 +10,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python 3.8
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: "3.8"
     - name: Start MotoServer
diff --git a/.github/workflows/tests_sdk_ruby.yml b/.github/workflows/tests_sdk_ruby.yml
index 14e4cbbca..58ed1230c 100644
--- a/.github/workflows/tests_sdk_ruby.yml
+++ b/.github/workflows/tests_sdk_ruby.yml
@@ -13,11 +13,11 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Ruby ${{ matrix.ruby-version }}
-        uses: ruby/setup-ruby@ec02537da5712d66d4d50a0f33b7eb52773b5ed1
+        uses: ruby/setup-ruby@af848b40be8bb463a751551a1180d74782ba8a72
         with:
           ruby-version: ${{ matrix.ruby-version }}
       - name: Set up Python 3.8
-        uses: actions/setup-python@v4
+        uses: actions/setup-python@v5
         with:
           python-version: "3.8"
       - name: Start MotoServer
diff --git a/.github/workflows/tests_servermode.yml b/.github/workflows/tests_servermode.yml
index cfad0bef3..12d904650 100644
--- a/.github/workflows/tests_servermode.yml
+++ b/.github/workflows/tests_servermode.yml
@@ -14,7 +14,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
     - name: Start MotoServer
diff --git a/.github/workflows/tests_without_docker.yml b/.github/workflows/tests_without_docker.yml
index 4b4a7cedb..8f8f6b06c 100644
--- a/.github/workflows/tests_without_docker.yml
+++ b/.github/workflows/tests_without_docker.yml
@@ -21,7 +21,7 @@ jobs:
       with:
         fetch-depth: 0
     - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v4
+      uses: actions/setup-python@v5
       with:
         python-version: ${{ matrix.python-version }}
     - name: Update pip
diff --git a/tests/test_ec2/test_availability_zones_and_regions.py b/tests/test_ec2/test_availability_zones_and_regions.py
index 7d593bdd5..a0886b75f 100644
--- a/tests/test_ec2/test_availability_zones_and_regions.py
+++ b/tests/test_ec2/test_availability_zones_and_regions.py
@@ -38,6 +38,45 @@ def test_boto3_availability_zones():
             assert region in rec["ZoneName"]
 
 
+@mock_ec2
+def test_availability_zones__parameters():
+    us_east = boto3.client("ec2", "us-east-1")
+    zones = us_east.describe_availability_zones(ZoneNames=["us-east-1b"])[
+        "AvailabilityZones"
+    ]
+    assert len(zones) == 1
+    assert zones[0]["ZoneId"] == "use1-az1"
+
+    zones = us_east.describe_availability_zones(ZoneNames=["us-east-1a", "us-east-1b"])[
+        "AvailabilityZones"
+    ]
+    assert len(zones) == 2
+    assert set([zone["ZoneId"] for zone in zones]) == {"use1-az1", "use1-az6"}
+
+    zones = us_east.describe_availability_zones(ZoneIds=["use1-az1"])[
+        "AvailabilityZones"
+    ]
+    assert len(zones) == 1
+    assert zones[0]["ZoneId"] == "use1-az1"
+
+    zones = us_east.describe_availability_zones(
+        Filters=[{"Name": "state", "Values": ["unavailable"]}]
+    )["AvailabilityZones"]
+    assert zones == []
+
+    zones = us_east.describe_availability_zones(
+        Filters=[{"Name": "zone-id", "Values": ["use1-az2"]}]
+    )["AvailabilityZones"]
+    assert len(zones) == 1
+    assert zones[0]["ZoneId"] == "use1-az2"
+
+    zones = us_east.describe_availability_zones(
+        Filters=[{"Name": "zone-name", "Values": ["us-east-1b"]}]
+    )["AvailabilityZones"]
+    assert len(zones) == 1
+    assert zones[0]["ZoneId"] == "use1-az1"
+
+
 @mock_ec2
 def test_describe_availability_zones_dryrun():
     client = boto3.client("ec2", region_name="us-east-1")
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index 941e79ec3..f9deba6bc 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -8,7 +8,6 @@ from gzip import GzipFile
 from io import BytesIO
 from unittest import SkipTest
 from urllib.parse import parse_qs, urlparse
-from uuid import uuid4
 
 import boto3
 import botocore.exceptions
@@ -647,48 +646,6 @@ def test_key_version():
     assert key["VersionId"] == versions[-1]
 
 
-@mock_s3
-def test_list_versions():
-    s3_resource = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
-    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket = s3_resource.Bucket("foobar")
-    bucket.create()
-    bucket.Versioning().enable()
-
-    key_versions = []
-
-    key = bucket.put_object(Key="the-key", Body=b"Version 1")
-    key_versions.append(key.version_id)
-    key = bucket.put_object(Key="the-key", Body=b"Version 2")
-    key_versions.append(key.version_id)
-    assert len(key_versions) == 2
-
-    versions = client.list_object_versions(Bucket="foobar")["Versions"]
-    assert len(versions) == 2
-
-    assert versions[0]["Key"] == "the-key"
-    assert versions[0]["VersionId"] == key_versions[1]
-    resp = client.get_object(Bucket="foobar", Key="the-key")
-    assert resp["Body"].read() == b"Version 2"
-    resp = client.get_object(
-        Bucket="foobar", Key="the-key", VersionId=versions[0]["VersionId"]
-    )
-    assert resp["Body"].read() == b"Version 2"
-
-    assert versions[1]["Key"] == "the-key"
-    assert versions[1]["VersionId"] == key_versions[0]
-    resp = client.get_object(
-        Bucket="foobar", Key="the-key", VersionId=versions[1]["VersionId"]
-    )
-    assert resp["Body"].read() == b"Version 1"
-
-    bucket.put_object(Key="the2-key", Body=b"Version 1")
-
-    assert len(list(bucket.objects.all())) == 2
-    versions = client.list_object_versions(Bucket="foobar", Prefix="the2")["Versions"]
-    assert len(versions) == 1
-
-
 @mock_s3
 def test_acl_setting():
     s3_resource = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
@@ -2383,507 +2340,6 @@ def test_put_bucket_notification_errors():
     )
 
 
-@mock_s3
-def test_list_object_versions():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "000" + str(uuid4())
-    key = "key-with-versions"
-    s3_client.create_bucket(Bucket=bucket_name)
-    s3_client.put_bucket_versioning(
-        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
-    )
-    items = (b"v1", b"v2")
-    for body in items:
-        s3_client.put_object(Bucket=bucket_name, Key=key, Body=body)
-    response = s3_client.list_object_versions(Bucket=bucket_name)
-    # Two object versions should be returned
-    assert len(response["Versions"]) == 2
-    keys = {item["Key"] for item in response["Versions"]}
-    assert keys == {key}
-
-    # the first item in the list should be the latest
-    assert response["Versions"][0]["IsLatest"] is True
-
-    # Test latest object version is returned
-    response = s3_client.get_object(Bucket=bucket_name, Key=key)
-    assert response["Body"].read() == items[-1]
-
-
-@mock_s3
-def test_list_object_versions_with_delimiter():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "000" + str(uuid4())
-    s3_client.create_bucket(Bucket=bucket_name)
-    s3_client.put_bucket_versioning(
-        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
-    )
-    for key_index in list(range(1, 5)) + list(range(10, 14)):
-        for version_index in range(1, 4):
-            body = f"data-{version_index}".encode("UTF-8")
-            s3_client.put_object(
-                Bucket=bucket_name, Key=f"key{key_index}-with-data", Body=body
-            )
-            s3_client.put_object(
-                Bucket=bucket_name, Key=f"key{key_index}-without-data", Body=b""
-            )
-    response = s3_client.list_object_versions(Bucket=bucket_name)
-    # All object versions should be returned
-    # 8 keys * 2 (one with, one without) * 3 versions per key
-    assert len(response["Versions"]) == 48
-
-    # Use start of key as delimiter
-    response = s3_client.list_object_versions(Bucket=bucket_name, Delimiter="key1")
-    assert response["CommonPrefixes"] == [{"Prefix": "key1"}]
-    assert response["Delimiter"] == "key1"
-    # 3 keys that do not contain the phrase 'key1' (key2, key3, key4) * * 2 *  3
-    assert len(response["Versions"]) == 18
-
-    # Use in-between key as delimiter
-    response = s3_client.list_object_versions(Bucket=bucket_name, Delimiter="-with-")
-    assert response["CommonPrefixes"] == [
-        {"Prefix": "key1-with-"},
-        {"Prefix": "key10-with-"},
-        {"Prefix": "key11-with-"},
-        {"Prefix": "key12-with-"},
-        {"Prefix": "key13-with-"},
-        {"Prefix": "key2-with-"},
-        {"Prefix": "key3-with-"},
-        {"Prefix": "key4-with-"},
-    ]
-
-    assert response["Delimiter"] == "-with-"
-    # key(1/10/11/12/13)-without, key(2/3/4)-without
-    assert len(response["Versions"]) == (8 * 1 * 3)
-
-    # Use in-between key as delimiter
-    response = s3_client.list_object_versions(Bucket=bucket_name, Delimiter="1-with-")
-    assert response["CommonPrefixes"] == (
-        [{"Prefix": "key1-with-"}, {"Prefix": "key11-with-"}]
-    )
-    assert response["Delimiter"] == "1-with-"
-    assert len(response["Versions"]) == 42
-    all_keys = {v["Key"] for v in response["Versions"]}
-    assert "key1-without-data" in all_keys
-    assert "key1-with-data" not in all_keys
-    assert "key4-with-data" in all_keys
-    assert "key4-without-data" in all_keys
-
-    # Use in-between key as delimiter + prefix
-    response = s3_client.list_object_versions(
-        Bucket=bucket_name, Prefix="key1", Delimiter="with-"
-    )
-    assert response["CommonPrefixes"] == [
-        {"Prefix": "key1-with-"},
-        {"Prefix": "key10-with-"},
-        {"Prefix": "key11-with-"},
-        {"Prefix": "key12-with-"},
-        {"Prefix": "key13-with-"},
-    ]
-    assert response["Delimiter"] == "with-"
-    assert response["KeyMarker"] == ""
-    assert "NextKeyMarker" not in response
-    assert len(response["Versions"]) == 15
-    all_keys = {v["Key"] for v in response["Versions"]}
-    assert all_keys == {
-        "key1-without-data",
-        "key10-without-data",
-        "key11-without-data",
-        "key13-without-data",
-        "key12-without-data",
-    }
-
-    # Start at KeyMarker, and filter using Prefix+Delimiter for all subsequent keys
-    response = s3_client.list_object_versions(
-        Bucket=bucket_name, Prefix="key1", Delimiter="with-", KeyMarker="key11"
-    )
-    assert response["CommonPrefixes"] == [
-        {"Prefix": "key11-with-"},
-        {"Prefix": "key12-with-"},
-        {"Prefix": "key13-with-"},
-    ]
-    assert response["Delimiter"] == "with-"
-    assert response["KeyMarker"] == "key11"
-    assert "NextKeyMarker" not in response
-    assert len(response["Versions"]) == 9
-    all_keys = {v["Key"] for v in response["Versions"]}
-    assert all_keys == (
-        {"key11-without-data", "key12-without-data", "key13-without-data"}
-    )
-
-    # Delimiter with Prefix being the entire key
-    response = s3_client.list_object_versions(
-        Bucket=bucket_name, Prefix="key1-with-data", Delimiter="-"
-    )
-    assert len(response["Versions"]) == 3
-    assert "CommonPrefixes" not in response
-
-    # Delimiter without prefix
-    response = s3_client.list_object_versions(Bucket=bucket_name, Delimiter="-with-")
-    assert len(response["CommonPrefixes"]) == 8
-    assert {"Prefix": "key1-with-"} in response["CommonPrefixes"]
-    # Should return all keys -without-data
-    assert len(response["Versions"]) == 24
-
-
-@mock_s3
-def test_list_object_versions_with_delimiter_for_deleted_objects():
-    bucket_name = "tests_bucket"
-    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    # Create bucket with versioning
-    client.create_bucket(Bucket=bucket_name)
-    client.put_bucket_versioning(
-        Bucket=bucket_name,
-        VersioningConfiguration={"MFADelete": "Disabled", "Status": "Enabled"},
-    )
-
-    # Create a history of objects
-    for pos in range(2):
-        client.put_object(
-            Bucket=bucket_name, Key=f"obj_{pos}", Body=f"object {pos}".encode("utf-8")
-        )
-
-    for pos in range(2):
-        client.put_object(
-            Bucket=bucket_name,
-            Key=f"hist_obj_{pos}",
-            Body=f"history object {pos}".encode("utf-8"),
-        )
-        for hist_pos in range(2):
-            client.put_object(
-                Bucket=bucket_name,
-                Key=f"hist_obj_{pos}",
-                Body=f"object {pos} {hist_pos}".encode("utf-8"),
-            )
-
-    for pos in range(2):
-        client.put_object(
-            Bucket=bucket_name,
-            Key=f"del_obj_{pos}",
-            Body=f"deleted object {pos}".encode("utf-8"),
-        )
-        client.delete_object(Bucket=bucket_name, Key=f"del_obj_{pos}")
-
-    # Verify we only retrieve the DeleteMarkers that have this prefix
-    objs = client.list_object_versions(Bucket=bucket_name)
-    assert [dm["Key"] for dm in objs["DeleteMarkers"]] == ["del_obj_0", "del_obj_1"]
-
-    hist_objs = client.list_object_versions(Bucket=bucket_name, Prefix="hist_obj")
-    assert "DeleteMarkers" not in hist_objs
-
-    del_objs = client.list_object_versions(Bucket=bucket_name, Prefix="del_obj_0")
-    assert [dm["Key"] for dm in del_objs["DeleteMarkers"]] == ["del_obj_0"]
-
-
-@mock_s3
-def test_list_object_versions_with_versioning_disabled():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "mybucket"
-    key = "key-with-versions"
-    s3_client.create_bucket(Bucket=bucket_name)
-    items = (b"v1", b"v2")
-    for body in items:
-        s3_client.put_object(Bucket=bucket_name, Key=key, Body=body)
-    response = s3_client.list_object_versions(Bucket=bucket_name)
-
-    # One object version should be returned
-    assert len(response["Versions"]) == 1
-    assert response["Versions"][0]["Key"] == key
-
-    # The version id should be the string null
-    assert response["Versions"][0]["VersionId"] == "null"
-
-    # Test latest object version is returned
-    response = s3_client.get_object(Bucket=bucket_name, Key=key)
-    assert "VersionId" not in response["ResponseMetadata"]["HTTPHeaders"]
-    assert response["Body"].read() == items[-1]
-
-
-@mock_s3
-def test_list_object_versions_with_versioning_enabled_late():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "mybucket"
-    key = "key-with-versions"
-    s3_client.create_bucket(Bucket=bucket_name)
-    items = (b"v1", b"v2")
-    s3_client.put_object(Bucket=bucket_name, Key=key, Body=b"v1")
-    s3_client.put_bucket_versioning(
-        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
-    )
-    s3_client.put_object(Bucket=bucket_name, Key=key, Body=b"v2")
-    response = s3_client.list_object_versions(Bucket=bucket_name)
-
-    # Two object versions should be returned
-    assert len(response["Versions"]) == 2
-    keys = {item["Key"] for item in response["Versions"]}
-    assert keys == {key}
-
-    # There should still be a null version id.
-    versions_id = {item["VersionId"] for item in response["Versions"]}
-    assert "null" in versions_id
-
-    # Test latest object version is returned
-    response = s3_client.get_object(Bucket=bucket_name, Key=key)
-    assert response["Body"].read() == items[-1]
-
-
-@mock_s3
-def test_list_object_versions_with_paging():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "000" + str(uuid4())
-    s3_client.create_bucket(Bucket=bucket_name)
-    s3_client.put_bucket_versioning(
-        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
-    )
-
-    obj1ver1 = s3_client.put_object(Bucket=bucket_name, Key="obj1", Body=b"ver1")
-    obj1ver2 = s3_client.put_object(Bucket=bucket_name, Key="obj1", Body=b"ver2")
-    obj1ver3 = s3_client.put_object(Bucket=bucket_name, Key="obj1", Body=b"ver3")
-
-    page1 = s3_client.list_object_versions(
-        Bucket=bucket_name,
-        MaxKeys=2,
-    )
-
-    # Page should have two versions only, and should be truncated.
-    assert len(page1["Versions"]) == 2
-    assert "DeleteMarkers" not in page1
-    assert page1["IsTruncated"] is True
-
-    # This should be obj1 ver3 (latest).
-    assert page1["Versions"][0]["VersionId"] == obj1ver3["VersionId"]
-    assert page1["Versions"][0]["IsLatest"] is True
-
-    # This should be obj1 ver2.
-    assert page1["Versions"][1]["VersionId"] == obj1ver2["VersionId"]
-
-    # The next key/version markers should point to obj1 ver1.
-    assert "NextKeyMarker" in page1
-    assert page1["NextKeyMarker"] == "obj1"
-    assert "NextVersionIdMarker" in page1
-    assert page1["NextVersionIdMarker"] == obj1ver1["VersionId"]
-
-    # Second page should be the last page and have the oldest version.
-    page2 = s3_client.list_object_versions(
-        Bucket=bucket_name,
-        MaxKeys=2,
-        KeyMarker=page1["NextKeyMarker"],
-        VersionIdMarker=page1["NextVersionIdMarker"],
-    )
-
-    # Page should have one version only, and not be truncated.
-    assert len(page2["Versions"]) == 1
-    assert "DeleteMarkers" not in page2
-    assert page2["IsTruncated"] is False
-    assert "NextKeyMarker" not in page2
-    assert "NextVersionIdMarker" not in page2
-
-    # This should be obj1 ver1.
-    assert page2["Versions"][0]["VersionId"] == obj1ver1["VersionId"]
-
-
-@mock_s3
-def test_list_object_versions_with_paging_and_delete_markers():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "000" + str(uuid4())
-    s3_client.create_bucket(Bucket=bucket_name)
-    s3_client.put_bucket_versioning(
-        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
-    )
-
-    # A mix of versions and delete markers.
-    obj1ver1 = s3_client.put_object(Bucket=bucket_name, Key="obj1", Body=b"ver1")
-    obj2ver1 = s3_client.put_object(Bucket=bucket_name, Key="obj2", Body=b"ver1")
-    obj1dmk1 = s3_client.delete_object(Bucket=bucket_name, Key="obj1")
-    obj1dmk2 = s3_client.delete_object(Bucket=bucket_name, Key="obj1")
-    obj1dmk3 = s3_client.delete_object(Bucket=bucket_name, Key="obj1")
-    obj2dmk1 = s3_client.delete_object(Bucket=bucket_name, Key="obj2")
-
-    page1 = s3_client.list_object_versions(
-        Bucket=bucket_name,
-        MaxKeys=2,
-    )
-
-    # Page should have one version and one delete marker, and be truncated.
-    assert "Versions" not in page1
-    assert len(page1["DeleteMarkers"]) == 2
-    assert page1["IsTruncated"] is True
-
-    # This should be obj1 dmk3 (latest).
-    assert page1["DeleteMarkers"][0]["VersionId"] == obj1dmk3["VersionId"]
-    assert page1["DeleteMarkers"][0]["IsLatest"] is True
-
-    # This should be obj1 dmk2.
-    assert page1["DeleteMarkers"][1]["VersionId"] == obj1dmk2["VersionId"]
-
-    # The next key/version markers should point to obj1 dmk1.
-    assert "NextKeyMarker" in page1
-    assert page1["NextKeyMarker"] == "obj1"
-    assert "NextVersionIdMarker" in page1
-    assert page1["NextVersionIdMarker"] == obj1dmk1["VersionId"]
-
-    page2 = s3_client.list_object_versions(
-        Bucket=bucket_name,
-        MaxKeys=2,
-        KeyMarker=page1["NextKeyMarker"],
-        VersionIdMarker=page1["NextVersionIdMarker"],
-    )
-
-    # Page should have one version and one delete marker, and be truncated.
-    assert len(page2["Versions"]) == 1
-    assert len(page2["DeleteMarkers"]) == 1
-    assert page2["IsTruncated"] is True
-
-    # This should be obj1 ver1.
-    assert page2["Versions"][0]["VersionId"] == obj1ver1["VersionId"]
-
-    # This should be obj1 dmk1.
-    assert page2["DeleteMarkers"][0]["VersionId"] == obj1dmk1["VersionId"]
-
-    # The next key/version markers should point to obj2 dmk1.
-    assert "NextKeyMarker" in page2
-    assert page2["NextKeyMarker"] == "obj2"
-    assert "NextVersionIdMarker" in page2
-    assert page2["NextVersionIdMarker"] == obj2dmk1["VersionId"]
-
-    page3 = s3_client.list_object_versions(
-        Bucket=bucket_name,
-        MaxKeys=2,
-        KeyMarker=page2["NextKeyMarker"],
-        VersionIdMarker=page2["NextVersionIdMarker"],
-    )
-
-    # Page should have one version and one delete marker, and not be truncated.
-    assert len(page3["Versions"]) == 1
-    assert len(page3["DeleteMarkers"]) == 1
-    assert page3["IsTruncated"] is False
-
-    # This should be obj2 ver1.
-    assert page3["Versions"][0]["VersionId"] == obj2ver1["VersionId"]
-
-    # This should be obj2 dmk1 (latest).
-    assert page3["DeleteMarkers"][0]["VersionId"] == obj2dmk1["VersionId"]
-    assert page3["DeleteMarkers"][0]["IsLatest"]
-
-    # There should not be any next key/version marker.
-    assert "NextKeyMarker" not in page3
-    assert "NextVersionIdMarker" not in page3
-
-
-@mock_s3
-def test_list_object_versions_with_paging_and_delimiter():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "000" + str(uuid4())
-    s3_client.create_bucket(Bucket=bucket_name)
-    s3_client.put_bucket_versioning(
-        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
-    )
-
-    # Copied from test_list_object_versions_with_delimiter.
-    for key_index in list(range(1, 5)) + list(range(10, 14)):
-        for version_index in range(1, 4):
-            body = f"data-{version_index}".encode("UTF-8")
-            s3_client.put_object(
-                Bucket=bucket_name, Key=f"key{key_index}-with-data", Body=body
-            )
-            s3_client.put_object(
-                Bucket=bucket_name, Key=f"key{key_index}-without-data", Body=b""
-            )
-
-    page1 = s3_client.list_object_versions(
-        Bucket=bucket_name,
-        Delimiter="with-",
-        Prefix="key1",
-        KeyMarker="key11",
-        MaxKeys=5,
-    )
-
-    assert "Versions" in page1
-    assert len(page1["Versions"]) == 3
-    assert [v["Key"] for v in page1["Versions"]] == [
-        "key11-without-data",
-        "key11-without-data",
-        "key11-without-data",
-    ]
-
-    assert "CommonPrefixes" in page1
-    assert [p["Prefix"] for p in page1["CommonPrefixes"]] == [
-        "key11-with-",
-        "key12-with-",
-    ]
-
-    assert page1["IsTruncated"] is True
-    assert page1["NextKeyMarker"] == "key12-with-"
-    assert "NextVersionIdMarker" not in page1
-
-    page2 = s3_client.list_object_versions(
-        Bucket=bucket_name,
-        Delimiter="with-",
-        Prefix="key1",
-        KeyMarker="key12-with-",
-        VersionIdMarker="",
-        MaxKeys=5,
-    )
-
-    assert "Versions" in page2
-    assert len(page2["Versions"]) == 4
-    assert [v["Key"] for v in page2["Versions"]] == [
-        "key12-without-data",
-        "key12-without-data",
-        "key12-without-data",
-        "key13-without-data",
-    ]
-
-    assert "CommonPrefixes" in page2
-    assert [p["Prefix"] for p in page2["CommonPrefixes"]] == [
-        "key13-with-",
-    ]
-
-    assert page2["IsTruncated"] is True
-    assert page2["NextKeyMarker"] == "key13-without-data"
-    assert "NextVersionIdMarker" in page2  # FIXME check VersionId?
-
-    page3 = s3_client.list_object_versions(
-        Bucket=bucket_name,
-        Delimiter="with-",
-        Prefix="key1",
-        KeyMarker="key13-without-data",
-        VersionIdMarker=page2["NextVersionIdMarker"],
-        MaxKeys=5,
-    )
-
-    assert "Versions" in page3
-    assert len(page3["Versions"]) == 2
-    assert [v["Key"] for v in page3["Versions"]] == [
-        "key13-without-data",
-        "key13-without-data",
-    ]
-
-    assert "CommonPrefixes" not in page3
-
-    assert page3["IsTruncated"] is False
-    assert "NextKeyMarker" not in page3
-    assert "NextVersionIdMarker" not in page3
-
-
-@mock_s3
-def test_bad_prefix_list_object_versions():
-    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
-    bucket_name = "mybucket"
-    key = "key-with-versions"
-    bad_prefix = "key-that-does-not-exist"
-    s3_client.create_bucket(Bucket=bucket_name)
-    s3_client.put_bucket_versioning(
-        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
-    )
-    items = (b"v1", b"v2")
-    for body in items:
-        s3_client.put_object(Bucket=bucket_name, Key=key, Body=body)
-    response = s3_client.list_object_versions(Bucket=bucket_name, Prefix=bad_prefix)
-    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
-    assert "Versions" not in response
-    assert "DeleteMarkers" not in response
-
-
 @mock_s3
 def test_delete_markers():
     s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
diff --git a/tests/test_s3/test_s3_list_object_versions.py b/tests/test_s3/test_s3_list_object_versions.py
new file mode 100644
index 000000000..8df6133dc
--- /dev/null
+++ b/tests/test_s3/test_s3_list_object_versions.py
@@ -0,0 +1,640 @@
+from uuid import uuid4
+
+import boto3
+import pytest
+
+from moto import mock_s3
+from moto.s3.responses import DEFAULT_REGION_NAME
+
+from . import s3_aws_verified
+
+
+@mock_s3
+def test_list_versions():
+    s3_resource = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
+    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket = s3_resource.Bucket("foobar")
+    bucket.create()
+    bucket.Versioning().enable()
+
+    key_versions = []
+
+    key = bucket.put_object(Key="the-key", Body=b"Version 1")
+    key_versions.append(key.version_id)
+    key = bucket.put_object(Key="the-key", Body=b"Version 2")
+    key_versions.append(key.version_id)
+    assert len(key_versions) == 2
+
+    versions = client.list_object_versions(Bucket="foobar")["Versions"]
+    assert len(versions) == 2
+
+    assert versions[0]["Key"] == "the-key"
+    assert versions[0]["VersionId"] == key_versions[1]
+    resp = client.get_object(Bucket="foobar", Key="the-key")
+    assert resp["Body"].read() == b"Version 2"
+    resp = client.get_object(
+        Bucket="foobar", Key="the-key", VersionId=versions[0]["VersionId"]
+    )
+    assert resp["Body"].read() == b"Version 2"
+
+    assert versions[1]["Key"] == "the-key"
+    assert versions[1]["VersionId"] == key_versions[0]
+    resp = client.get_object(
+        Bucket="foobar", Key="the-key", VersionId=versions[1]["VersionId"]
+    )
+    assert resp["Body"].read() == b"Version 1"
+
+    bucket.put_object(Key="the2-key", Body=b"Version 1")
+
+    assert len(list(bucket.objects.all())) == 2
+    versions = client.list_object_versions(Bucket="foobar", Prefix="the2")["Versions"]
+    assert len(versions) == 1
+
+
+@mock_s3
+def test_list_object_versions():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "000" + str(uuid4())
+    key = "key-with-versions"
+    s3_client.create_bucket(Bucket=bucket_name)
+    s3_client.put_bucket_versioning(
+        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
+    )
+    items = (b"v1", b"v2")
+    for body in items:
+        s3_client.put_object(Bucket=bucket_name, Key=key, Body=body)
+    response = s3_client.list_object_versions(Bucket=bucket_name)
+    # Two object versions should be returned
+    assert len(response["Versions"]) == 2
+    keys = {item["Key"] for item in response["Versions"]}
+    assert keys == {key}
+
+    # the first item in the list should be the latest
+    assert response["Versions"][0]["IsLatest"] is True
+
+    # Test latest object version is returned
+    response = s3_client.get_object(Bucket=bucket_name, Key=key)
+    assert response["Body"].read() == items[-1]
+
+
+@pytest.mark.aws_verified
+@s3_aws_verified
+def test_list_object_versions_with_delimiter(bucket_name=None):
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    s3_client.put_bucket_versioning(
+        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
+    )
+    for key_index in list(range(1, 5)) + list(range(10, 14)):
+        for version_index in range(1, 4):
+            body = f"data-{version_index}".encode("UTF-8")
+            s3_client.put_object(
+                Bucket=bucket_name, Key=f"key{key_index}-with-data", Body=body
+            )
+            s3_client.put_object(
+                Bucket=bucket_name, Key=f"key{key_index}-without-data", Body=b""
+            )
+    response = s3_client.list_object_versions(Bucket=bucket_name)
+    # All object versions should be returned
+    # 8 keys * 2 (one with, one without) * 3 versions per key
+    assert len(response["Versions"]) == 48
+
+    # Use start of key as delimiter
+    response = s3_client.list_object_versions(Bucket=bucket_name, Delimiter="key1")
+    assert response["CommonPrefixes"] == [{"Prefix": "key1"}]
+    assert response["Delimiter"] == "key1"
+    # 3 keys that do not contain the phrase 'key1' (key2, key3, key4) * * 2 *  3
+    assert len(response["Versions"]) == 18
+
+    # Use in-between key as delimiter
+    response = s3_client.list_object_versions(Bucket=bucket_name, Delimiter="-with-")
+    assert response["CommonPrefixes"] == [
+        {"Prefix": "key1-with-"},
+        {"Prefix": "key10-with-"},
+        {"Prefix": "key11-with-"},
+        {"Prefix": "key12-with-"},
+        {"Prefix": "key13-with-"},
+        {"Prefix": "key2-with-"},
+        {"Prefix": "key3-with-"},
+        {"Prefix": "key4-with-"},
+    ]
+
+    assert response["Delimiter"] == "-with-"
+    # key(1/10/11/12/13)-without, key(2/3/4)-without
+    assert len(response["Versions"]) == (8 * 1 * 3)
+
+    # Use in-between key as delimiter
+    response = s3_client.list_object_versions(Bucket=bucket_name, Delimiter="1-with-")
+    assert response["CommonPrefixes"] == (
+        [{"Prefix": "key1-with-"}, {"Prefix": "key11-with-"}]
+    )
+    assert response["Delimiter"] == "1-with-"
+    assert len(response["Versions"]) == 42
+    all_keys = {v["Key"] for v in response["Versions"]}
+    assert "key1-without-data" in all_keys
+    assert "key1-with-data" not in all_keys
+    assert "key4-with-data" in all_keys
+    assert "key4-without-data" in all_keys
+
+    # Use in-between key as delimiter + prefix
+    response = s3_client.list_object_versions(
+        Bucket=bucket_name, Prefix="key1", Delimiter="with-"
+    )
+    assert response["CommonPrefixes"] == [
+        {"Prefix": "key1-with-"},
+        {"Prefix": "key10-with-"},
+        {"Prefix": "key11-with-"},
+        {"Prefix": "key12-with-"},
+        {"Prefix": "key13-with-"},
+    ]
+    assert response["Delimiter"] == "with-"
+    assert response["KeyMarker"] == ""
+    assert "NextKeyMarker" not in response
+    assert len(response["Versions"]) == 15
+    all_keys = {v["Key"] for v in response["Versions"]}
+    assert all_keys == {
+        "key1-without-data",
+        "key10-without-data",
+        "key11-without-data",
+        "key13-without-data",
+        "key12-without-data",
+    }
+
+    # Start at KeyMarker, and filter using Prefix+Delimiter for all subsequent keys
+    response = s3_client.list_object_versions(
+        Bucket=bucket_name, Prefix="key1", Delimiter="with-", KeyMarker="key11"
+    )
+    assert response["CommonPrefixes"] == [
+        {"Prefix": "key11-with-"},
+        {"Prefix": "key12-with-"},
+        {"Prefix": "key13-with-"},
+    ]
+    assert response["Delimiter"] == "with-"
+    assert response["KeyMarker"] == "key11"
+    assert "NextKeyMarker" not in response
+    assert len(response["Versions"]) == 9
+    all_keys = {v["Key"] for v in response["Versions"]}
+    assert all_keys == (
+        {"key11-without-data", "key12-without-data", "key13-without-data"}
+    )
+
+    # Delimiter with Prefix being the entire key
+    response = s3_client.list_object_versions(
+        Bucket=bucket_name, Prefix="key1-with-data", Delimiter="-"
+    )
+    assert len(response["Versions"]) == 3
+    assert "CommonPrefixes" not in response
+
+    # Delimiter without prefix
+    response = s3_client.list_object_versions(Bucket=bucket_name, Delimiter="-with-")
+    assert len(response["CommonPrefixes"]) == 8
+    assert {"Prefix": "key1-with-"} in response["CommonPrefixes"]
+    # Should return all keys -without-data
+    assert len(response["Versions"]) == 24
+
+
+@mock_s3
+def test_list_object_versions_with_delimiter_for_deleted_objects():
+    bucket_name = "tests_bucket"
+    client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    # Create bucket with versioning
+    client.create_bucket(Bucket=bucket_name)
+    client.put_bucket_versioning(
+        Bucket=bucket_name,
+        VersioningConfiguration={"MFADelete": "Disabled", "Status": "Enabled"},
+    )
+
+    # Create a history of objects
+    for pos in range(2):
+        client.put_object(
+            Bucket=bucket_name, Key=f"obj_{pos}", Body=f"object {pos}".encode("utf-8")
+        )
+
+    for pos in range(2):
+        client.put_object(
+            Bucket=bucket_name,
+            Key=f"hist_obj_{pos}",
+            Body=f"history object {pos}".encode("utf-8"),
+        )
+        for hist_pos in range(2):
+            client.put_object(
+                Bucket=bucket_name,
+                Key=f"hist_obj_{pos}",
+                Body=f"object {pos} {hist_pos}".encode("utf-8"),
+            )
+
+    for pos in range(2):
+        client.put_object(
+            Bucket=bucket_name,
+            Key=f"del_obj_{pos}",
+            Body=f"deleted object {pos}".encode("utf-8"),
+        )
+        client.delete_object(Bucket=bucket_name, Key=f"del_obj_{pos}")
+
+    # Verify we only retrieve the DeleteMarkers that have this prefix
+    objs = client.list_object_versions(Bucket=bucket_name)
+    assert [dm["Key"] for dm in objs["DeleteMarkers"]] == ["del_obj_0", "del_obj_1"]
+
+    hist_objs = client.list_object_versions(Bucket=bucket_name, Prefix="hist_obj")
+    assert "DeleteMarkers" not in hist_objs
+
+    del_objs = client.list_object_versions(Bucket=bucket_name, Prefix="del_obj_0")
+    assert [dm["Key"] for dm in del_objs["DeleteMarkers"]] == ["del_obj_0"]
+
+
+@mock_s3
+def test_list_object_versions_with_versioning_disabled():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "mybucket"
+    key = "key-with-versions"
+    s3_client.create_bucket(Bucket=bucket_name)
+    items = (b"v1", b"v2")
+    for body in items:
+        s3_client.put_object(Bucket=bucket_name, Key=key, Body=body)
+    response = s3_client.list_object_versions(Bucket=bucket_name)
+
+    # One object version should be returned
+    assert len(response["Versions"]) == 1
+    assert response["Versions"][0]["Key"] == key
+
+    # The version id should be the string null
+    assert response["Versions"][0]["VersionId"] == "null"
+
+    # Test latest object version is returned
+    response = s3_client.get_object(Bucket=bucket_name, Key=key)
+    assert "VersionId" not in response["ResponseMetadata"]["HTTPHeaders"]
+    assert response["Body"].read() == items[-1]
+
+
+@mock_s3
+def test_list_object_versions_with_versioning_enabled_late():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "mybucket"
+    key = "key-with-versions"
+    s3_client.create_bucket(Bucket=bucket_name)
+    items = (b"v1", b"v2")
+    s3_client.put_object(Bucket=bucket_name, Key=key, Body=b"v1")
+    s3_client.put_bucket_versioning(
+        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
+    )
+    s3_client.put_object(Bucket=bucket_name, Key=key, Body=b"v2")
+    response = s3_client.list_object_versions(Bucket=bucket_name)
+
+    # Two object versions should be returned
+    assert len(response["Versions"]) == 2
+    keys = {item["Key"] for item in response["Versions"]}
+    assert keys == {key}
+
+    # There should still be a null version id.
+    versions_id = {item["VersionId"] for item in response["Versions"]}
+    assert "null" in versions_id
+
+    # Test latest object version is returned
+    response = s3_client.get_object(Bucket=bucket_name, Key=key)
+    assert response["Body"].read() == items[-1]
+
+
+@pytest.mark.aws_verified
+@s3_aws_verified
+def test_list_object_versions_with_paging(bucket_name=None):
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    s3_client.put_bucket_versioning(
+        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
+    )
+
+    obj1ver1 = s3_client.put_object(Bucket=bucket_name, Key="obj1", Body=b"ver1")
+    obj1ver2 = s3_client.put_object(Bucket=bucket_name, Key="obj1", Body=b"ver2")
+    obj1ver3 = s3_client.put_object(Bucket=bucket_name, Key="obj1", Body=b"ver3")
+
+    page1 = s3_client.list_object_versions(
+        Bucket=bucket_name,
+        MaxKeys=2,
+    )
+
+    # Page should have two versions only, and should be truncated.
+    assert len(page1["Versions"]) == 2
+    assert "DeleteMarkers" not in page1
+    assert page1["IsTruncated"] is True
+
+    # This should be obj1 ver3 (latest).
+    assert page1["Versions"][0]["VersionId"] == obj1ver3["VersionId"]
+    assert page1["Versions"][0]["IsLatest"] is True
+
+    # This should be obj1 ver2.
+    assert page1["Versions"][1]["VersionId"] == obj1ver2["VersionId"]
+
+    # The next key/version markers should point to obj1 ver1.
+    assert "NextKeyMarker" in page1
+    assert page1["NextKeyMarker"] == "obj1"
+    assert "NextVersionIdMarker" in page1
+    assert page1["NextVersionIdMarker"] == obj1ver2["VersionId"]
+
+    # Second page should be the last page and have the oldest version.
+    page2 = s3_client.list_object_versions(
+        Bucket=bucket_name,
+        MaxKeys=2,
+        KeyMarker=page1["NextKeyMarker"],
+        VersionIdMarker=page1["NextVersionIdMarker"],
+    )
+
+    # Page should have one version only, and not be truncated.
+    assert len(page2["Versions"]) == 1
+    assert "DeleteMarkers" not in page2
+    assert page2["IsTruncated"] is False
+    assert "NextKeyMarker" not in page2
+    assert "NextVersionIdMarker" not in page2
+
+    # This should be obj1 ver1.
+    assert page2["Versions"][0]["VersionId"] == obj1ver1["VersionId"]
+
+
+@pytest.mark.aws_verified
+@s3_aws_verified
+def test_list_object_versions_with_paging_and_delete_markers(bucket_name=None):
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    s3_client.put_bucket_versioning(
+        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
+    )
+
+    # A mix of versions and delete markers.
+    obj1ver1 = s3_client.put_object(Bucket=bucket_name, Key="obj1", Body=b"ver1")
+    obj2ver1 = s3_client.put_object(Bucket=bucket_name, Key="obj2", Body=b"ver1")
+    obj1dmk1 = s3_client.delete_object(Bucket=bucket_name, Key="obj1")
+    obj1dmk2 = s3_client.delete_object(Bucket=bucket_name, Key="obj1")
+    obj1dmk3 = s3_client.delete_object(Bucket=bucket_name, Key="obj1")
+    obj2dmk1 = s3_client.delete_object(Bucket=bucket_name, Key="obj2")
+
+    page1 = s3_client.list_object_versions(
+        Bucket=bucket_name,
+        MaxKeys=2,
+    )
+
+    # Page should have one version and one delete marker, and be truncated.
+    assert "Versions" not in page1
+    assert len(page1["DeleteMarkers"]) == 2
+    assert page1["IsTruncated"] is True
+
+    # This should be obj1 dmk3 (latest).
+    assert page1["DeleteMarkers"][0]["VersionId"] == obj1dmk3["VersionId"]
+    assert page1["DeleteMarkers"][0]["IsLatest"] is True
+
+    # This should be obj1 dmk2.
+    assert page1["DeleteMarkers"][1]["VersionId"] == obj1dmk2["VersionId"]
+
+    # The next key/version markers should point to obj1 dmk1.
+    assert "NextKeyMarker" in page1
+    assert page1["NextKeyMarker"] == "obj1"
+    assert "NextVersionIdMarker" in page1
+    assert page1["NextVersionIdMarker"] == obj1dmk2["VersionId"]
+
+    page2 = s3_client.list_object_versions(
+        Bucket=bucket_name,
+        MaxKeys=2,
+        KeyMarker=page1["NextKeyMarker"],
+        VersionIdMarker=page1["NextVersionIdMarker"],
+    )
+
+    # Page should have one version and one delete marker, and be truncated.
+    assert len(page2["Versions"]) == 1
+    assert len(page2["DeleteMarkers"]) == 1
+    assert page2["IsTruncated"] is True
+
+    # This should be obj1 ver1.
+    assert page2["Versions"][0]["VersionId"] == obj1ver1["VersionId"]
+
+    # This should be obj1 dmk1.
+    assert page2["DeleteMarkers"][0]["VersionId"] == obj1dmk1["VersionId"]
+
+    # The next key/version markers should point to obj2 dmk1.
+    assert "NextKeyMarker" in page2
+    assert page2["NextKeyMarker"] == "obj1"
+    assert "NextVersionIdMarker" in page2
+    assert page2["NextVersionIdMarker"] == obj1ver1["VersionId"]
+
+    page3 = s3_client.list_object_versions(
+        Bucket=bucket_name,
+        MaxKeys=2,
+        KeyMarker=page2["NextKeyMarker"],
+        VersionIdMarker=page2["NextVersionIdMarker"],
+    )
+
+    # Page should have one version and one delete marker, and not be truncated.
+    assert len(page3["Versions"]) == 1
+    assert len(page3["DeleteMarkers"]) == 1
+    assert page3["IsTruncated"] is False
+
+    # This should be obj2 ver1.
+    assert page3["Versions"][0]["VersionId"] == obj2ver1["VersionId"]
+
+    # This should be obj2 dmk1 (latest).
+    assert page3["DeleteMarkers"][0]["VersionId"] == obj2dmk1["VersionId"]
+    assert page3["DeleteMarkers"][0]["IsLatest"]
+
+    # There should not be any next key/version marker.
+    assert "NextKeyMarker" not in page3
+    assert "NextVersionIdMarker" not in page3
+
+
+@pytest.mark.aws_verified
+@s3_aws_verified
+def test_list_object_versions_with_paging_and_delimiter(bucket_name=None):
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+
+    s3_client.put_bucket_versioning(
+        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
+    )
+
+    # Copied from test_list_object_versions_with_delimiter.
+    for key_index in list(range(1, 5)) + list(range(10, 14)):
+        for version_index in range(1, 4):
+            body = f"data-{version_index}".encode("UTF-8")
+            s3_client.put_object(
+                Bucket=bucket_name, Key=f"key{key_index}-with-data", Body=body
+            )
+            s3_client.put_object(
+                Bucket=bucket_name, Key=f"key{key_index}-without-data", Body=b""
+            )
+
+    page1 = s3_client.list_object_versions(
+        Bucket=bucket_name,
+        Delimiter="with-",
+        Prefix="key1",
+        KeyMarker="key11",
+        MaxKeys=5,
+    )
+
+    assert "Versions" in page1
+    assert len(page1["Versions"]) == 3
+    assert [v["Key"] for v in page1["Versions"]] == [
+        "key11-without-data",
+        "key11-without-data",
+        "key11-without-data",
+    ]
+
+    assert "CommonPrefixes" in page1
+    assert [p["Prefix"] for p in page1["CommonPrefixes"]] == [
+        "key11-with-",
+        "key12-with-",
+    ]
+
+    assert page1["IsTruncated"] is True
+    assert page1["NextKeyMarker"] == "key12-with-"
+    assert "NextVersionIdMarker" not in page1
+
+    page2 = s3_client.list_object_versions(
+        Bucket=bucket_name,
+        Delimiter="with-",
+        Prefix="key1",
+        KeyMarker="key12-with-",
+        MaxKeys=5,
+    )
+
+    assert "Versions" in page2
+    assert len(page2["Versions"]) == 4
+    assert [v["Key"] for v in page2["Versions"]] == [
+        "key12-without-data",
+        "key12-without-data",
+        "key12-without-data",
+        "key13-without-data",
+    ]
+
+    assert "CommonPrefixes" in page2
+    assert [p["Prefix"] for p in page2["CommonPrefixes"]] == [
+        "key13-with-",
+    ]
+
+    assert page2["IsTruncated"] is True
+    assert page2["NextKeyMarker"] == "key13-without-data"
+    assert "NextVersionIdMarker" in page2  # FIXME check VersionId?
+
+    page3 = s3_client.list_object_versions(
+        Bucket=bucket_name,
+        Delimiter="with-",
+        Prefix="key1",
+        KeyMarker="key13-without-data",
+        VersionIdMarker=page2["NextVersionIdMarker"],
+        MaxKeys=5,
+    )
+
+    assert "Versions" in page3
+    assert len(page3["Versions"]) == 2
+    assert [v["Key"] for v in page3["Versions"]] == [
+        "key13-without-data",
+        "key13-without-data",
+    ]
+
+    assert "CommonPrefixes" not in page3
+
+    assert page3["IsTruncated"] is False
+    assert "NextKeyMarker" not in page3
+    assert "NextVersionIdMarker" not in page3
+
+
+@mock_s3
+def test_bad_prefix_list_object_versions():
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "mybucket"
+    key = "key-with-versions"
+    bad_prefix = "key-that-does-not-exist"
+    s3_client.create_bucket(Bucket=bucket_name)
+    s3_client.put_bucket_versioning(
+        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
+    )
+    items = (b"v1", b"v2")
+    for body in items:
+        s3_client.put_object(Bucket=bucket_name, Key=key, Body=body)
+    response = s3_client.list_object_versions(Bucket=bucket_name, Prefix=bad_prefix)
+    assert response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    assert "Versions" not in response
+    assert "DeleteMarkers" not in response
+
+
+@pytest.mark.aws_verified
+@s3_aws_verified
+def test_list_object_versions__sort_order(bucket_name=None):
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    s3_client.put_bucket_versioning(
+        Bucket=bucket_name, VersioningConfiguration={"Status": "Enabled"}
+    )
+
+    # Put one object, and delete it
+    b_ver1 = s3_client.put_object(Bucket=bucket_name, Key="bbb", Body=b"ver1")[
+        "VersionId"
+    ]
+
+    b_del = s3_client.delete_object(Bucket=bucket_name, Key="bbb")["VersionId"]
+
+    # Asking for one version:
+    # We'll get the deleted version first
+    version_list = s3_client.list_object_versions(Bucket=bucket_name, MaxKeys=1)
+    assert "Versions" not in version_list
+    assert len(version_list["DeleteMarkers"]) == 1
+    assert version_list["DeleteMarkers"][0]["VersionId"] == b_del
+
+    # Create an object with a name that comes first alphabetically
+    # We'll get this object first
+    a_ver1 = s3_client.put_object(Bucket=bucket_name, Key="aaa", Body=b"ver1")[
+        "VersionId"
+    ]
+
+    # Asking for one version:
+    # We'll get the new object first (because it's alphabetically earlier)
+    version_list = s3_client.list_object_versions(Bucket=bucket_name, MaxKeys=1)
+    assert "DeleteMarkers" not in version_list
+    assert len(version_list["Versions"]) == 1
+    assert version_list["Versions"][0]["VersionId"] == a_ver1
+
+    # To double-check: create another object that's alphabetically at the end, but most recent
+    s3_client.put_object(Bucket=bucket_name, Key="zzz", Body=b"ver1")
+
+    # Asking for one version:
+    # We'll still get the a-object first (because it's alphabetically earlier)
+    version_list = s3_client.list_object_versions(Bucket=bucket_name, MaxKeys=1)
+    assert "DeleteMarkers" not in version_list
+    assert len(version_list["Versions"]) == 1
+    assert version_list["Versions"][0]["VersionId"] == a_ver1
+
+    # Asking for two versions:
+    # We'll get the a-object first, and the deleted version
+    version_list = s3_client.list_object_versions(Bucket=bucket_name, MaxKeys=2)
+    assert len(version_list["Versions"]) == 1
+    assert version_list["Versions"][0]["VersionId"] == a_ver1
+    assert len(version_list["DeleteMarkers"]) == 1
+    assert version_list["DeleteMarkers"][0]["VersionId"] == b_del
+
+    # Create a different version of the b-object
+    b_ver2 = s3_client.put_object(Bucket=bucket_name, Key="bbb", Body=b"ver2")[
+        "VersionId"
+    ]
+
+    # We'll still get the new object first
+    version_list = s3_client.list_object_versions(Bucket=bucket_name, MaxKeys=1)
+    assert len(version_list["Versions"]) == 1
+    assert version_list["Versions"][0]["VersionId"] == a_ver1
+
+    # Asking for two versions:
+    # We'll get the latest version of b
+    version_list = s3_client.list_object_versions(Bucket=bucket_name, MaxKeys=2)
+    assert len(version_list["Versions"]) == 2
+    assert version_list["Versions"][0]["VersionId"] == a_ver1
+    assert version_list["Versions"][1]["VersionId"] == b_ver2
+
+    # Asking for three versions:
+    # We also get the deleted version
+    # Which proves that we order by time first, and only then by type
+    version_list = s3_client.list_object_versions(Bucket=bucket_name, MaxKeys=3)
+    assert len(version_list["Versions"]) == 2
+    assert version_list["Versions"][0]["VersionId"] == a_ver1
+    assert version_list["Versions"][1]["VersionId"] == b_ver2
+    assert len(version_list["DeleteMarkers"]) == 1
+    assert version_list["DeleteMarkers"][0]["VersionId"] == b_del
+
+    # Asking for four versions:
+    # Only now do we get the initial b-object
+    # Because it was created the earliest (of all a + b objects)
+    version_list = s3_client.list_object_versions(Bucket=bucket_name, MaxKeys=4)
+    assert len(version_list["Versions"]) == 3
+    assert version_list["Versions"][0]["VersionId"] == a_ver1
+    assert version_list["Versions"][1]["VersionId"] == b_ver2
+    assert version_list["Versions"][2]["VersionId"] == b_ver1
+    assert len(version_list["DeleteMarkers"]) == 1
+    assert version_list["DeleteMarkers"][0]["VersionId"] == b_del