Repository: getmoto/moto
Pull request #7722 (2024-05-27 22:50:28)

Bump ruby/setup-ruby from 1.176.2 to 1.177.1
Bumps [ruby/setup-ruby](https://github.com/ruby/setup-ruby) from 1.176.2 to 1.177.1.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/ruby/setup-ruby/commit/943103cae7d3f1bb1e4951d5fcc7928b40e4b742"><code>943103c</code></a> Use downloadAndExtract() for truffleruby+graalvm too</li>
<li><a href="https://github.com/ruby/setup-ruby/commit/7dc18ff0ca6e3630d3f29d2a85ebf6cc27ae9d6c"><code>7dc18ff</code></a> Exclude ruby 1.9 and 2.2 test for ubuntu-24.04</li>
<li><a href="https://github.com/ruby/setup-ruby/commit/00a1085dffc13f7fb05d1864e0a689650814932e"><code>00a1085</code></a> Update README about ubuntu-24.04</li>
<li><a href="https://github.com/ruby/setup-ruby/commit/868b8c087368cc932c95aff40e0b214813952a3a"><code>868b8c0</code></a> Test against ubuntu-24.04 runner</li>
<li><a href="https://github.com/ruby/setup-ruby/commit/4f20bb71029fcb38ea5db1f28bb683f55b53edae"><code>4f20bb7</code></a> Support ubuntu-24.04-x64</li>
<li>See full diff in <a href="https://github.com/ruby/setup-ruby/compare/6615b4b8a02c78c4d711c06df4b8a20aa685a45d...943103cae7d3f1bb1e4951d5fcc7928b40e4b742">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruby/setup-ruby&package-manager=github_actions&previous-version=1.176.2&new-version=1.177.1)](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/docs/docs/configuration/environment_variables.rst b/docs/docs/configuration/environment_variables.rst
index 2520bc0cf..2cc0382c0 100644
--- a/docs/docs/configuration/environment_variables.rst
+++ b/docs/docs/configuration/environment_variables.rst
@@ -32,6 +32,8 @@ The following is a non-exhaustive list of the environment variables that can be
 +-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
 | MOTO_S3_CUSTOM_ENDPOINTS      | str      |           | See :ref:`s3`.                                                                                  |
 +-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
+| MOTO_S3_DEFAULT_MAX_KEYS      | str      |           | See :ref:`s3`.                                                                                  |
++-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
 | MOTO_PRETTIFY_RESPONSES       | bool     | False     | Prettify responses from Moto, making it easier to read and debug.                               |
 +-------------------------------+----------+-----------+-------------------------------------------------------------------------------------------------+
 
diff --git a/docs/docs/services/s3.rst b/docs/docs/services/s3.rst
index 155e1ff07..1d599a25f 100644
--- a/docs/docs/services/s3.rst
+++ b/docs/docs/services/s3.rst
@@ -84,8 +84,26 @@ s3
 - [ ] list_directory_buckets
 - [ ] list_multipart_uploads
 - [X] list_object_versions
+  
+        The default value for the MaxKeys-argument is 100. This can be configured with an environment variable:
+
+        MOTO_S3_DEFAULT_MAX_KEYS=5
+        
+
 - [X] list_objects
+  
+        The default value for the MaxKeys-argument is 100. This can be configured with an environment variable:
+
+        MOTO_S3_DEFAULT_MAX_KEYS=5
+        
+
 - [X] list_objects_v2
+  
+        The default value for the MaxKeys-argument is 100. This can be configured with an environment variable:
+
+        MOTO_S3_DEFAULT_MAX_KEYS=5
+        
+
 - [X] list_parts
 - [X] put_bucket_accelerate_configuration
 - [X] put_bucket_acl
diff --git a/moto/s3/models.py b/moto/s3/models.py
index 86d2ce921..b18b6def8 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -1652,6 +1652,14 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
 
     _-_-_-_
 
+    When listing objects, the default max-keys value is 1000, just as with AWS. Use the following environment variable to configure this this:
+
+    .. sourcecode:: bash
+
+        MOTO_S3_DEFAULT_MAX_KEYS=256
+
+    _-_-_-_
+
     CrossAccount access is allowed by default. If you want Moto to throw an AccessDenied-error when accessing a bucket in another account, use this environment variable:
 
     .. sourcecode:: bash
@@ -1881,6 +1889,11 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
     ) -> Tuple[
         List[FakeKey], List[str], List[FakeDeleteMarker], Optional[str], Optional[str]
     ]:
