Repository: getmoto/moto
Pull request: added code to resolve issue #5372
Dear sir,
I have written code to resolve the issue number #5372. I have made following changes for the same:
1. models.py file (moto/moto/sqs/models.py): 
   Line number 47: Micro defined as MAXIMUM_MESSAGE_DELAY = 900
   Line number 808: In send_message function, I have put condition to check if delay_seconds is greater than MAXIMUM_MESSAGE_DELAY. If it is greater then InvalidParameterValue exception is raised with message described in the issue

2. test_sqs.py file (moto/tests/test_sqs.py):
   Line number 3125: Written test to check if above approach works for 2 messages that are given in the issue.  One message is sent with delayseconds =180 which has to be sent successfully and another message is sent with delayseconds=1800 which should raise an exception. 
When I run the test for sqs module, test test_message_delay_is_more_than_15_minutes() is passed.
Kindly Let me know if any further changes are required in this issue.

Thank you,
Rahi Bhensdadia

Problem statement:
SQS allows sending of messages with delays of longer than 15 minutes
# Versions

`moto`: 3.1.16
`boto3`: 1.21.46

# Code
```
response = boto3.client('sqs').send_message_batch(
    QueueUrl='the-queue-url,
    Entries=[
        {
            'Id': '1'
            'MessageBody': 'foo',
            'DelaySeconds': 180
        },
        {
            'Id': '2'
            'MessageBody': 'foo',
            'DelaySeconds': 1800
        }
    ]
)
```

# Expectation

An response with a failure for the second message:
```
{'Successful': [{'Id': '180', 'MessageId': '...', 'MD5OfMessageBody': '...'}], 'Failed': [{'Id': '1800', 'SenderFault': True, 'Code': 'InvalidParameterValue', 'Message': 'Value 1800 for parameter DelaySeconds is invalid. Reason: DelaySeconds must be >= 0 and <= 900.'}], 'ResponseMetadata': {'RequestId': 'a59c20de-034d-5922-b608-7bb267435efc', 'HTTPStatusCode': 200, 'HTTPHeaders': {'x-amzn-requestid': 'a59c20de-034d-5922-b608-7bb267435efc', 'date': 'Tue, 09 Aug 2022 13:31:58 GMT', 'content-type': 'text/xml', 'content-length': '714'}, 'RetryAttempts': 0}}
```

# Reality
```
{'Successful': [{'Id': '180', 'MessageId': '...', 'MD5OfMessageBody': '...'}, {'Id': '1800', 'MessageId': '...', 'MD5OfMessageBody': '...'}], 'ResponseMetadata': {'RequestId': '5ZAAK7GFDHO7FZSZVFMFD36QVM5TDZWPYRKFXL26GTJME1WU82KV', 'HTTPStatusCode': 200, 'HTTPHeaders': {'server': 'amazon.com', 'x-amzn-requestid': '5ZAAK7GFDHO7FZSZVFMFD36QVM5TDZWPYRKFXL26GTJME1WU82KV', 'x-amz-crc32': '3107167176'}, 'RetryAttempts': 0}}
```

Hints from issue discussion:
Thanks for raising this @paulmdavies - marking it as an enhancement to add this validation.
I want to work on this issue. I found it interesting and I think I can fix this. It would be great if I get the approval from the owner.
PR's are always welcome, @RahiBhensdadia! There is a guide here with some tips that may be helpful for contributors: http://docs.getmoto.org/en/latest/docs/contributing/index.html

Please let us know if you need any help with either issue.
Thank you sir for your kind consideration. I will go through this documentation and start working on the enhancement. I'll approach if I need help.

Commit message: added code to resolve issue #5372

Patch to review:
diff --git a/moto/sqs/models.py b/moto/sqs/models.py
index 0d4f93cc14cc..dcdde561bd7f 100644
--- a/moto/sqs/models.py
+++ b/moto/sqs/models.py
@@ -44,6 +44,8 @@
 MAXIMUM_MESSAGE_SIZE_ATTR_LOWER_BOUND = 1024
 MAXIMUM_MESSAGE_SIZE_ATTR_UPPER_BOUND = MAXIMUM_MESSAGE_LENGTH
 
