Repository: getmoto/moto

Issue:
Allow FIFO SQS queues as event source for Lambda invocations
Hi! :)

I'm having some issues with the SQS module of the moto library. I created a Lambda function that would be invoked when a message is inserted into a FIFO SQS queue. It is supported by [AWS since 2019](https://aws.amazon.com/blogs/compute/new-for-aws-lambda-sqs-fifo-as-an-event-source/) but it doesn't seem to be supported by moto yet.

I've looked at the code and [it seems to be a simple condition](https://github.com/getmoto/moto/blob/master/moto/awslambda/models.py#L1648) that prevents the correct behavior. Removing the `if` condition and making the `else` content execute by default should resolve the issue in my opinion. I managed to test it locally and it successfully invoked the Lambda function.

I am using the latest version of moto (v4.1.3) as well as the latest version of boto3 (v1.26.78).

Below is an example of the correct behavior.

## Base code

```python
SQS_QUEUE_NAME = "example.fifo"

sqs_client = boto3.client("sqs", region_name=REGION)
lambda_client = boto3.client("lambda", region_name=REGION)

queue = self.client_sqs.create_queue(QueueName=SQS_QUEUE_NAME,
                                     Attributes={"FifoQueue": "true"})

# We'll try to get the ARN of the queue
SINK_QUEUE_ARN = self.client_sqs.get_queue_attributes(
    QueueUrl=queue.get("QueueUrl"),
    AttributeNames=["QueueArn"]
).get("Attributes").get("QueueArn")

# lambda role creation ...

lambda_client.create_function(
    FunctionName=LAMBDA_FUNCTION_NAME,
    Runtime="python3.9",
    Role=ROLE_ARN,
    Handler="lambda_handler.lambda_handler",
    Code={
        # ...
    },
    Timeout=300,
    MemorySize=128,
    Publish=True
)

# we subscribe lambda to the SQS queue
lambda_client.create_event_source_mapping( # <-- will throw an error
    EventSourceArn=SINK_QUEUE_ARN,
    FunctionName=LAMBDA_FUNCTION_NAME,
    StartingPosition="LATEST",
    BatchSize=10,
    Enabled=True
)

```

## What we expect
The FIFO SQS queue should be successfully mapped to the Lambda function. When a message is sent to it, the Lambda should be invoked.

## What we get
```
E botocore.errorfactory.InvalidParameterValueException: An error occurred (InvalidParameterValueException) when calling the CreateEventSourceMapping operation: <?xml version="1.0" encoding="UTF-8"?>
E <ErrorResponse>
E   <Errors>
E     <Error>
E       <Code>InvalidParameterValueException</Code>
E         <Message><![CDATA[arn:aws:sqs:us-east-1:123456789012:example.fifo is FIFO]]></Message>
E     </Error>
E   </Errors>
E   <RequestId>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE</RequestId>
E </ErrorResponse>
```


Discussion:
Hi @shamelin, thanks for raising this! That looks like the correct fix to me - I'll mark it as an enhancement to add proper support for this.
Thank you so much! :) Cheers!

Resolving patch:
diff --git a/moto/awslambda/models.py b/moto/awslambda/models.py
--- a/moto/awslambda/models.py
+++ b/moto/awslambda/models.py
@@ -1643,19 +1643,15 @@ def create_event_source_mapping(self, spec: Dict[str, Any]) -> EventSourceMappin
                     raise RESTError(
                         "ResourceConflictException", "The resource already exists."
                     )
-                if queue.fifo_queue:
-                    raise RESTError(
-                        "InvalidParameterValueException", f"{queue.queue_arn} is FIFO"
-                    )
-                else:
-                    spec.update({"FunctionArn": func.function_arn})
-                    esm = EventSourceMapping(spec)
-                    self._event_source_mappings[esm.uuid] = esm
+                spec.update({"FunctionArn": func.function_arn})
+                esm = EventSourceMapping(spec)
+                self._event_source_mappings[esm.uuid] = esm
 