+        """
+        The default value for the MaxKeys-argument is 100. This can be configured with an environment variable:
+
+        MOTO_S3_DEFAULT_MAX_KEYS=5
+        """
         bucket = self.get_bucket(bucket_name)
 
         common_prefixes: Set[str] = set()
@@ -2571,6 +2584,11 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
         marker: Optional[str],
         max_keys: Optional[int],
     ) -> Tuple[Set[FakeKey], Set[str], bool, Optional[str]]:
+        """
+        The default value for the MaxKeys-argument is 100. This can be configured with an environment variable:
+
+        MOTO_S3_DEFAULT_MAX_KEYS=5
+        """
         key_results = set()
         folder_results = set()
         if prefix:
@@ -2624,6 +2642,11 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
         start_after: Optional[str],
         max_keys: int,
     ) -> Tuple[Set[Union[FakeKey, str]], bool, Optional[str]]:
+        """
+        The default value for the MaxKeys-argument is 100. This can be configured with an environment variable:
+
+        MOTO_S3_DEFAULT_MAX_KEYS=5
+        """
         result_keys, result_folders, _, _ = self.list_objects(
             bucket, prefix, delimiter, marker=None, max_keys=None
         )
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index dd9e9c91c..c53588c78 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -619,7 +619,9 @@ class S3Response(BaseResponse):
         elif "versions" in querystring:
             delimiter = querystring.get("delimiter", [None])[0]
             key_marker = querystring.get("key-marker", [None])[0]
-            max_keys = int(querystring.get("max-keys", [1000])[0])
+            max_keys = int(
+                querystring.get("max-keys", [settings.get_s3_default_max_keys()])[0]
+            )
             prefix = querystring.get("prefix", [""])[0]
             version_id_marker = querystring.get("version-id-marker", [None])[0]
 
@@ -693,7 +695,9 @@ class S3Response(BaseResponse):
         if prefix and isinstance(prefix, bytes):
             prefix = prefix.decode("utf-8")
         delimiter = querystring.get("delimiter", [None])[0]
-        max_keys = int(querystring.get("max-keys", [1000])[0])
+        max_keys = int(
+            querystring.get("max-keys", [settings.get_s3_default_max_keys()])[0]
+        )
         marker = querystring.get("marker", [None])[0]
         encoding_type = querystring.get("encoding-type", [None])[0]
 
@@ -756,7 +760,9 @@ class S3Response(BaseResponse):
         delimiter = querystring.get("delimiter", [None])[0]
 
         fetch_owner = querystring.get("fetch-owner", [False])[0]
-        max_keys = int(querystring.get("max-keys", [1000])[0])
+        max_keys = int(
+            querystring.get("max-keys", [settings.get_s3_default_max_keys()])[0]
+        )
         start_after = querystring.get("start-after", [None])[0]
         encoding_type = querystring.get("encoding-type", [None])[0]
 
diff --git a/moto/settings.py b/moto/settings.py
index 263839c81..a537d0143 100644
--- a/moto/settings.py
+++ b/moto/settings.py
@@ -72,6 +72,10 @@ def get_s3_default_key_buffer_size() -> int:
     )
 
 
+def get_s3_default_max_keys() -> int:
+    return int(os.environ.get("MOTO_S3_DEFAULT_MAX_KEYS", 1000))
+
+
 def s3_allow_crossdomain_access() -> bool:
     return os.environ.get("MOTO_S3_ALLOW_CROSSACCOUNT_ACCESS", "true").lower() == "true"
 


Test patch:
diff --git a/.github/workflows/tests_sdk_ruby.yml b/.github/workflows/tests_sdk_ruby.yml
index fa6f0f230..4997aa25c 100644
--- a/.github/workflows/tests_sdk_ruby.yml
+++ b/.github/workflows/tests_sdk_ruby.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Set up Ruby ${{ matrix.ruby-version }}
-        uses: ruby/setup-ruby@6615b4b8a02c78c4d711c06df4b8a20aa685a45d
+        uses: ruby/setup-ruby@943103cae7d3f1bb1e4951d5fcc7928b40e4b742
         with:
           ruby-version: ${{ matrix.ruby-version }}
       - name: Set up Python 3.8
