Repository: getmoto/moto
Pull request #7468 (2024-03-14 16:10:40)

S3: `Days` must not be provided for select request when restoring object
- Raise error when both `Days` and `Type` parameters exist in restore object request.

![image](https://github.com/getmoto/moto/assets/61897166/2f364864-297f-4c30-bdea-dd07fc3162b0)
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/s3/client/restore_object.html

related: https://github.com/getmoto/moto/issues/7467


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 6d28d4849..b18aa501f 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -1,16 +1,16 @@
 
 ## acm
 <details>
-<summary>40% implemented</summary>
+<summary>60% implemented</summary>
 
 - [X] add_tags_to_certificate
 - [X] delete_certificate
-- [ ] describe_certificate
+- [X] describe_certificate
 - [X] export_certificate
 - [ ] get_account_configuration
 - [X] get_certificate
-- [ ] import_certificate
-- [ ] list_certificates
+- [X] import_certificate
+- [X] list_certificates
 - [ ] list_tags_for_certificate
 - [ ] put_account_configuration
 - [X] remove_tags_from_certificate
diff --git a/Makefile b/Makefile
index a5c5e34ce..33e277ee3 100644
--- a/Makefile
+++ b/Makefile
@@ -31,7 +31,7 @@ lint:
 
 format:
 	black moto/ tests/
-	ruff --fix moto/ tests/
+	ruff check --fix moto/ tests/
 
 test-only:
 	rm -f .coverage
diff --git a/docs/docs/services/acm.rst b/docs/docs/services/acm.rst
index 8b1ee3654..cebe0d025 100644
--- a/docs/docs/services/acm.rst
+++ b/docs/docs/services/acm.rst
@@ -16,12 +16,12 @@ acm
 
 - [X] add_tags_to_certificate
 - [X] delete_certificate
-- [ ] describe_certificate
+- [X] describe_certificate
 - [X] export_certificate
 - [ ] get_account_configuration
 - [X] get_certificate
-- [ ] import_certificate
-- [ ] list_certificates
+- [X] import_certificate
+- [X] list_certificates
 - [ ] list_tags_for_certificate
 - [ ] put_account_configuration
 - [X] remove_tags_from_certificate
diff --git a/moto/acm/models.py b/moto/acm/models.py
index 484de6430..585d99530 100644
--- a/moto/acm/models.py
+++ b/moto/acm/models.py
@@ -454,7 +454,7 @@ class AWSCertificateManagerBackend(BaseBackend):
             "expires": utcnow() + datetime.timedelta(hours=1),
         }
 
-    def import_cert(
+    def import_certificate(
         self,
         certificate: bytes,
         private_key: bytes,
@@ -492,13 +492,7 @@ class AWSCertificateManagerBackend(BaseBackend):
 
         return bundle.arn
 
-    def get_certificates_list(self, statuses: List[str]) -> Iterable[CertBundle]:
-        """
-        Get list of certificates
-
-        :return: List of certificates
-        :rtype: list of CertBundle
-        """
+    def list_certificates(self, statuses: List[str]) -> Iterable[CertBundle]:
         for arn in self._certificates.keys():
             cert = self.get_certificate(arn)
             if not statuses or cert.status in statuses:
@@ -512,6 +506,9 @@ class AWSCertificateManagerBackend(BaseBackend):
         cert_bundle.check()
         return cert_bundle
 
+    def describe_certificate(self, arn: str) -> CertBundle:
+        return self.get_certificate(arn)
+
     def delete_certificate(self, arn: str) -> None:
         if arn not in self._certificates:
             raise CertificateNotFound(arn=arn, account_id=self.account_id)
diff --git a/moto/acm/responses.py b/moto/acm/responses.py
index 7b3636e95..899cbbf24 100644
--- a/moto/acm/responses.py
+++ b/moto/acm/responses.py
@@ -57,7 +57,7 @@ class AWSCertificateManagerResponse(BaseResponse):
                 dict(status=400),
             )
 
-        cert_bundle = self.acm_backend.get_certificate(arn)
+        cert_bundle = self.acm_backend.describe_certificate(arn)
 
         return json.dumps(cert_bundle.describe())
 
@@ -120,7 +120,7 @@ class AWSCertificateManagerResponse(BaseResponse):
                     "The certificate chain is not PEM-encoded or is not valid."
                 )
 
