Repository: getmoto/moto
Pull request #7117 (2023-12-11 22:09:28)

Bump actions/setup-go from 4 to 5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/setup-go/releases">actions/setup-go's releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<p>In scope of this release, we change Nodejs runtime from node16 to node20 (<a href="https://redirect.github.com/actions/setup-go/pull/421">actions/setup-go#421</a>). Moreover, we update some dependencies to the latest versions (<a href="https://redirect.github.com/actions/setup-go/pull/445">actions/setup-go#445</a>).</p>
<p>Besides, this release contains such changes as:</p>
<ul>
<li>Fix hosted tool cache usage on windows by <a href="https://github.com/galargh"><code>@​galargh</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/411">actions/setup-go#411</a></li>
<li>Improve documentation regarding dependencies caching by <a href="https://github.com/artemgavrilov"><code>@​artemgavrilov</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/417">actions/setup-go#417</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/galargh"><code>@​galargh</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/411">actions/setup-go#411</a></li>
<li><a href="https://github.com/artemgavrilov"><code>@​artemgavrilov</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/417">actions/setup-go#417</a></li>
<li><a href="https://github.com/chenrui333"><code>@​chenrui333</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/421">actions/setup-go#421</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-go/compare/v4...v5.0.0">https://github.com/actions/setup-go/compare/v4...v5.0.0</a></p>
<h2>v4.1.0</h2>
<h2>What's Changed</h2>
<p>In scope of this release, slow installation on Windows was fixed by <a href="https://github.com/dsame"><code>@​dsame</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/393">actions/setup-go#393</a> and OS version was added to <code>primaryKey</code> for Ubuntu runners to avoid conflicts (<a href="https://redirect.github.com/actions/setup-go/pull/383">actions/setup-go#383</a>)</p>
<p>This release also includes the following changes:</p>
<ul>
<li>Remove implicit dependencies by <a href="https://github.com/nikolai-laevskii"><code>@​nikolai-laevskii</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/378">actions/setup-go#378</a></li>
<li>Update action.yml by <a href="https://github.com/mkelly"><code>@​mkelly</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/379">actions/setup-go#379</a></li>
<li>Added a description that go-version should be specified as a string type by <a href="https://github.com/n3xem"><code>@​n3xem</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/367">actions/setup-go#367</a></li>
<li>Add note about YAML parsing versions by <a href="https://github.com/dmitry-shibanov"><code>@​dmitry-shibanov</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/382">actions/setup-go#382</a></li>
<li>Automatic update of configuration files from 05/23/2023 by <a href="https://github.com/github-actions"><code>@​github-actions</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/377">actions/setup-go#377</a></li>
<li>Bump tough-cookie and <code>@​azure/ms-rest-js</code> by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/392">actions/setup-go#392</a></li>
<li>Bump word-wrap from 1.2.3 to 1.2.4 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/397">actions/setup-go#397</a></li>
<li>Bump semver from 6.3.0 to 6.3.1 by <a href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/396">actions/setup-go#396</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/mkelly"><code>@​mkelly</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/379">actions/setup-go#379</a></li>
<li><a href="https://github.com/n3xem"><code>@​n3xem</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/367">actions/setup-go#367</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-go/compare/v4...v4.1.0">https://github.com/actions/setup-go/compare/v4...v4.1.0</a></p>
<h2>v4.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update documentation for <code>v4</code> by <a href="https://github.com/dsame"><code>@​dsame</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/354">actions/setup-go#354</a></li>
<li>Fix glob bug in the package.json scripts section by <a href="https://github.com/IvanZosimov"><code>@​IvanZosimov</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/359">actions/setup-go#359</a></li>
<li>Bump <code>xml2js</code> dependency by <a href="https://github.com/dmitry-shibanov"><code>@​dmitry-shibanov</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/370">actions/setup-go#370</a></li>
<li>Bump <code>@actions/cache</code> dependency to v3.2.1 by <a href="https://github.com/nikolai-laevskii"><code>@​nikolai-laevskii</code></a> in <a href="https://redirect.github.com/actions/setup-go/pull/374">actions/setup-go#374</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/nikolai-laevskii"><code>@​nikolai-laevskii</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-go/pull/374">actions/setup-go#374</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-go/compare/v4...v4.0.1">https://github.com/actions/setup-go/compare/v4...v4.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/setup-go/commit/0c52d547c9bc32b1aa3301fd7a9cb496313a4491"><code>0c52d54</code></a> Update dependencies for node20 (<a href="https://redirect.github.com/actions/setup-go/issues/445">#445</a>)</li>
<li><a href="https://github.com/actions/setup-go/commit/bfd2fb341f32be7281829126376a12a780ca79fc"><code>bfd2fb3</code></a> Merge pull request <a href="https://redirect.github.com/actions/setup-go/issues/421">#421</a> from chenrui333/node20-runtime</li>
<li><a href="https://github.com/actions/setup-go/commit/3d65fa57fcbfe4a359b6b71a6c65e6eec12984eb"><code>3d65fa5</code></a> feat: bump to use actions/checkout@v4</li>
<li><a href="https://github.com/actions/setup-go/commit/8a505c9cf2e2726eda7f3268d6992e386a12da52"><code>8a505c9</code></a> feat: bump to use node20 runtime</li>
<li><a href="https://github.com/actions/setup-go/commit/883490dfd06f396ebe0b738bc313a53cf9d851e5"><code>883490d</code></a> Merge pull request <a href="https://redirect.github.com/actions/setup-go/issues/417">#417</a> from artemgavrilov/main</li>
<li><a href="https://github.com/actions/setup-go/commit/d45ebba0ce181dc5604aaf69ce5a0bdcbd3b1807"><code>d45ebba</code></a> Rephrase sentence</li>
<li><a href="https://github.com/actions/setup-go/commit/317c6617fa9e4e67f1e5e20ad8bc98bf298a0f8f"><code>317c661</code></a> Replace <code>wildcards</code> term with <code>globs</code>.</li>
<li><a href="https://github.com/actions/setup-go/commit/f90673ad641a19d0689fba58b5c79adc54be5d81"><code>f90673a</code></a> Merge pull request <a href="https://redirect.github.com/actions/setup-go/issues/1">#1</a> from artemgavrilov/caching-docs-improvement</li>
<li><a href="https://github.com/actions/setup-go/commit/801823434715e45aa48743a38182d33b33675d02"><code>8018234</code></a> Improve documentation regarding dependencies cachin</li>
<li><a href="https://github.com/actions/setup-go/commit/d085b4fe57b6e17262cbebc67b4d2d341d8938c2"><code>d085b4f</code></a> Merge pull request <a href="https://redirect.github.com/actions/setup-go/issues/411">#411</a> from galargh/fix/windows-hostedtoolcache</li>
<li>Additional commits viewable in <a href="https://github.com/actions/setup-go/compare/v4...v5">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-go&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


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..240ac55c3 100644
--- a/.github/workflows/test_terraform.yml
+++ b/.github/workflows/test_terraform.yml
@@ -23,12 +23,12 @@ jobs:
     - uses: actions/checkout@v4
       with:
         submodules: 'true'
-    - uses: actions/setup-go@v4
+    - uses: actions/setup-go@v5
       with:
         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