Repository: ansible-collections/amazon.aws

Issue:
[lambda_event] Aws SSO with function Name Invalid ARN
### Summary

As we can only pass function name and not arn like i see in documentation we can't deploy using AWS sso because the creation of the ARN from the function name say Validation Error:
```
[lambda_event](fatal: [localhost]: FAILED! => {"changed": false, "msg": "Error retrieving stream event notification configuration: An error occurred (ValidationException) when calling the ListEventSourceMappings operation: 1 validation error detected: Value 'arn:aws:lambda:eu-west-3::function:function-name' at 'functionName' failed to satisfy constraint: Member must satisfy regular expression pattern: (arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?"})
```


### Issue Type

Bug Report

### Component Name

lambda_event

### Ansible Version

```console (paste below)
$ ansible --version
ansible [core 2.13.13]
  config file = None
  configured module search path = ['/home/rvolpi/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/rvolpi/.local/share/virtualenvs/ops-njY6lVi6/lib/python3.10/site-packages/ansible
  ansible collection location = /home/rvolpi/.ansible/collections:/usr/share/ansible/collections
  executable location = /home/rvolpi/.local/share/virtualenvs/ops-njY6lVi6/bin/ansible
  python version = 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0]
  jinja version = 3.1.2
  libyaml = True

```


### Collection Versions

```console (paste below)
$ ansible-galaxy collection list

# /home/rvolpi/.ansible/collections/ansible_collections
Collection               Version
------------------------ -------
amazon.aws               7.0.0  
ansible.posix            1.5.4  
community.aws            7.0.0  
community.crypto         2.15.0 
community.general        8.0.2  
community.mysql          3.7.2  
community.vmware         4.0.0  
devsec.hardening         8.8.0  
serdigital64.application 2.1.1  
serdigital64.automation  3.2.2  
serdigital64.backup      2.1.1  
serdigital64.development 2.1.2  
serdigital64.devops      2.1.3  
serdigital64.security    2.1.2  
serdigital64.system      2.1.2  

# /home/rvolpi/.local/share/virtualenvs/ops-njY6lVi6/lib/python3.10/site-packages/ansible_collections
Collection                    Version
----------------------------- -------
amazon.aws                    3.5.0  
ansible.netcommon             3.1.3  
ansible.posix                 1.4.0  
ansible.utils                 2.8.0  
ansible.windows               1.12.0 
arista.eos                    5.0.1  
awx.awx                       21.10.0
azure.azcollection            1.14.0 
check_point.mgmt              2.3.0  
chocolatey.chocolatey         1.3.1  
cisco.aci                     2.3.0  
cisco.asa                     3.1.0  
cisco.dnac                    6.6.1  
cisco.intersight              1.0.22 
cisco.ios                     3.3.2  
cisco.iosxr                   3.3.1  
cisco.ise                     2.5.9  
cisco.meraki                  2.13.0 
cisco.mso                     2.1.0  
cisco.nso                     1.0.3  
cisco.nxos                    3.2.0  
cisco.ucs                     1.8.0  
cloud.common                  2.1.2  
cloudscale_ch.cloud           2.2.3  
community.aws                 3.6.0  
community.azure               1.1.0  
community.ciscosmb            1.0.5  
community.crypto              2.9.0  
community.digitalocean        1.22.0 
community.dns                 2.4.2  
community.docker              2.7.3  
community.fortios             1.0.0  
community.general             5.8.3  
community.google              1.0.0  
community.grafana             1.5.3  
community.hashi_vault         3.4.0  
community.hrobot              1.6.0  
community.libvirt             1.2.0  
community.mongodb             1.4.2  
community.mysql               3.5.1  
community.network             4.0.2  
community.okd                 2.2.0  
community.postgresql          2.3.1  
community.proxysql            1.4.0  
community.rabbitmq            1.2.3  
community.routeros            2.5.0  
community.sap                 1.0.0  
community.sap_libs            1.4.0  
community.skydive             1.0.0  
community.sops                1.5.0  
community.vmware              2.10.2 
community.windows             1.11.1 
community.zabbix              1.9.0  
containers.podman             1.10.1 
cyberark.conjur               1.2.0  
cyberark.pas                  1.0.14 
dellemc.enterprise_sonic      1.1.2  
dellemc.openmanage            5.5.0  
dellemc.os10                  1.1.1  
dellemc.os6                   1.0.7  
dellemc.os9                   1.0.4  
f5networks.f5_modules         1.21.0 
fortinet.fortimanager         2.1.7  
fortinet.fortios              2.2.1  
frr.frr                       2.0.0  
gluster.gluster               1.0.2  
google.cloud                  1.0.2  
hetzner.hcloud                1.9.0  
hpe.nimble                    1.1.4  
ibm.qradar                    2.1.0  
ibm.spectrum_virtualize       1.10.0 
infinidat.infinibox           1.3.12 
infoblox.nios_modules         1.4.1  
inspur.ispim                  1.2.0  
inspur.sm                     2.3.0  
junipernetworks.junos         3.1.0  
kubernetes.core               2.3.2  
lowlydba.sqlserver            1.2.0  
mellanox.onyx                 1.0.0  
netapp.aws                    21.7.0 
netapp.azure                  21.10.0
netapp.cloudmanager           21.21.0
netapp.elementsw              21.7.0 
netapp.ontap                  21.24.1
netapp.storagegrid            21.11.1
netapp.um_info                21.8.0 
netapp_eseries.santricity     1.3.1  
netbox.netbox                 3.9.0  
ngine_io.cloudstack           2.3.0  
ngine_io.exoscale             1.0.0  
ngine_io.vultr                1.1.2  
openstack.cloud               1.10.0 
openvswitch.openvswitch       2.1.0  
ovirt.ovirt                   2.4.1  
purestorage.flasharray        1.15.0 
purestorage.flashblade        1.10.0 
purestorage.fusion            1.2.0  
sensu.sensu_go                1.13.1 
servicenow.servicenow         1.0.6  
splunk.es                     2.1.0  
t_systems_mms.icinga_director 1.31.4 
theforeman.foreman            3.7.0  
vmware.vmware_rest            2.2.0  
vultr.cloud                   1.3.1  
vyos.vyos                     3.0.1  
wti.remote                    1.0.4 
```


