Repository: getmoto/moto
Pull request #8106 (2024-09-09 12:19:03)

ELBv2: Fix CF bug when passing certificates to ELB listener creation
Context:

Recently our tests started failing with a next error:

```
pattern = '^arn:(aws|aws-cn|aws-iso|aws-iso-b|aws-us-gov):iam:'
string = [{'CertificateArn': 'arn:aws:acm:us-east-1:123456789012:certificate/<cert_id>', 'certificate_arn': 'arn:aws:acm:us-east-1:123456789012:certificate/<cert_id>'}]
flags = 0

    def search(pattern, string, flags=0):
        """Scan through string looking for a match to the pattern, returning
        a Match object, or None if no match was found."""
>       return _compile(pattern, flags).search(string)
E       TypeError: expected string or bytes-like object, got 'list'
```

I tracked down an issue to this change - https://github.com/getmoto/moto/pull/8054

In my understanding in case of listener creation from cloudformation `certificate` variable is not a string, but an array and this newly added code is failing. 

This PR adds a change so certificate passed as a string to `create_listener` method in cloudformation case similar to what is in the [elb client code](https://github.com/getmoto/moto/blob/master/moto/elbv2/responses.py#L462-L465).

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/8106?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.46%. Comparing base [(`b574a9a`)](https://app.codecov.io/gh/getmoto/moto/commit/b574a9a0d43961c47f4da5ac944579f13a8fd006?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`c0d1837`)](https://app.codecov.io/gh/getmoto/moto/commit/c0d1837b722d6a480a6a79496eda9de82c764586?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).
> Report is 5 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #8106   +/-   ##
=======================================
  Coverage   94.45%   94.46%           
=======================================
  Files        1141     1141           
  Lines       98198    98201    +3     
=======================================
+ Hits        92757    92761    +4     
+ Misses       5441     5440    -1     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/8106/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/8106/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `28.95% <0.00%> (-0.01%)` | :arrow_down: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/8106/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.43% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/8106?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py
index ba4e2bef6..961be5ee1 100644
--- a/moto/elbv2/models.py
+++ b/moto/elbv2/models.py
@@ -361,8 +361,12 @@ class FakeListener(CloudFormationModel):
 
         default_actions = elbv2_backend.convert_and_validate_properties(properties)
         certificates = elbv2_backend.convert_and_validate_certificates(certificates)
+        if certificates:
+            certificate = certificates[0].get("certificate_arn")
+        else:
+            certificate = None
         listener = elbv2_backend.create_listener(
-            load_balancer_arn, protocol, port, ssl_policy, certificates, default_actions
+            load_balancer_arn, protocol, port, ssl_policy, certificate, default_actions
         )
         return listener
 


Test patch:
diff --git a/tests/test_elbv2/test_elbv2_cloudformation.py b/tests/test_elbv2/test_elbv2_cloudformation.py
index f4ae48248..c418a3465 100644
--- a/tests/test_elbv2/test_elbv2_cloudformation.py
+++ b/tests/test_elbv2/test_elbv2_cloudformation.py
@@ -326,3 +326,96 @@ def test_fixed_response_action_listener_rule_cloudformation():
             },
         }
     ]
+
+
+@mock_aws
+def test_https_listener_rule_with_certificate_cloudformation():
+    cnf_conn = boto3.client("cloudformation", region_name="us-east-1")
+    elbv2_client = boto3.client("elbv2", region_name="us-east-1")
+    acm_client = boto3.client("acm", region_name="us-east-1")
+
+    acm_request_response = acm_client.request_certificate(
+        DomainName="fake.domain.com",
+        DomainValidationOptions=[
+            {"DomainName": "fake.domain.com", "ValidationDomain": "domain.com"},
+        ],
+    )
+    certificate_arn = acm_request_response["CertificateArn"]
+
+    template = {
+        "AWSTemplateFormatVersion": "2010-09-09",
+        "Description": "ECS Cluster Test CloudFormation",
+        "Resources": {
+            "testVPC": {
+                "Type": "AWS::EC2::VPC",
+                "Properties": {"CidrBlock": "10.0.0.0/16"},
+            },
+            "subnet1": {
+                "Type": "AWS::EC2::Subnet",
+                "Properties": {
+                    "CidrBlock": "10.0.0.0/24",
+                    "VpcId": {"Ref": "testVPC"},
+                    "AvalabilityZone": "us-east-1b",
+                },
+            },
+            "subnet2": {
+                "Type": "AWS::EC2::Subnet",
+                "Properties": {
+                    "CidrBlock": "10.0.1.0/24",
+                    "VpcId": {"Ref": "testVPC"},
+                    "AvalabilityZone": "us-east-1b",
+                },
+            },
+            "testLb": {
+                "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer",
+                "Properties": {
+                    "Name": "my-lb",
+                    "Subnets": [{"Ref": "subnet1"}, {"Ref": "subnet2"}],
+                    "Type": "application",
+                    "SecurityGroups": [],
+                },
+            },
+            "testListener": {
+                "Type": "AWS::ElasticLoadBalancingV2::Listener",
+                "Properties": {
+                    "LoadBalancerArn": {"Ref": "testLb"},
+                    "Certificates": [{"CertificateArn": certificate_arn}],
+                    "Port": 443,
+                    "Protocol": "HTTPS",
+                    "DefaultActions": [
+                        {
+                            "Type": "fixed-response",
+                            "FixedResponseConfig": {
+                                "ContentType": "text/plain",
+                                "MessageBody": "This page does not exist",
+                                "StatusCode": "404",
+                            },
+                        }
+                    ],
+                },
+            },
+        },
+    }
+    template_json = json.dumps(template)
+    cnf_conn.create_stack(StackName="test-stack", TemplateBody=template_json)
+
+    resp = elbv2_client.describe_load_balancers(Names=["my-lb"])
+    assert len(resp["LoadBalancers"]) == 1
+    load_balancer_arn = resp["LoadBalancers"][0]["LoadBalancerArn"]
+
+    listeners = elbv2_client.describe_listeners(LoadBalancerArn=load_balancer_arn)
+
+    assert len(listeners["Listeners"]) == 1
+    assert listeners["Listeners"][0]["DefaultActions"] == [
+        {
+            "Type": "fixed-response",
+            "FixedResponseConfig": {
+                "ContentType": "text/plain",
+                "MessageBody": "This page does not exist",
+                "StatusCode": "404",
+            },
+        }
+    ]
+    assert listeners["Listeners"][0]["Certificates"] == [
+        {"CertificateArn": certificate_arn}
+    ]