diff --git a/tests/test_dynamodb/test_dynamodb_update_expressions.py b/tests/test_dynamodb/test_dynamodb_update_expressions.py
index c353cf4f4..5ffcedbac 100644
--- a/tests/test_dynamodb/test_dynamodb_update_expressions.py
+++ b/tests/test_dynamodb/test_dynamodb_update_expressions.py
@@ -180,8 +180,8 @@ def test_update_item_add_empty_set(table_name=None):
     err = exc.value.response["Error"]
     assert err["Code"] == "ValidationException"
     assert (
-        err["Message"]
-        == "ExpressionAttributeValues contains invalid value: One or more parameter values were invalid: An string set  may not be empty"
+        "ExpressionAttributeValues contains invalid value: One or more parameter values were invalid: An string set  may not be empty"
+        in err["Message"]
     )
 
     assert dynamodb.scan(TableName=table_name)["Items"] == [
diff --git a/tests/test_s3/test_s3_list_object_versions.py b/tests/test_s3/test_s3_list_object_versions.py
index 3719eb2c3..0ec0466c8 100644
--- a/tests/test_s3/test_s3_list_object_versions.py
+++ b/tests/test_s3/test_s3_list_object_versions.py
@@ -1,10 +1,11 @@
 from time import sleep
+from unittest import SkipTest, mock
 from uuid import uuid4
 
 import boto3
 import pytest
 
-from moto import mock_aws
+from moto import mock_aws, settings
 from moto.s3.responses import DEFAULT_REGION_NAME
 
 from . import s3_aws_verified
@@ -639,3 +640,25 @@ def enable_versioning(bucket_name, s3_client):
     while resp.get("Status") != "Enabled":
         sleep(0.1)
         resp = s3_client.get_bucket_versioning(Bucket=bucket_name)
+
+
+@mock_aws
+@mock.patch.dict("os.environ", {"MOTO_S3_DEFAULT_MAX_KEYS": "5"})
+def test_list_object_versions_with_custom_limit():
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Can only set env var in DecoratorMode")
+    s3_client = boto3.client("s3", region_name=DEFAULT_REGION_NAME)
+    s3_resource = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
+    bucket_name = "foobar"
+    bucket = s3_resource.Bucket(bucket_name)
+    bucket.create()
+    bucket.Versioning().enable()
+
+    for i in range(10):
+        s3_client.put_object(
+            Bucket=bucket_name, Key="obj", Body=b"ver" + str(i).encode("utf-8")
+        )
+
+    page1 = s3_client.list_object_versions(Bucket=bucket_name)
+    assert page1["MaxKeys"] == 5
+    assert len(page1["Versions"]) == 5
diff --git a/tests/test_s3/test_s3_list_objects.py b/tests/test_s3/test_s3_list_objects.py
index ffb500f71..f436aa22f 100644
--- a/tests/test_s3/test_s3_list_objects.py
+++ b/tests/test_s3/test_s3_list_objects.py
@@ -1,6 +1,10 @@
+from unittest import SkipTest, mock
+
 import boto3
 import pytest
 
+from moto import mock_aws, settings
+
 from . import s3_aws_verified
 
 
@@ -50,3 +54,36 @@ def test_s3_filter_with_manual_marker(name=None) -> None:
     assert pages[1] == ["key_05", "key_06", "key_07", "key_08", "key_09"]
     assert pages[2] == ["key_10", "key_11", "key_12", "key_13", "key_14"]
     assert pages[3] == ["key_15", "key_16"]
+
+
+@mock_aws
+@mock.patch.dict("os.environ", {"MOTO_S3_DEFAULT_MAX_KEYS": "5"})
+def test_list_object_versions_with_custom_limit():
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Can only set env var in DecoratorMode")
+    s3_client = boto3.client("s3", region_name="us-east-1")
+    s3_resource = boto3.resource("s3", region_name="us-east-1")
+    bucket_name = "foobar"
+    bucket = s3_resource.Bucket(bucket_name)
+    bucket.create()
+
+    for i in range(8):
+        s3_client.put_object(Bucket=bucket_name, Key=f"obj_{i}", Body=b"data")
+
+    page1 = s3_client.list_objects(Bucket=bucket_name)
+    assert page1["MaxKeys"] == 5
+    assert len(page1["Contents"]) == 5
+
+    page2 = s3_client.list_objects(Bucket=bucket_name, Marker=page1["NextMarker"])
+    assert page2["MaxKeys"] == 5
+    assert len(page2["Contents"]) == 3
+
+    page1 = s3_client.list_objects_v2(Bucket=bucket_name)
+    assert page1["MaxKeys"] == 5
+    assert len(page1["Contents"]) == 5
+
+    page2 = s3_client.list_objects_v2(
+        Bucket=bucket_name, ContinuationToken=page1["NextContinuationToken"]
+    )
+    assert page2["MaxKeys"] == 5
+    assert len(page2["Contents"]) == 3