### AWS SDK versions

```console (paste below)
$ pip show boto boto3 botocore
Name: boto
Version: 2.49.0
Summary: Amazon Web Services Library
Home-page: https://github.com/boto/boto/
Author: Mitch Garnaat
Author-email: mitch@garnaat.com
License: MIT
Location: /home/rvolpi/.local/share/virtualenvs/ops-njY6lVi6/lib/python3.10/site-packages
Requires: 
Required-by: 
---
Name: boto3
Version: 1.29.0
Summary: The AWS SDK for Python
Home-page: https://github.com/boto/boto3
Author: Amazon Web Services
Author-email: 
License: Apache License 2.0
Location: /home/rvolpi/.local/share/virtualenvs/ops-njY6lVi6/lib/python3.10/site-packages
Requires: botocore, jmespath, s3transfer
Required-by: checkov, cloudsplaining
---
Name: botocore
Version: 1.32.0
Summary: Low-level, data-driven core of boto 3.
Home-page: https://github.com/boto/botocore
Author: Amazon Web Services
Author-email: 
License: Apache License 2.0
Location: /home/rvolpi/.local/share/virtualenvs/ops-njY6lVi6/lib/python3.10/site-packages
Requires: jmespath, python-dateutil, urllib3
Required-by: boto3, cloudsplaining, s3transfer

```


### Configuration

```console (paste below)
$ ansible-config dump --only-changed
(None)
```


### OS / Environment

Ubuntu 22.04

### Steps to Reproduce