-                    # Set backend function on queue
-                    queue.lambda_event_source_mappings[esm.function_arn] = esm
+                # Set backend function on queue
+                queue.lambda_event_source_mappings[esm.function_arn] = esm
+
+                return esm
 
-                    return esm
         ddbstream_backend = dynamodbstreams_backends[self.account_id][self.region_name]
         ddb_backend = dynamodb_backends[self.account_id][self.region_name]
         for stream in json.loads(ddbstream_backend.list_streams())["Streams"]:
@@ -1792,6 +1788,14 @@ def _send_sqs_message(
                 }
             ]
         }
+        if queue_arn.endswith(".fifo"):
+            # Messages from FIFO queue have additional attributes
+            event["Records"][0]["attributes"].update(
+                {
+                    "MessageGroupId": message.group_id,
+                    "MessageDeduplicationId": message.deduplication_id,
+                }
+            )
 
         request_headers: Dict[str, Any] = {}
         response_headers: Dict[str, Any] = {}


Test patch:
diff --git a/tests/test_sqs/test_integration.py b/tests/test_sqs/test_integration.py
deleted file mode 100644
--- a/tests/test_sqs/test_integration.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import json
-import time
-import uuid
-import boto3
-import sure  # noqa # pylint: disable=unused-import
-from moto import mock_sqs, mock_lambda, mock_logs
-
-from tests.test_awslambda.test_lambda import get_test_zip_file1, get_role_name
-
-
-@mock_logs
-@mock_lambda
-@mock_sqs
-def test_invoke_function_from_sqs_exception():
-    logs_conn = boto3.client("logs", region_name="us-east-1")
-    sqs = boto3.resource("sqs", region_name="us-east-1")
-    queue = sqs.create_queue(QueueName="test-sqs-queue1")
-
-    conn = boto3.client("lambda", region_name="us-east-1")
-    func = conn.create_function(
-        FunctionName="testFunction",
-        Runtime="python2.7",
-        Role=get_role_name(),
-        Handler="lambda_function.lambda_handler",
-        Code={"ZipFile": get_test_zip_file1()},
-        Description="test lambda function",
-        Timeout=3,
-        MemorySize=128,
-        Publish=True,
-    )
-
-    response = conn.create_event_source_mapping(
-        EventSourceArn=queue.attributes["QueueArn"], FunctionName=func["FunctionArn"]
-    )
-
-    assert response["EventSourceArn"] == queue.attributes["QueueArn"]
-    assert response["State"] == "Enabled"
-
-    entries = [
-        {
-            "Id": "1",
-            "MessageBody": json.dumps({"uuid": str(uuid.uuid4()), "test": "test"}),
-        }
-    ]
-
-    queue.send_messages(Entries=entries)
-
-    start = time.time()
-    while (time.time() - start) < 30:
-        result = logs_conn.describe_log_streams(logGroupName="/aws/lambda/testFunction")
-        log_streams = result.get("logStreams")
-        if not log_streams:
-            time.sleep(1)
-            continue
-        assert len(log_streams) >= 1
-
-        result = logs_conn.get_log_events(
-            logGroupName="/aws/lambda/testFunction",
-            logStreamName=log_streams[0]["logStreamName"],
-        )
-        for event in result.get("events"):
-            if "custom log event" in event["message"]:
-                return
-        time.sleep(1)
-
-    assert False, "Test Failed"
diff --git a/tests/test_sqs/test_sqs_integration.py b/tests/test_sqs/test_sqs_integration.py
--- a/tests/test_sqs/test_sqs_integration.py
+++ b/tests/test_sqs/test_sqs_integration.py
@@ -5,19 +5,22 @@
 
 from moto import mock_lambda, mock_sqs, mock_logs
 from tests.test_awslambda.test_lambda import get_test_zip_file1, get_role_name
+from tests.test_awslambda.utilities import get_test_zip_file_print_event
 
 
 @mock_logs
 @mock_lambda
 @mock_sqs
-def test_invoke_function_from_sqs_exception():
+def test_invoke_function_from_sqs_queue():
     logs_conn = boto3.client("logs", region_name="us-east-1")
     sqs = boto3.resource("sqs", region_name="us-east-1")