-        arn = self.acm_backend.import_cert(
+        arn = self.acm_backend.import_certificate(
             certificate, private_key, chain=chain, arn=current_arn, tags=tags
         )
 
@@ -129,7 +129,7 @@ class AWSCertificateManagerResponse(BaseResponse):
     def list_certificates(self) -> str:
         certs = []
         statuses = self._get_param("CertificateStatuses")
-        for cert_bundle in self.acm_backend.get_certificates_list(statuses):
+        for cert_bundle in self.acm_backend.list_certificates(statuses):
             certs.append(cert_bundle.describe()["Certificate"])
 
         result = {"CertificateSummaryList": certs}
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
index 5a1216be7..0a6b913b6 100644
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -475,6 +475,11 @@ def parse_condition(condition: Union[Dict[str, Any], bool], resources_map: "Reso
 def parse_output(
     output_logical_id: str, output_json: Any, resources_map: "ResourceMap"
 ) -> Optional[Output]:
+    if "Condition" in output_json and not resources_map.lazy_condition_map.get(
+        output_json["Condition"]
+    ):
+        # This Resource is not initialized - impossible to show Output
+        return None
     output_json = clean_json(output_json, resources_map)
     if "Value" not in output_json:
         return None
@@ -684,11 +689,22 @@ class ResourceMap(collections_abc.Mapping):  # type: ignore[type-arg]
     def validate_outputs(self) -> None:
         outputs = self._template.get("Outputs") or {}
         for value in outputs.values():
+            if "Condition" in value:
+                if not self.lazy_condition_map[value["Condition"]]:
+                    # This Output is not shown - no point in validating it
+                    continue
             value = value.get("Value", {})
             if "Fn::GetAtt" in value:
-                resource_type = self._resource_json_map.get(value["Fn::GetAtt"][0])[  # type: ignore[index]
-                    "Type"
-                ]
+                resource_name = value["Fn::GetAtt"][0]
+                resource = self._resource_json_map.get(resource_name)
+                # validate resource will be created
+                if "Condition" in resource:  # type: ignore
+                    if not self.lazy_condition_map[resource["Condition"]]:  # type: ignore[index]
+                        raise ValidationError(
+                            message=f"Unresolved resource dependencies [{resource_name}] in the Outputs block of the template"
+                        )
+                # Validate attribute exists on this Type
+                resource_type = resource["Type"]  # type: ignore[index]
                 attr = value["Fn::GetAtt"][1]
                 resource_class = resource_class_from_type(resource_type)
                 if not resource_class.has_cfn_attr(attr):
diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py
index 07a1fb363..b848f5e14 100644
--- a/moto/cloudformation/responses.py
+++ b/moto/cloudformation/responses.py
@@ -812,6 +812,7 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResponse>
         <NotificationARNs/>
         {% endif %}
         <DisableRollback>false</DisableRollback>
+        {%if stack.stack_outputs %}
         <Outputs>
         {% for output in stack.stack_outputs %}
           <member>
@@ -820,6 +821,7 @@ DESCRIBE_STACKS_TEMPLATE = """<DescribeStacksResponse>
           </member>
         {% endfor %}
         </Outputs>
+        {% endif %}
         <Parameters>
         {% for param_name, param_value in stack.stack_parameters.items() %}
           <member>
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index 0e960764e..051eb7ee4 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -869,6 +869,7 @@ class CognitoIdpUser(BaseModel):
         flat_attributes.update(flatten_attrs(new_attributes))
         self.attribute_lookup = flat_attributes
         self.attributes = expand_attrs(flat_attributes)
+        self.last_modified_date = utcnow()
 
     def delete_attributes(self, attrs_to_delete: List[str]) -> None:
         flat_attributes = flatten_attrs(self.attributes)
@@ -891,6 +892,7 @@ class CognitoIdpUser(BaseModel):
             )
         self.attribute_lookup = flat_attributes
         self.attributes = expand_attrs(flat_attributes)
+        self.last_modified_date = utcnow()
 
 
 class CognitoResourceServer(BaseModel):
diff --git a/moto/s3/exceptions.py b/moto/s3/exceptions.py
index dfea0dec6..7f12ae885 100644
--- a/moto/s3/exceptions.py
+++ b/moto/s3/exceptions.py
@@ -584,3 +584,23 @@ class HeadOnDeleteMarker(Exception):
 
     def __init__(self, marker: "FakeDeleteMarker"):
         self.marker = marker
+
+
+class DaysMustNotProvidedForSelectRequest(S3ClientError):
+    code = 400
+
+    def __init__(self) -> None:
+        super().__init__(
+            "DaysMustNotProvidedForSelectRequest",
+            "`Days` must not be provided for select requests",
+        )
+
+
+class DaysMustProvidedExceptForSelectRequest(S3ClientError):
+    code = 400
+
+    def __init__(self) -> None:
+        super().__init__(
+            "DaysMustProvidedExceptForSelectRequest",
+            "`Days` must be provided except for select requests",
+        )
diff --git a/moto/s3/models.py b/moto/s3/models.py
index b96a1581c..7f3722a6d 100644
--- a/moto/s3/models.py
+++ b/moto/s3/models.py
@@ -37,6 +37,8 @@ from moto.s3.exceptions import (
     BucketNeedsToBeNew,
     CopyObjectMustChangeSomething,
     CrossLocationLoggingProhibitted,
+    DaysMustNotProvidedForSelectRequest,
+    DaysMustProvidedExceptForSelectRequest,
     EntityTooSmall,
     HeadOnDeleteMarker,
     InvalidBucketName,
@@ -2882,14 +2884,24 @@ class S3Backend(BaseBackend, CloudWatchMetricProvider):
             for x in query_result
         ]
 
-    def restore_object(self, bucket_name: str, key_name: str, days: str) -> bool:
+    def restore_object(
+        self, bucket_name: str, key_name: str, days: Optional[str], type_: Optional[str]
+    ) -> bool:
         key = self.get_object(bucket_name, key_name)
         if not key:
             raise MissingKey
+
+        if days is None and type_ is None:
+            raise DaysMustProvidedExceptForSelectRequest()
+
+        if days and type_:
+            raise DaysMustNotProvidedForSelectRequest()
+
         if key.storage_class not in ARCHIVE_STORAGE_CLASSES:
             raise InvalidObjectState(storage_class=key.storage_class)
         had_expiry_date = key.expiry_date is not None
-        key.restore(int(days))
+        if days:
+            key.restore(int(days))
         return had_expiry_date
 
     def upload_file(self) -> None:
diff --git a/moto/s3/responses.py b/moto/s3/responses.py
index a737f74df..7e72a1c15 100644
--- a/moto/s3/responses.py
+++ b/moto/s3/responses.py
@@ -2272,10 +2272,12 @@ class S3Response(BaseResponse):
             )
 
         elif "restore" in query:
-            es = minidom.parseString(body).getElementsByTagName("Days")
-            days = es[0].childNodes[0].wholeText
+            params = xmltodict.parse(body)["RestoreRequest"]
             previously_restored = self.backend.restore_object(
-                bucket_name, key_name, days
+                bucket_name,
+                key_name,
+                params.get("Days", None),
+                params.get("Type", None),
             )
             status_code = 200 if previously_restored else 202
             return status_code, {}, ""


Test patch:
diff --git a/.github/workflows/tests_real_aws.yml b/.github/workflows/tests_real_aws.yml
index 350464194..71c401ad4 100644
--- a/.github/workflows/tests_real_aws.yml
+++ b/.github/workflows/tests_real_aws.yml
@@ -42,4 +42,4 @@ jobs:
       env:
         MOTO_TEST_ALLOW_AWS_REQUEST: ${{ true }}
       run: |
-        pytest -sv tests/test_dynamodb/ tests/test_ec2/ tests/test_iam/ tests/test_lakeformation/ tests/test_logs/ tests/test_sqs/ tests/test_ses/ tests/test_s3* tests/test_sns/ -m aws_verified
+        pytest -sv tests/test_cloudformation/ tests/test_dynamodb/ tests/test_ec2/ tests/test_iam/ tests/test_lakeformation/ tests/test_logs/ tests/test_sqs/ tests/test_ses/ tests/test_s3* tests/test_sns/ -m aws_verified
diff --git a/tests/test_cloudformation/__init__.py b/tests/test_cloudformation/__init__.py
index e69de29bb..2dc371035 100644
--- a/tests/test_cloudformation/__init__.py
+++ b/tests/test_cloudformation/__init__.py
@@ -0,0 +1,31 @@
+import os
+from functools import wraps
+
+from moto import mock_aws
+
+
+def cloudformation_aws_verified():
+    """
+    Function that is verified to work against AWS.
+    Can be run against AWS at any time by setting:
+      MOTO_TEST_ALLOW_AWS_REQUEST=true
+
+    If this environment variable is not set, the function runs in a `mock_aws` context.
+    """
+
+    def inner(func):
+        @wraps(func)
+        def pagination_wrapper():
+            allow_aws_request = (
+                os.environ.get("MOTO_TEST_ALLOW_AWS_REQUEST", "false").lower() == "true"
+            )
+
+            if allow_aws_request:
+                return func()
+            else:
+                with mock_aws():
+                    return func()
+
+        return pagination_wrapper
+
+    return inner
diff --git a/tests/test_cloudformation/test_cloudformation_custom_resources.py b/tests/test_cloudformation/test_cloudformation_custom_resources.py
index 97e1d8e9b..b5da00d38 100644
--- a/tests/test_cloudformation/test_cloudformation_custom_resources.py
+++ b/tests/test_cloudformation/test_cloudformation_custom_resources.py
@@ -150,7 +150,7 @@ def test_create_custom_lambda_resource__verify_manual_request():
     )
     stack_id = stack["StackId"]
     stack = cf.describe_stacks(StackName=stack_id)["Stacks"][0]
-    assert stack["Outputs"] == []
+    assert "Outputs" not in stack
     assert stack["StackStatus"] == "CREATE_IN_PROGRESS"
 
     callback_url = f"http://cloudformation.{region_name}.amazonaws.com/cloudformation_{region_name}/cfnresponse?stack={stack_id}"
diff --git a/tests/test_cloudformation/test_conditions.py b/tests/test_cloudformation/test_conditions.py
new file mode 100644
index 000000000..343d3403c
--- /dev/null
+++ b/tests/test_cloudformation/test_conditions.py
@@ -0,0 +1,212 @@
+from uuid import uuid4
+
+import boto3
+import pytest
+from botocore.exceptions import ClientError
+
+from . import cloudformation_aws_verified
+
+conditional_output_conditional_value = """
+Outputs:
+  roleArn:
+    Condition: RoleCondition
+    Value: !GetAtt role.Arn
+
+Parameters:
+  roleName:
+    Type: String
+    Description: Predefined role name
+
+Conditions:
+  RoleCondition: !Equals [ !Ref roleName, "myrole" ]
+
+Resources:
+  role:
+    Condition: RoleCondition
+    Type: 'AWS::IAM::Role'
+    Properties:
+      AssumeRolePolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service:
+                - ec2.amazonaws.com
+            Action:
+              - 'sts:AssumeRole'
+"""
+
+
+@pytest.mark.aws_verified
+@cloudformation_aws_verified()
+def test_conditional_output_conditional_value():
+    cf = boto3.client("cloudformation", region_name="us-east-1")
+    param = [{"ParameterKey": "roleName", "ParameterValue": "myrole"}]
+    name = f"stack{str(uuid4())[0:6]}"
+    cf.create_stack(
+        StackName=name,
+        TemplateBody=conditional_output_conditional_value,
+        Parameters=param,
+        Capabilities=["CAPABILITY_IAM"],
+    )
+    waiter = cf.get_waiter("stack_create_complete")
+    waiter.wait(StackName=name)
+
+    stack = cf.describe_stacks(StackName=name)["Stacks"][0]
+    assert stack["Outputs"][0]["OutputKey"] == "roleArn"
+
+    cf.delete_stack(StackName=name)
+
+    # verify role does not exist with different name
+    param = [{"ParameterKey": "roleName", "ParameterValue": "diffname"}]
+    cf.create_stack(
+        StackName=f"{name}2",
+        TemplateBody=conditional_output_conditional_value,
+        Parameters=param,
+        Capabilities=["CAPABILITY_IAM"],
+    )
+    waiter = cf.get_waiter("stack_create_complete")
+    waiter.wait(StackName=f"{name}2")
+    stack = cf.describe_stacks()["Stacks"][0]
+    assert "Outputs" not in stack
+
+    cf.delete_stack(StackName=f"{name}2")
+
+
+permanent_output_conditional_value = """
+Outputs:
+  roleArn:
+    Value: !GetAtt role.Arn
+
+Parameters:
+  roleName:
+    Type: String
+    Description: Predefined role name
+
+Conditions:
+  RoleCondition: !Equals [ !Ref roleName, "myrole" ]
+
+Resources:
+  role:
+    Condition: RoleCondition
+    Type: 'AWS::IAM::Role'
+    Properties:
+      AssumeRolePolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service:
+                - ec2.amazonaws.com
+            Action:
+              - 'sts:AssumeRole'
+"""
+
+
+@pytest.mark.aws_verified
+@cloudformation_aws_verified()
+def test_permanent_output_conditional_value():
+    cf = boto3.client("cloudformation", region_name="us-east-1")
+    param = [{"ParameterKey": "roleName", "ParameterValue": "myrole"}]
+
+    name = f"stack{str(uuid4())[0:6]}"
+    cf.create_stack(
+        StackName=name,
+        TemplateBody=permanent_output_conditional_value,
+        Parameters=param,
+        Capabilities=["CAPABILITY_IAM"],
+    )
+    waiter = cf.get_waiter("stack_create_complete")
+    waiter.wait(StackName=name)
+
+    # Works - because role exists
+    stack = cf.describe_stacks(StackName=name)["Stacks"][0]
+    assert stack["Outputs"][0]["OutputKey"] == "roleArn"
+
+    cf.delete_stack(StackName=name)
+
+    # Role does not exist with different name
+    # So we can't get the Output either
+    param = [{"ParameterKey": "roleName", "ParameterValue": "diffname"}]
+    with pytest.raises(ClientError) as exc:
+        cf.create_stack(
+            StackName=f"{name}2",
+            TemplateBody=permanent_output_conditional_value,
+            Parameters=param,
+            Capabilities=["CAPABILITY_IAM"],
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ValidationError"
+    assert (
+        err["Message"]
+        == "Unresolved resource dependencies [role] in the Outputs block of the template"
+    )
+
+
+conditional_output_of_permanent_value = """
+Outputs:
+  roleArn:
+    Condition: RoleCondition
+    Value: !GetAtt role.Arn
+
+Parameters:
+  roleName:
+    Type: String
+    Description: Predefined role name
+
+Conditions:
+  RoleCondition: !Equals [ !Ref roleName, "myrole" ]
+
+Resources:
+  role:
+    Type: 'AWS::IAM::Role'
+    Properties:
+      AssumeRolePolicyDocument:
+        Version: 2012-10-17
+        Statement:
+          - Effect: Allow
+            Principal:
+              Service:
+                - ec2.amazonaws.com
+            Action:
+              - 'sts:AssumeRole'
+"""
+
+
+@pytest.mark.aws_verified
+@cloudformation_aws_verified()
+def test_conditional_output_of_permanent_value():
+    cf = boto3.client("cloudformation", region_name="us-east-1")
+    param = [{"ParameterKey": "roleName", "ParameterValue": "myrole"}]
+
+    name = f"stack{str(uuid4())[0:6]}"
+    cf.create_stack(
+        StackName=name,
+        TemplateBody=conditional_output_of_permanent_value,
+        Parameters=param,
+        Capabilities=["CAPABILITY_IAM"],
+    )
+    waiter = cf.get_waiter("stack_create_complete")
+    waiter.wait(StackName=name)
+
+    stack = cf.describe_stacks(StackName=name)["Stacks"][0]
+    assert stack["Outputs"][0]["OutputKey"] == "roleArn"
+
+    cf.delete_stack(StackName=name)
+
+    # verify role does not exist with different name
+    param = [{"ParameterKey": "roleName", "ParameterValue": "diffname"}]
+    cf.create_stack(
+        StackName=f"{name}2",
+        TemplateBody=conditional_output_of_permanent_value,
+        Parameters=param,
+        Capabilities=["CAPABILITY_IAM"],
+    )
+    waiter = cf.get_waiter("stack_create_complete")
+    waiter.wait(StackName=f"{name}2")
+    stack = cf.describe_stacks(StackName=f"{name}2")["Stacks"][0]
+    # The Role does exist
+    # But Output is conditional, and just doesn't return anything
+    assert "Outputs" not in stack
+
+    cf.delete_stack(StackName=f"{name}2")
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
index 1ff151bfb..ee4691d99 100644
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -6,6 +6,7 @@ import json
 import os
 import random
 import re
+import time
 import uuid
 from unittest import SkipTest, mock
 
@@ -2346,6 +2347,65 @@ def test_admin_get_missing_user_with_username_attributes():
     assert err["Message"] == "User does not exist."
 
 
+@mock_aws
+def test_new_user_contains_same_created_and_updated_dates():
+    conn = boto3.client("cognito-idp", "us-west-2")
+
+    username = "test@example.com"
+    user_pool_id = conn.create_user_pool(
+        PoolName=str(uuid.uuid4()), UsernameAttributes=["email"]
+    )["UserPool"]["Id"]
+
+    resp = conn.admin_create_user(UserPoolId=user_pool_id, Username=username)
+    new_user = resp["User"]
+
+    assert new_user["UserCreateDate"] == new_user["UserLastModifiedDate"]
+
+
+@mock_aws
+def test_update_user_attributes_also_changes_last_modified_date():
+    conn = boto3.client("cognito-idp", "us-west-2")
+
+    username = "test@example.com"
+    user_pool_id = conn.create_user_pool(
+        PoolName=str(uuid.uuid4()), UsernameAttributes=["email"]
+    )["UserPool"]["Id"]
+    conn.admin_create_user(UserPoolId=user_pool_id, Username=username)
+
+    time.sleep(1)  # Wait enough to account for a different in the time format
+    conn.admin_update_user_attributes(
+        UserPoolId=user_pool_id,
+        Username=username,
+        UserAttributes=[{"Name": "phone_number", "Value": "+123456789"}],
+    )
+
+    user = conn.admin_get_user(UserPoolId=user_pool_id, Username=username)
+    assert user["UserCreateDate"] < user["UserLastModifiedDate"]
+
+
+@mock_aws
+def test_delete_user_attributes_also_changes_last_modified_date():
+    conn = boto3.client("cognito-idp", "us-west-2")
+
+    username = "test@example.com"
+    user_pool_id = conn.create_user_pool(
+        PoolName=str(uuid.uuid4()), UsernameAttributes=["email"]
+    )["UserPool"]["Id"]
+    conn.admin_create_user(
+        UserPoolId=user_pool_id,
+        Username=username,
+        UserAttributes=[{"Name": "phone_number", "Value": "+123456789"}],
+    )
+
+    time.sleep(1)  # Wait enough to account for a different in the time format
+    conn.admin_delete_user_attributes(
+        UserPoolId=user_pool_id, Username=username, UserAttributeNames=["phone_number"]
+    )
+
+    user = conn.admin_get_user(UserPoolId=user_pool_id, Username=username)
+    assert user["UserCreateDate"] < user["UserLastModifiedDate"]
+
+
 @mock_aws
 def test_get_user():
     conn = boto3.client("cognito-idp", "us-west-2")
diff --git a/tests/test_s3/test_s3.py b/tests/test_s3/test_s3.py
index cbb5c1d16..7fb2664ab 100644
--- a/tests/test_s3/test_s3.py
+++ b/tests/test_s3/test_s3.py
@@ -627,6 +627,32 @@ def test_cannot_restore_standard_class_object():
     )
 
 
+@mock_aws
+def test_restore_object_invalid_request_params():
+    if not settings.TEST_DECORATOR_MODE:
+        raise SkipTest("Can't set transition directly in ServerMode")
+
+    s3_resource = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)
+    bucket = s3_resource.Bucket("foobar")
+    bucket.create()
+
+    key = bucket.put_object(Key="the-key", Body=b"somedata", StorageClass="GLACIER")
+
+    # `Days` must be provided except for select requests
+    with pytest.raises(ClientError) as exc:
+        key.restore_object(RestoreRequest={})
+    err = exc.value.response["Error"]
+    assert err["Code"] == "DaysMustProvidedExceptForSelectRequest"
+    assert err["Message"] == "`Days` must be provided except for select requests"
+
+    # `Days` must not be provided for select requests
+    with pytest.raises(ClientError) as exc:
+        key.restore_object(RestoreRequest={"Days": 1, "Type": "SELECT"})
+    err = exc.value.response["Error"]
+    assert err["Code"] == "DaysMustNotProvidedForSelectRequest"
+    assert err["Message"] == "`Days` must not be provided for select requests"
+
+
 @mock_aws
 def test_get_versioning_status():
     s3_resource = boto3.resource("s3", region_name=DEFAULT_REGION_NAME)