+MAXIMUM_MESSAGE_DELAY = 900
+
 TRANSPORT_TYPE_ENCODINGS = {
     "String": b"\x01",
     "Binary": b"\x02",
@@ -799,11 +801,16 @@ def send_message(
         if message_attributes:
             message.message_attributes = message_attributes
 
+        if delay_seconds > MAXIMUM_MESSAGE_DELAY:
+            invalidDelay = True
+        else:
+            invalidDelay = False
+
         message.mark_sent(delay_seconds=delay_seconds)
 
         queue.add_message(message)
 
-        return message
+        return message, invalidDelay
 
     def send_message_batch(self, queue_name, entries):
         self.get_queue(queue_name)
@@ -834,9 +841,11 @@ def send_message_batch(self, queue_name, entries):
             raise TooManyEntriesInBatchRequest(len(entries))
 
         messages = []
+
+        failedInvalidDelay = []
         for entry in entries.values():
             # Loop through looking for messages
-            message = self.send_message(
+            message, invalidDelay = self.send_message(
                 queue_name,
                 entry["MessageBody"],
                 message_attributes=entry["MessageAttributes"],
@@ -846,9 +855,12 @@ def send_message_batch(self, queue_name, entries):
             )
             message.user_id = entry["Id"]
 
-            messages.append(message)
+            if not invalidDelay:
+                messages.append(message)
+            else:
+                failedInvalidDelay.append(entry)
 
-        return messages
+        return messages, failedInvalidDelay
 
     def _get_first_duplicate_id(self, ids):
         unique_ids = set()
diff --git a/moto/sqs/responses.py b/moto/sqs/responses.py
index c8546b2abc4f..6235e6f474e6 100644
--- a/moto/sqs/responses.py
+++ b/moto/sqs/responses.py
@@ -299,10 +299,27 @@ def send_message_batch(self):
         if entries == {}:
             raise EmptyBatchRequest()
 
-        messages = self.sqs_backend.send_message_batch(queue_name, entries)
+        # UPDATED Line 302-317, 320
+        messages, failedInvalidDelay = self.sqs_backend.send_message_batch(
+            queue_name, entries
+        )
+        # print(f'Failed due to invalid delay: {failedInvalidDelay}')
+
+        errors = []
+        for entry in failedInvalidDelay:
+            errors.append(
+                {
+                    "Id": entry["Id"],
+                    "SenderFault": "true",
+                    "Code": "InvalidParameterValue",
+                    "Message": "Value 1800 for parameter DelaySeconds is invalid. Reason: DelaySeconds must be &gt;= 0 and &lt;= 900.",
+                }
+            )
+
+        # print(f'Errors: {errors}')
 
         template = self.response_template(SEND_MESSAGE_BATCH_RESPONSE)
-        return template.render(messages=messages)
+        return template.render(messages=messages, errors=errors)
 
     def delete_message(self):
         queue_name = self._get_queue_name()
@@ -650,6 +667,7 @@ def list_queue_tags(self):
   </ResponseMetadata>
 </ReceiveMessageResponse>"""
 
+# UPDATED Line 681-688
 SEND_MESSAGE_BATCH_RESPONSE = """<SendMessageBatchResponse>
 <SendMessageBatchResult>
     {% for message in messages %}
@@ -662,6 +680,14 @@ def list_queue_tags(self):
             {% endif %}
         </SendMessageBatchResultEntry>
     {% endfor %}
+    {% for error_dict in errors %}
+    <BatchResultErrorEntry>
+        <Id>{{ error_dict['Id'] }}</Id>
+        <Code>{{ error_dict['Code'] }}</Code>
+        <Message>{{ error_dict['Message'] }}</Message>
+        <SenderFault>{{ error_dict['SenderFault'] }}</SenderFault>
+    </BatchResultErrorEntry>
+    {% endfor %}
 </SendMessageBatchResult>
 <ResponseMetadata>
     <RequestId></RequestId>
diff --git a/tests/test_sqs/test_sqs.py b/tests/test_sqs/test_sqs.py
index ad215e7360b2..c6ca9e584d34 100644
--- a/tests/test_sqs/test_sqs.py
+++ b/tests/test_sqs/test_sqs.py
@@ -3115,3 +3115,44 @@ def test_message_has_windows_return():
     messages = queue.receive_messages()
     messages.should.have.length_of(1)
     messages[0].body.should.match(message)
+
+
+@mock_sqs
+def test_message_delay_is_more_than_15_minutes():
+    client = boto3.client("sqs", region_name="us-east-1")
+    response = client.create_queue(
+        QueueName=f"{str(uuid4())[0:6]}.fifo", Attributes={"FifoQueue": "true"}
+    )
+    queue_url = response["QueueUrl"]
+
+    msgs = client.send_message_batch(
+        QueueUrl=queue_url,
+        Entries=[
+            {
+                "Id": "1",
+                "MessageBody": "foo",
+                "DelaySeconds": 180,
+                "MessageGroupId": "message_group_id_1",
+            },
+            {
+                "Id": "2",
+                "MessageBody": "foo",
+                "DelaySeconds": 1800,
+                "MessageGroupId": "message_group_id_1",
+            },
+            # {
+            #     "Id": "3",
+            #     "MessageBody": "foo",
+            #     "DelaySeconds": 100,
+            #     "MessageGroupId": "message_group_id_1",
+            # },
+            # {
+            #     "Id": "4",
+            #     "MessageBody": "foo",
+            #     "DelaySeconds": 2800,
+            #     "MessageGroupId": "message_group_id_1",
+            # },
+        ],
+    )
+
+    print(msgs)


Code review comments:
diff_hunk: @@ -3115,3 +3115,44 @@ def test_message_has_windows_return():
     messages = queue.receive_messages()
     messages.should.have.length_of(1)
     messages[0].body.should.match(message)
+
+
+@mock_sqs
+def test_message_delay_is_more_than_15_minutes():
+    client = boto3.client("sqs", region_name="us-east-1")
+    response = client.create_queue(
+        QueueName=f"{str(uuid4())[0:6]}.fifo", Attributes={"FifoQueue": "true"}
+    )
+    queue_url = response["QueueUrl"]
+
+    msgs = client.send_message_batch(
+        QueueUrl=queue_url,
+        Entries=[
+            {
+                "Id": "1",
+                "MessageBody": "foo",
+                "DelaySeconds": 180,
+                "MessageGroupId": "message_group_id_1",
+            },
+            {
+                "Id": "2",
+                "MessageBody": "foo",
+                "DelaySeconds": 1800,
+                "MessageGroupId": "message_group_id_1",
+            },
+            # {
+            #     "Id": "3",
+            #     "MessageBody": "foo",
+            #     "DelaySeconds": 100,
+            #     "MessageGroupId": "message_group_id_1",
+            # },
+            # {
+            #     "Id": "4",
+            #     "MessageBody": "foo",
+            #     "DelaySeconds": 2800,
+            #     "MessageGroupId": "message_group_id_1",
+            # },
+        ],
+    )
+
+    print(msgs)
path: tests/test_sqs/test_sqs.py
text: @user1:
Can we add some assertions here, that `msgs` contains the correct response?

@author:
for this part, what exact assertions you are expecting. Can you please elaborate

@author:
I have done something like this:

@user2
def test_message_delay_is_more_than_15_minutes():
    client = boto3.client("sqs", region_name="us-east-1")
    response = client.create_queue(
        QueueName=f"{str(uuid4())[0:6]}.fifo", Attributes={"FifoQueue": "true"}
    )
    queue_url = response["QueueUrl"]

    response = client.send_message_batch(
        QueueUrl=queue_url,
        Entries=[
            {
                "Id": "id_1",
                "MessageBody": "body_1",
                "DelaySeconds": 10,
                "MessageAttributes": {
                    "attribute_name_1": {
                        "StringValue": "attribute_value_1",
                        "DataType": "String",
                    }
                },
                "MessageGroupId": "message_group_id_1",
                "MessageDeduplicationId": "message_deduplication_id_1",
            },
            {
                "Id": "id_2",
                "MessageBody": "body_2",
                "DelaySeconds": 1800,
                "MessageAttributes": {
                    "attribute_name_2": {"StringValue": "123", "DataType": "Number"}
                },
                "MessageGroupId": "message_group_id_2",
                "MessageDeduplicationId": "message_deduplication_id_2",
            },
        ],
    )

    sorted([entry["Id"] for entry in response["Successful"]]).should.equal(["id_1"])

    sorted([entry["Id"] for entry in response["Failed"]]).should.equal(["id_2"])

    print(response)

    time.sleep(11)

    response = client.receive_message(
        QueueUrl=queue_url,
        MaxNumberOfMessages=10,
        MessageAttributeNames=["attribute_name_1", "attribute_name_2"],
        AttributeNames=["MessageDeduplicationId", "MessageGroupId"],
    )

    response["Messages"].should.have.length_of(1)
    response["Messages"][0]["Body"].should.equal("body_1")
    response["Messages"][0]["MessageAttributes"].should.equal(
        {"attribute_name_1": {"StringValue": "attribute_value_1", "DataType": "String"}}
    )
    response["Messages"][0]["Attributes"]["MessageGroupId"].should.equal(
        "message_group_id_1"
    )
    response["Messages"][0]["Attributes"]["MessageDeduplicationId"].should.equal(
        "message_deduplication_id_1"
    )

I am receiving the message using receive_message function and then checking its attributes. Here the problem is - if delay seconds is i then we have to wait for i+1 seconds to receive the message then only we can do some checking. Is it ok or you are expecting something else

@user1:
Yes, I think that's fine. 

To speed things up, we can just reduce the DelaySeconds-parameter - e.g., if we can set it to 2, so we only have to wait 3 seconds.

@author:
OK sir I have raised PR with required changes. Let me know if it works

diff_hunk: @@ -834,9 +841,11 @@ def send_message_batch(self, queue_name, entries):
             raise TooManyEntriesInBatchRequest(len(entries))
 
         messages = []
+
+        failedInvalidDelay = []
         for entry in entries.values():
             # Loop through looking for messages
-            message = self.send_message(
+            message, invalidDelay = self.send_message(
path: moto/sqs/models.py
text: @user1:
The `DelaySeconds`-parameter can also part of the `boto3.client("sqs").send_message(..)`-call, so IMO we should stick with the initial implementation. Raising the error in `def send_message()` ensures that the validation happens in both scenarios, when calling `send_message` and `send_message_batch`.

That just means that `send_message_batch` should catch the error, something like:
```
try:
    message = self.send_message(..)
    messages.append(message)
except ..:
    failedInvalidDelay.append(entry)
```

@author:
Ok sir this part I understood and implemented it.