-    queue = sqs.create_queue(QueueName="test-sqs-queue1")
+    queue_name = str(uuid.uuid4())[0:6]
+    queue = sqs.create_queue(QueueName=queue_name)
 
+    fn_name = str(uuid.uuid4())[0:6]
     conn = boto3.client("lambda", region_name="us-east-1")
     func = conn.create_function(
-        FunctionName="testFunction",
+        FunctionName=fn_name,
         Runtime="python2.7",
         Role=get_role_name(),
         Handler="lambda_function.lambda_handler",
@@ -46,20 +49,89 @@ def test_invoke_function_from_sqs_exception():
 
     start = time.time()
     while (time.time() - start) < 30:
-        result = logs_conn.describe_log_streams(logGroupName="/aws/lambda/testFunction")
+        result = logs_conn.describe_log_streams(logGroupName=f"/aws/lambda/{fn_name}")
         log_streams = result.get("logStreams")
         if not log_streams:
-            time.sleep(1)
+            time.sleep(0.5)
             continue
         assert len(log_streams) >= 1
 
         result = logs_conn.get_log_events(
-            logGroupName="/aws/lambda/testFunction",
+            logGroupName=f"/aws/lambda/{fn_name}",
             logStreamName=log_streams[0]["logStreamName"],
         )
         for event in result.get("events"):
             if "custom log event" in event["message"]:
                 return
-        time.sleep(1)
+        time.sleep(0.5)
+
+    assert False, "Test Failed"
+
+
+@mock_logs
+@mock_lambda
+@mock_sqs
+def test_invoke_function_from_sqs_fifo_queue():
+    """
+    Create a FIFO Queue
+    Create a Lambda Function
+    Send a message to the queue
+    Verify the Lambda has been invoked with the correct message body
+       (By checking the resulting log files)
+    """
+    logs_conn = boto3.client("logs", region_name="us-east-1")
+    sqs = boto3.resource("sqs", region_name="us-east-1")
+    queue_name = str(uuid.uuid4())[0:6] + ".fifo"
+    queue = sqs.create_queue(QueueName=queue_name, Attributes={"FifoQueue": "true"})
+
+    fn_name = str(uuid.uuid4())[0:6]
+    conn = boto3.client("lambda", region_name="us-east-1")
+    func = conn.create_function(
+        FunctionName=fn_name,
+        Runtime="python3.7",
+        Role=get_role_name(),
+        Handler="lambda_function.lambda_handler",
+        Code={"ZipFile": get_test_zip_file_print_event()},
+        Timeout=3,
+        MemorySize=128,
+        Publish=True,
+    )
+
+    response = conn.create_event_source_mapping(
+        EventSourceArn=queue.attributes["QueueArn"], FunctionName=func["FunctionArn"]
+    )
+
+    assert response["EventSourceArn"] == queue.attributes["QueueArn"]
+    assert response["State"] == "Enabled"
+
+    entries = [{"Id": "1", "MessageBody": "some body", "MessageGroupId": "mg1"}]
+
+    queue.send_messages(Entries=entries)
+
+    start = time.time()
+    while (time.time() - start) < 30:
+        result = logs_conn.describe_log_streams(logGroupName=f"/aws/lambda/{fn_name}")
+        log_streams = result.get("logStreams")
+        if not log_streams:
+            time.sleep(0.5)
+            continue
+        assert len(log_streams) >= 1
+
+        result = logs_conn.get_log_events(
+            logGroupName=f"/aws/lambda/{fn_name}",
+            logStreamName=log_streams[0]["logStreamName"],
+        )
+
+        for event in result.get("events"):
+            try:
+                body = json.loads(event.get("message"))
+                atts = body["Records"][0]["attributes"]
+                atts.should.have.key("MessageGroupId").equals("mg1")
+                atts.should.have.key("MessageDeduplicationId")
+                return
+            except:  # noqa: E722 Do not use bare except
+                pass
+
+        time.sleep(0.5)
 
     assert False, "Test Failed"