<!--- Paste example playbooks or commands between quotes below -->
Using [AWS SSO Systeme](https://docs.aws.amazon.com/cli/latest/userguide/sso-configure-profile-token.html)
```yaml 
  community.aws.lambda_event:
    region: "{{ region }}"
    profile: '{{ profile }}'
    state: present
    event_source: sqs
    function_name: "function-name"
    source_params:
      source_arn: "{{ queue_arn }}"
      enabled: True
```


### Expected Results

```console
TASK [functions : map queue events to lambda] **********************************
ok: [localhost]

```

### Actual Results

```console (paste below)
TASK [functions : map queue events to lambda] **********************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "Error retrieving stream event notification configuration: An error occurred (ValidationException) when calling the ListEventSourceMappings operation: 1 validation error detected: Value 'arn:aws:lambda:eu-west-3::function:function-name' at 'functionName' failed to satisfy constraint: Member must satisfy regular expression pattern: (arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?"}

```


### Code of Conduct

- [X] I agree to follow the Ansible Code of Conduct


Discussion:

Resolving patch:
diff --git a/plugins/modules/lambda_event.py b/plugins/modules/lambda_event.py
--- a/plugins/modules/lambda_event.py
+++ b/plugins/modules/lambda_event.py
@@ -138,6 +138,7 @@
 import re
 
 try:
+    from botocore.exceptions import BotoCoreError
     from botocore.exceptions import ClientError
     from botocore.exceptions import MissingParametersError
     from botocore.exceptions import ParamValidationError
@@ -146,9 +147,9 @@
 
 from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
 
-from ansible_collections.amazon.aws.plugins.module_utils.botocore import boto3_conn
-from ansible_collections.amazon.aws.plugins.module_utils.botocore import get_aws_connection_info
+from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
 from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
+from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
 
 # ---------------------------------------------------------------------------------------------------
 #
@@ -157,122 +158,47 @@
 # ---------------------------------------------------------------------------------------------------
 
 
-class AWSConnection:
-    """
-    Create the connection object and client objects as required.
-    """
-
-    def __init__(self, ansible_obj, resources, use_boto3=True):
-        try:
-            self.region, self.endpoint, aws_connect_kwargs = get_aws_connection_info(ansible_obj, boto3=use_boto3)
-
-            self.resource_client = dict()
-            if not resources:
-                resources = ["lambda"]
-
-            resources.append("iam")
-
-            for resource in resources:
-                aws_connect_kwargs.update(
-                    dict(region=self.region, endpoint=self.endpoint, conn_type="client", resource=resource)
-                )
-                self.resource_client[resource] = boto3_conn(ansible_obj, **aws_connect_kwargs)
-
-            # if region is not provided, then get default profile/session region
-            if not self.region:
-                self.region = self.resource_client["lambda"].meta.region_name
-
-        except (ClientError, ParamValidationError, MissingParametersError) as e:
-            ansible_obj.fail_json(msg=f"Unable to connect, authorize or access resource: {e}")
-
-        # set account ID
-        try:
-            self.account_id = self.resource_client["iam"].get_user()["User"]["Arn"].split(":")[4]
-        except (ClientError, ValueError, KeyError, IndexError):
-            self.account_id = ""
-
-    def client(self, resource="lambda"):
-        return self.resource_client[resource]
-
-
-def pc(key):
-    """
-    Changes python key into Pascale case equivalent. For example, 'this_function_name' becomes 'ThisFunctionName'.
-
-    :param key:
-    :return:
-    """
-
-    return "".join([token.capitalize() for token in key.split("_")])
-
-
-def ordered_obj(obj):
-    """
-    Order object for comparison purposes
-
-    :param obj:
-    :return:
-    """
-
-    if isinstance(obj, dict):
-        return sorted((k, ordered_obj(v)) for k, v in obj.items())
-    if isinstance(obj, list):
-        return sorted(ordered_obj(x) for x in obj)
-    else:
-        return obj
-
-
-def set_api_sub_params(params):
-    """
-    Sets module sub-parameters to those expected by the boto3 API.
-
-    :param params:
-    :return:
-    """
-
-    api_params = dict()
-
-    for param in params.keys():
-        param_value = params.get(param, None)
-        if param_value:
-            api_params[pc(param)] = param_value
-
-    return api_params
-
-
-def validate_params(module, aws):
+def validate_params(module, client):
     """
     Performs basic parameter validation.
 
-    :param module:
-    :param aws:
+    :param module: The AnsibleAWSModule object
+    :param client: The client used to perform requests to AWS
     :return:
     """
 
     function_name = module.params["lambda_function_arn"]
+    qualifier = get_qualifier(module)
 
     # validate function name
     if not re.search(r"^[\w\-:]+$", function_name):
         module.fail_json(
             msg=f"Function name {function_name} is invalid. Names must contain only alphanumeric characters and hyphens.",
         )
-    if len(function_name) > 64 and not function_name.startswith("arn:aws:lambda:"):
-        module.fail_json(msg=f'Function name "{function_name}" exceeds 64 character limit')
-
-    elif len(function_name) > 140 and function_name.startswith("arn:aws:lambda:"):
-        module.fail_json(msg=f'ARN "{function_name}" exceeds 140 character limit')
 
-    # check if 'function_name' needs to be expanded in full ARN format
-    if not module.params["lambda_function_arn"].startswith("arn:aws:lambda:"):
-        function_name = module.params["lambda_function_arn"]
-        module.params["lambda_function_arn"] = f"arn:aws:lambda:{aws.region}:{aws.account_id}:function:{function_name}"
-
-    qualifier = get_qualifier(module)
-    if qualifier:
-        function_arn = module.params["lambda_function_arn"]
-        module.params["lambda_function_arn"] = f"{function_arn}:{qualifier}"
+    # lamba_fuction_arn contains only the function name (not the arn)
+    if not function_name.startswith("arn:aws:lambda:"):
+        if len(function_name) > 64:
+            module.fail_json(msg=f'Function name "{function_name}" exceeds 64 character limit')
+        try:
+            params = {"FunctionName": function_name}
+            if qualifier:
+                params["Qualifier"] = qualifier
+            response = client.get_function(**params)
+            module.params["lambda_function_arn"] = response["Configuration"]["FunctionArn"]
+        except is_boto3_error_code("ResourceNotFoundException"):
+            msg = f"An error occurred: The function '{function_name}' does not exist."
+            if qualifier:
+                msg = f"An error occurred: The function '{function_name}' (qualifier={qualifier}) does not exist."
+            module.fail_json(msg=msg)
+        except ClientError as e:  # pylint: disable=duplicate-except
+            module.fail_json(msg=f"An error occurred while trying to describe function '{function_name}': {e}")
+    else:
+        if len(function_name) > 140:
+            module.fail_json(msg='ARN "{0}" exceeds 140 character limit'.format(function_name))
 
-    return
+        if qualifier:
+            module.params["lambda_function_arn"] = f"{function_name}:{qualifier}"
 
 
 def get_qualifier(module):
@@ -302,7 +228,7 @@ def get_qualifier(module):
 # ---------------------------------------------------------------------------------------------------
 
 
-def lambda_event_stream(module, aws):
+def lambda_event_stream(module, client):
     """
     Adds, updates or deletes lambda stream (DynamoDb, Kinesis) event notifications.
     :param module:
@@ -310,7 +236,6 @@ def lambda_event_stream(module, aws):
     :return:
     """
 
-    client = aws.client("lambda")
     facts = dict()
     changed = False
     current_state = "absent"
@@ -333,9 +258,7 @@ def lambda_event_stream(module, aws):
         try:
             source_params["batch_size"] = int(batch_size)
         except ValueError:
-            module.fail_json(
-                msg=f"Source parameter 'batch_size' must be an integer, found: {source_params['batch_size']}"
-            )
+            module.fail_json(msg=f"Source parameter 'batch_size' must be an integer, found: {batch_size}")
 
     # optional boolean value needs special treatment as not present does not imply False
     source_param_enabled = module.boolean(source_params.get("enabled", "True"))
@@ -351,16 +274,15 @@ def lambda_event_stream(module, aws):
     if state == "present":
         if current_state == "absent":
             starting_position = source_params.get("starting_position")
-            if starting_position:
+            event_source = module.params.get("event_source")
+            if event_source == "stream":
+                if not starting_position:
+                    module.fail_json(
+                        msg="Source parameter 'starting_position' is required for stream event notification."
+                    )
                 api_params.update(StartingPosition=starting_position)
-            elif module.params.get("event_source") == "sqs":
-                # starting position is not required for SQS
-                pass
-            else:
-                module.fail_json(msg="Source parameter 'starting_position' is required for stream event notification.")
-
-            if source_arn:
-                api_params.update(Enabled=source_param_enabled)
+
+            api_params.update(Enabled=source_param_enabled)
             if source_params.get("batch_size"):
                 api_params.update(BatchSize=source_params.get("batch_size"))
             if source_params.get("function_response_types"):
@@ -375,9 +297,8 @@ def lambda_event_stream(module, aws):
 
         else:
             # current_state is 'present'
-            api_params = dict(FunctionName=module.params["lambda_function_arn"])
             current_mapping = facts[0]
-            api_params.update(UUID=current_mapping["UUID"])
+            api_params = dict(FunctionName=module.params["lambda_function_arn"], UUID=current_mapping["UUID"])
             mapping_changed = False
 
             # check if anything changed
@@ -438,12 +359,15 @@ def main():
         required_together=[],
     )
 
-    aws = AWSConnection(module, ["lambda"])
+    try:
+        client = module.client("lambda", retry_decorator=AWSRetry.jittered_backoff())
+    except (ClientError, BotoCoreError) as e:
+        module.fail_json_aws(e, msg="Trying to connect to AWS")
 
-    validate_params(module, aws)
+    validate_params(module, client)
 
     if module.params["event_source"].lower() in ("stream", "sqs"):
-        results = lambda_event_stream(module, aws)
+        results = lambda_event_stream(module, client)
     else:
         module.fail_json(msg="Please select `stream` or `sqs` as the event type")
 


Test patch:
diff --git a/tests/unit/plugins/modules/test_lambda_event.py b/tests/unit/plugins/modules/test_lambda_event.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/plugins/modules/test_lambda_event.py
@@ -0,0 +1,377 @@
+#
+# (c) 2024 Red Hat Inc.
+#
+# This file is part of Ansible
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from copy import deepcopy
+from unittest.mock import MagicMock
+from unittest.mock import patch
+
+import pytest
+
+from ansible_collections.amazon.aws.plugins.modules.lambda_event import get_qualifier
+from ansible_collections.amazon.aws.plugins.modules.lambda_event import lambda_event_stream
+from ansible_collections.amazon.aws.plugins.modules.lambda_event import validate_params
+
+mock_get_qualifier = "ansible_collections.amazon.aws.plugins.modules.lambda_event.get_qualifier"
+mock_camel_dict_to_snake_dict = "ansible_collections.amazon.aws.plugins.modules.lambda_event.camel_dict_to_snake_dict"
+
+
+@pytest.fixture
+def ansible_aws_module():
+    module = MagicMock()
+    module.check_mode = False
+    module.params = {
+        "state": "present",
+        "lambda_function_arn": None,
+        "event_source": "sqs",
+        "source_params": {
+            "source_arn": "arn:aws:sqs:us-east-2:123456789012:ansible-test-sqs",
+            "batch_size": 200,
+            "starting_position": "LATEST",
+        },
+        "alias": None,
+        "version": 0,
+    }
+    module.exit_json = MagicMock()
+    module.exit_json.side_effect = SystemExit(1)
+    module.fail_json_aws = MagicMock()
+    module.fail_json_aws.side_effect = SystemExit(2)
+    module.fail_json = MagicMock()
+    module.fail_json.side_effect = SystemExit(2)
+    module.client = MagicMock()
+    module.client.return_value = MagicMock()
+    module.boolean = MagicMock()
+    module.boolean.side_effect = lambda x: x.lower() in ["true", "1", "t", "y", "yes"]
+    return module
+
+
+@pytest.mark.parametrize(
+    "module_params,expected",
+    [
+        ({"version": 1}, "1"),
+        ({"alias": "ansible-test"}, "ansible-test"),
+        ({"version": 1, "alias": "ansible-test"}, "1"),
+        ({}, None),
+    ],
+)
+def test_get_qualifier(ansible_aws_module, module_params, expected):
+    ansible_aws_module.params.update(module_params)
+    assert get_qualifier(ansible_aws_module) == expected
+
+
+@pytest.mark.parametrize(
+    "function_name,error_msg",
+    [
+        (
+            "invalid+function+name",
+            "Function name invalid+function+name is invalid. Names must contain only alphanumeric characters and hyphens.",
+        ),
+        (
+            "this_invalid_function_name_has_more_than_64_character_limit_this_is_not_allowed_by_the_module",
+            'Function name "this_invalid_function_name_has_more_than_64_character_limit_this_is_not_allowed_by_the_module" exceeds 64 character limit',
+        ),
+        (
+            "arn:aws:lambda:us-east-2:123456789012:function:ansible-test-ansible-test-ansible-test-sqs-lambda-function:"
+            "ansible-test-ansible-test-ansible-test-sqs-lambda-function-alias",
+            'ARN "arn:aws:lambda:us-east-2:123456789012:function:ansible-test-ansible-test-ansible-test-sqs-lambda-function:'
+            'ansible-test-ansible-test-ansible-test-sqs-lambda-function-alias" exceeds 140 character limit',
+        ),
+    ],
+)
+def test_validate_params_function_name_errors(ansible_aws_module, function_name, error_msg):
+    ansible_aws_module.params.update({"lambda_function_arn": function_name})
+    client = MagicMock()
+    client.get_function = MagicMock()
+    client.get_function.return_value = {}
+    with pytest.raises(SystemExit):
+        validate_params(ansible_aws_module, client)
+
+    ansible_aws_module.fail_json.assert_called_once_with(msg=error_msg)
+
+
+@pytest.mark.parametrize(
+    "qualifier",
+    [None, "ansible-test"],
+)
+@patch(mock_get_qualifier)
+def test_validate_params_with_function_arn(m_get_qualifier, ansible_aws_module, qualifier):
+    function_name = "arn:aws:lambda:us-east-2:123456789012:function:sqs_consumer"
+    ansible_aws_module.params.update({"lambda_function_arn": function_name})
+    m_get_qualifier.return_value = qualifier
+
+    client = MagicMock()
+    client.get_function = MagicMock()
+    client.get_function.return_value = {}
+
+    params = deepcopy(ansible_aws_module.params)
+    params["lambda_function_arn"] = f"{function_name}:{qualifier}" if qualifier else function_name
+
+    validate_params(ansible_aws_module, client)
+    assert params == ansible_aws_module.params
+    m_get_qualifier.assert_called_once()
+
+
+@pytest.mark.parametrize(
+    "qualifier",
+    [None, "ansible-test"],
+)
+@patch(mock_get_qualifier)
+def test_validate_params_with_function_name(m_get_qualifier, ansible_aws_module, qualifier):
+    function_arn = "arn:aws:lambda:us-east-2:123456789012:function:sqs_consumer"
+    function_name = "sqs_consumer"
+    ansible_aws_module.params.update({"lambda_function_arn": function_name})
+    m_get_qualifier.return_value = qualifier
+
+    client = MagicMock()
+    client.get_function = MagicMock()
+    client.get_function.return_value = {
+        "Configuration": {"FunctionArn": function_arn},
+    }
+
+    params = deepcopy(ansible_aws_module.params)
+    params["lambda_function_arn"] = function_arn
+
+    validate_params(ansible_aws_module, client)
+
+    assert params == ansible_aws_module.params
+    m_get_qualifier.assert_called_once()
+    api_params = {"FunctionName": function_name}
+    if qualifier:
+        api_params.update({"Qualifier": qualifier})
+    client.get_function.assert_called_once_with(**api_params)
+
+
+EventSourceMappings = [
+    {
+        "BatchSize": 10,
+        "EventSourceArn": "arn:aws:sqs:us-east-2:123456789012:sqs_consumer",
+        "FunctionArn": "arn:aws:lambda:us-east-2:123456789012:function:sqs_consumer",
+        "LastModified": "2024-02-08T15:24:57.014000+01:00",
+        "MaximumBatchingWindowInSeconds": 0,
+        "State": "Enabled",
+        "StateTransitionReason": "USER_INITIATED",
+        "UUID": "3ab96d4c-b0c4-4885-87d0-f58cb9c0a4cc",
+    }
+]
+
+
+@pytest.mark.parametrize(
+    "check_mode",
+    [True, False],
+)
+@pytest.mark.parametrize(
+    "existing_event_source",
+    [True, False],
+)
+@patch(mock_camel_dict_to_snake_dict)
+def test_lambda_event_stream_with_state_absent(
+    mock_camel_dict_to_snake_dict, ansible_aws_module, check_mode, existing_event_source
+):
+    function_name = "sqs_consumer"
+    ansible_aws_module.params.update({"lambda_function_arn": function_name, "state": "absent"})
+    ansible_aws_module.check_mode = check_mode
+
+    client = MagicMock()
+    client.list_event_source_mappings = MagicMock()
+
+    client.list_event_source_mappings.return_value = {
+        "EventSourceMappings": EventSourceMappings if existing_event_source else []
+    }
+    client.delete_event_source_mapping = MagicMock()
+    event_source_deleted = {"msg": "event source successfully deleted."}
+    client.delete_event_source_mapping.return_value = event_source_deleted
+    mock_camel_dict_to_snake_dict.side_effect = lambda x: x
+
+    events = []
+    changed = False
+    result = lambda_event_stream(ansible_aws_module, client)
+    changed = existing_event_source
+    if existing_event_source:
+        events = EventSourceMappings
+        if not check_mode:
+            events = event_source_deleted
+            client.delete_event_source_mapping.assert_called_once_with(UUID=EventSourceMappings[0]["UUID"])
+    else:
+        client.delete_event_source_mapping.assert_not_called()
+    assert dict(changed=changed, events=events) == result
+
+
+def test_lambda_event_stream_create_event_missing_starting_position(ansible_aws_module):
+    ansible_aws_module.params = {
+        "state": "present",
+        "lambda_function_arn": "sqs_consumer",
+        "event_source": "stream",
+        "source_params": {
+            "source_arn": "arn:aws:sqs:us-east-2:123456789012:ansible-test-sqs",
+            "batch_size": 200,
+        },
+        "alias": None,
+        "version": 0,
+    }
+
+    client = MagicMock()
+    client.list_event_source_mappings = MagicMock()
+    client.list_event_source_mappings.return_value = {"EventSourceMappings": []}
+
+    error_message = "Source parameter 'starting_position' is required for stream event notification."
+    with pytest.raises(SystemExit):
+        lambda_event_stream(ansible_aws_module, client)
+    ansible_aws_module.fail_json.assert_called_once_with(msg=error_message)
+
+
+@pytest.mark.parametrize(
+    "check_mode",
+    [True, False],
+)
+@pytest.mark.parametrize(
+    "module_params,api_params",
+    [
+        (
+            {
+                "state": "present",
+                "lambda_function_arn": "sqs_consumer",
+                "event_source": "stream",
+                "source_params": {
+                    "source_arn": "arn:aws:sqs:us-east-2:123456789012:ansible-test-sqs",
+                    "batch_size": 250,
+                    "starting_position": "END",
+                    "function_response_types": ["ReportBatchItemFailures"],
+                },
+                "alias": None,
+                "version": 0,
+            },
+            {
+                "FunctionName": "sqs_consumer",
+                "EventSourceArn": "arn:aws:sqs:us-east-2:123456789012:ansible-test-sqs",
+                "StartingPosition": "END",
+                "Enabled": True,
+                "BatchSize": 250,
+                "FunctionResponseTypes": ["ReportBatchItemFailures"],
+            },
+        ),
+        (
+            {
+                "state": "present",
+                "lambda_function_arn": "sqs_consumer",
+                "event_source": "stream",
+                "source_params": {
+                    "source_arn": "arn:aws:sqs:us-east-2:123456789012:ansible-test-sqs",
+                    "batch_size": 250,
+                    "starting_position": "END",
+                    "function_response_types": ["ReportBatchItemFailures"],
+                    "enabled": "no",
+                },
+                "alias": None,
+                "version": 0,
+            },
+            {
+                "FunctionName": "sqs_consumer",
+                "EventSourceArn": "arn:aws:sqs:us-east-2:123456789012:ansible-test-sqs",
+                "StartingPosition": "END",
+                "Enabled": False,
+                "BatchSize": 250,
+                "FunctionResponseTypes": ["ReportBatchItemFailures"],
+            },
+        ),
+        (
+            {
+                "state": "present",
+                "lambda_function_arn": "sqs_consumer",
+                "event_source": "sqs",
+                "source_params": {
+                    "source_arn": "arn:aws:sqs:us-east-2:123456789012:ansible-test-sqs",
+                    "batch_size": 101,
+                },
+                "alias": None,
+                "version": 0,
+            },
+            {
+                "FunctionName": "sqs_consumer",
+                "EventSourceArn": "arn:aws:sqs:us-east-2:123456789012:ansible-test-sqs",
+                "Enabled": True,
+                "BatchSize": 101,
+            },
+        ),
+    ],
+)
+@patch(mock_camel_dict_to_snake_dict)
+def test_lambda_event_stream_create_event(
+    mock_camel_dict_to_snake_dict, ansible_aws_module, check_mode, module_params, api_params
+):
+    ansible_aws_module.params = module_params
+    ansible_aws_module.check_mode = check_mode
+
+    client = MagicMock()
+    client.list_event_source_mappings = MagicMock()
+    client.list_event_source_mappings.return_value = {"EventSourceMappings": []}
+
+    client.create_event_source_mapping = MagicMock()
+    event_source_created = {"msg": "event source successfully created."}
+    client.create_event_source_mapping.return_value = event_source_created
+    mock_camel_dict_to_snake_dict.side_effect = lambda x: x
+
+    result = lambda_event_stream(ansible_aws_module, client)
+
+    events = []
+
+    if not check_mode:
+        events = event_source_created
+        client.create_event_source_mapping.assert_called_once_with(**api_params)
+    else:
+        client.create_event_source_mapping.assert_not_called()
+
+    assert dict(changed=True, events=events) == result
+
+
+@pytest.mark.parametrize(
+    "check_mode",
+    [True, False],
+)
+@pytest.mark.parametrize(
+    "module_source_params,current_mapping,api_params",
+    [
+        (
+            {"batch_size": 100, "enabled": "false"},
+            {"BatchSize": 120, "State": "Enabled"},
+            {"BatchSize": 100, "Enabled": False},
+        ),
+        (
+            {"batch_size": 100, "enabled": "true"},
+            {"BatchSize": 100, "State": "Enabled"},
+            {},
+        ),
+    ],
+)
+@patch(mock_camel_dict_to_snake_dict)
+def test_lambda_event_stream_update_event(
+    mock_camel_dict_to_snake_dict, ansible_aws_module, check_mode, module_source_params, current_mapping, api_params
+):
+    function_name = "ansible-test-update-event-function"
+    ansible_aws_module.params.update({"lambda_function_arn": function_name})
+    ansible_aws_module.params["source_params"].update(module_source_params)
+    ansible_aws_module.check_mode = check_mode
+
+    client = MagicMock()
+    client.list_event_source_mappings = MagicMock()
+    existing_event_source = deepcopy(EventSourceMappings)
+    existing_event_source[0].update(current_mapping)
+    client.list_event_source_mappings.return_value = {"EventSourceMappings": existing_event_source}
+
+    client.update_event_source_mapping = MagicMock()
+    event_source_updated = {"msg": "event source successfully updated."}
+    client.update_event_source_mapping.return_value = event_source_updated
+    mock_camel_dict_to_snake_dict.side_effect = lambda x: x
+
+    result = lambda_event_stream(ansible_aws_module, client)
+    if not api_params:
+        assert dict(changed=False, events=existing_event_source) == result
+        client.update_event_source_mapping.assert_not_called()
+    elif check_mode:
+        assert dict(changed=True, events=existing_event_source) == result
+        client.update_event_source_mapping.assert_not_called()
+    else:
+        api_params.update({"FunctionName": function_name, "UUID": existing_event_source[0]["UUID"]})
+        assert dict(changed=True, events=event_source_updated) == result
+        client.update_event_source_mapping.assert_called_once_with(**api_params)