Repository: getmoto/moto

Issue:
Can't create an elbv2 listener rule with ForwardConfig if forward is not the first action 
I've got tests which test this bit of code:

```
    elb_client.create_rule(
        ListenerArn=os.environ["HTTPS_LISTENER_ARN"],
        Conditions=[
            {
                "Field": "path-pattern",
                "PathPatternConfig": {
                    "Values": [
                        f"/{instance_name}*",
                    ]
                },
            },
        ],
        Priority=priority,
        Actions=[
            {
                "Type": "authenticate-cognito",
                "Order": 1,
                "AuthenticateCognitoConfig": {
                    "UserPoolArn": os.environ["POOL_ARN"],
                    "UserPoolClientId": os.environ["CLIENT_ID"],
                    "UserPoolDomain": os.environ["POOL_DOMAIN"],
                    "SessionCookieName": "AWSELBAuthSessionCookie",
                    "Scope": "openid",
                    "SessionTimeout": 604800,
                    "OnUnauthenticatedRequest": "authenticate",
                },
            },
            {
                "Type": action_type,
                "Order": 2,
                "ForwardConfig": {
                    "TargetGroups": [
                        {"TargetGroupArn": target_group, "Weight": 1},
                    ],
                    "TargetGroupStickinessConfig": {
                        "Enabled": False,
                    },
                },
            },
        ],
    )
    return rule
```

and they fail with:

```
======================================================================
ERROR: test_handler_running (test_handler.TestHandler)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/sebastian/Clones/jupyterhub/.venv/lib/python3.9/site-packages/moto/core/models.py", line 111, in wrapper
    result = func(*args, **kwargs)
  File "/Users/sebastian/Clones/jupyterhub/.venv/lib/python3.9/site-packages/moto/core/models.py", line 111, in wrapper
    result = func(*args, **kwargs)
  File "/Users/sebastian/Clones/jupyterhub/.venv/lib/python3.9/site-packages/mock/mock.py", line 1346, in patched
    return func(*newargs, **newkeywargs)
  File "/Users/sebastian/Clones/jupyterhub/test_handler.py", line 232, in test_handler_running
    handler.handler(RUNNING_EVENT, {"test": True})
  File "/Users/sebastian/Clones/jupyterhub/handler.py", line 214, in handler
    create_listener_rule(elb_client, target_group, instance_name)
  File "/Users/sebastian/Clones/jupyterhub/handler.py", line 80, in create_listener_rule
    rule = elb_client.create_rule(
  File "/Users/sebastian/Clones/jupyterhub/.venv/lib/python3.9/site-packages/botocore/client.py", line 386, in _api_call
    return self._make_api_call(operation_name, kwargs)
  File "/Users/sebastian/Clones/jupyterhub/.venv/lib/python3.9/site-packages/botocore/client.py", line 705, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.exceptions.ClientError: An error occurred (ValidationError) when calling the CreateRule operation: 1 validation error detected: Value 'forward' at 'actions.2.member.type' failed to satisfy constraint: Member must satisfy enum value set: [forward, redirect, fixed-response]
```

I have traced the origin to `_validate_actions` in `moto/elbv2/models.py`, the code I have should be covered by:

```
                action_type == "forward"
                and "forward_config._target_groups.member.{}._target_group_arn".format(
                    index
                )
                in action.data.keys()
                or "forward_config" in action.data.keys()
```

But because index is 2 and not 1 (data looks like this: `{'type': 'forward', 'order': '2', 'forward_config._target_groups.member.1._target_group_arn': 'arn:aws:elasticloadbalancing:ap-southeast-2:1:targetgroup/test-instance/50dc6c495c0c9188', 'forward_config._target_groups.member.1._weight': '1', 'forward_config._target_group_stickiness_config._enabled': 'false'}`) the lookup fails giving the above validation error.


Discussion:
Thanks for raising this @sblask!

Resolving patch:
diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py
--- a/moto/elbv2/models.py
+++ b/moto/elbv2/models.py
@@ -8,8 +8,6 @@
 from moto.core.exceptions import RESTError
 from moto.core import BaseBackend, BaseModel, CloudFormationModel
 from moto.core.utils import (
-    camelcase_to_underscores,
-    underscores_to_camelcase,
     iso_8601_datetime_with_milliseconds,
     get_random_hex,
 )
@@ -367,42 +365,42 @@ def __init__(self, listener_arn, conditions, priority, actions, is_default):
 class FakeAction(BaseModel):
     def __init__(self, data):
         self.data = data
-        self.type = data.get("type")
+        self.type = data.get("Type")
 
     def to_xml(self):
         template = Template(
             """<Type>{{ action.type }}</Type>
-            {% if action.type == "forward" and "forward_config" in action.data %}
+            {% if action.type == "forward" and "ForwardConfig" in action.data %}
             <ForwardConfig>
               <TargetGroups>
-                {% for target_group in action.data["forward_config"]["target_groups"] %}
+                {% for target_group in action.data["ForwardConfig"]["TargetGroups"] %}
                 <member>
-                  <TargetGroupArn>{{ target_group["target_group_arn"] }}</TargetGroupArn>
-                  <Weight>{{ target_group["weight"] }}</Weight>
+                  <TargetGroupArn>{{ target_group["TargetGroupArn"] }}</TargetGroupArn>
+                  <Weight>{{ target_group["Weight"] }}</Weight>
                 </member>
                 {% endfor %}
               </TargetGroups>
             </ForwardConfig>
             {% endif %}
-            {% if action.type == "forward" and "forward_config" not in action.data %}
-            <TargetGroupArn>{{ action.data["target_group_arn"] }}</TargetGroupArn>
+            {% if action.type == "forward" and "ForwardConfig" not in action.data %}
+            <TargetGroupArn>{{ action.data["TargetGroupArn"] }}</TargetGroupArn>
             {% elif action.type == "redirect" %}
             <RedirectConfig>
-                <Protocol>{{ action.data["redirect_config._protocol"] }}</Protocol>
-                <Port>{{ action.data["redirect_config._port"] }}</Port>
-                <StatusCode>{{ action.data["redirect_config._status_code"] }}</StatusCode>
+                <Protocol>{{ action.data["RedirectConfig"]["Protocol"] }}</Protocol>
+                <Port>{{ action.data["RedirectConfig"]["Port"] }}</Port>
+                <StatusCode>{{ action.data["RedirectConfig"]["StatusCode"] }}</StatusCode>
             </RedirectConfig>
             {% elif action.type == "authenticate-cognito" %}
             <AuthenticateCognitoConfig>
-                <UserPoolArn>{{ action.data["authenticate_cognito_config._user_pool_arn"] }}</UserPoolArn>
-                <UserPoolClientId>{{ action.data["authenticate_cognito_config._user_pool_client_id"] }}</UserPoolClientId>
-                <UserPoolDomain>{{ action.data["authenticate_cognito_config._user_pool_domain"] }}</UserPoolDomain>
+                <UserPoolArn>{{ action.data["AuthenticateCognitoConfig"]["UserPoolArn"] }}</UserPoolArn>
+                <UserPoolClientId>{{ action.data["AuthenticateCognitoConfig"]["UserPoolClientId"] }}</UserPoolClientId>
+                <UserPoolDomain>{{ action.data["AuthenticateCognitoConfig"]["UserPoolDomain"] }}</UserPoolDomain>
             </AuthenticateCognitoConfig>
             {% elif action.type == "fixed-response" %}
              <FixedResponseConfig>
-                <ContentType>{{ action.data["fixed_response_config._content_type"] }}</ContentType>
-                <MessageBody>{{ action.data["fixed_response_config._message_body"] }}</MessageBody>
-                <StatusCode>{{ action.data["fixed_response_config._status_code"] }}</StatusCode>
+                <ContentType>{{ action.data["FixedResponseConfig"]["ContentType"] }}</ContentType>
+                <MessageBody>{{ action.data["FixedResponseConfig"]["MessageBody"] }}</MessageBody>
+                <StatusCode>{{ action.data["FixedResponseConfig"]["StatusCode"] }}</StatusCode>
             </FixedResponseConfig>
             {% endif %}
             """
@@ -634,45 +632,13 @@ def convert_and_validate_action_properties(self, properties):
         default_actions = []
         for i, action in enumerate(properties["Actions"]):
             action_type = action["Type"]
-            if action_type == "forward" and "ForwardConfig" in action:
-                action_forward_config = action["ForwardConfig"]
-                action_target_groups = action_forward_config["TargetGroups"]
-                target_group_action = []
-                for action_target_group in action_target_groups:
-                    target_group_action.append(
-                        {
-                            "target_group_arn": action_target_group["TargetGroupArn"],
-                            "weight": action_target_group["Weight"],
-                        }
-                    )
-                default_actions.append(
-                    {
-                        "type": action_type,
-                        "forward_config": {"target_groups": target_group_action},
-                    }
-                )
-            elif action_type == "forward" and "ForwardConfig" not in action:
-                default_actions.append(
-                    {"type": action_type, "target_group_arn": action["TargetGroupArn"]}
-                )
-            elif action_type in [
+            if action_type in [
                 "redirect",
                 "authenticate-cognito",
                 "fixed-response",
+                "forward",
             ]:
-                redirect_action = {"type": action_type}
-                key = (
-                    underscores_to_camelcase(action_type.capitalize().replace("-", "_"))
-                    + "Config"
-                )
-                for redirect_config_key, redirect_config_value in action[key].items():
-                    # need to match the output of _get_list_prefix
-                    redirect_action[
-                        camelcase_to_underscores(key)
-                        + "._"
-                        + camelcase_to_underscores(redirect_config_key)
-                    ] = redirect_config_value
-                default_actions.append(redirect_action)
+                default_actions.append(action)
             else:
                 raise InvalidActionTypeError(action_type, i + 1)
         return default_actions
@@ -853,8 +819,8 @@ def _validate_actions(self, actions):
         for i, action in enumerate(actions):
             index = i + 1
             action_type = action.type
-            if action_type == "forward" and "target_group_arn" in action.data:
-                action_target_group_arn = action.data["target_group_arn"]
+            if action_type == "forward" and "TargetGroupArn" in action.data:
+                action_target_group_arn = action.data["TargetGroupArn"]
                 if action_target_group_arn not in target_group_arns:
                     raise ActionTargetGroupNotFoundError(action_target_group_arn)
             elif action_type == "fixed-response":
@@ -862,20 +828,13 @@ def _validate_actions(self, actions):
             elif action_type in ["redirect", "authenticate-cognito"]:
                 pass
             # pass if listener rule has forward_config as an Action property
-            elif (
-                action_type == "forward"
-                and "forward_config._target_groups.member.{}._target_group_arn".format(
-                    index
-                )
-                in action.data.keys()
-                or "forward_config" in action.data.keys()
-            ):
+            elif action_type == "forward" and "ForwardConfig" in action.data.keys():
                 pass
             else:
                 raise InvalidActionTypeError(action_type, index)
 
     def _validate_fixed_response_action(self, action, i, index):
-        status_code = action.data.get("fixed_response_config._status_code")
+        status_code = action.data.get("FixedResponseConfig", {}).get("StatusCode")
         if status_code is None:
             raise ParamValidationError(
                 report='Missing required parameter in Actions[%s].FixedResponseConfig: "StatusCode"'
@@ -889,7 +848,7 @@ def _validate_fixed_response_action(self, action, i, index):
                     status_code, index, expression
                 )
             )
-        content_type = action.data["fixed_response_config._content_type"]
+        content_type = action.data["FixedResponseConfig"].get("ContentType")
         if content_type and content_type not in [
             "text/plain",
             "text/css",
@@ -977,31 +936,20 @@ def convert_and_validate_certificates(self, certificates):
     def convert_and_validate_properties(self, properties):
 
         # transform default actions to confirm with the rest of the code and XML templates
+        # Caller: CF create/update for type "AWS::ElasticLoadBalancingV2::Listener"
         default_actions = []
         for i, action in enumerate(properties["DefaultActions"]):
             action_type = action["Type"]
             if action_type == "forward":
                 default_actions.append(
-                    {"type": action_type, "target_group_arn": action["TargetGroupArn"]}
+                    {"Type": action_type, "TargetGroupArn": action["TargetGroupArn"]}
                 )
             elif action_type in [
                 "redirect",
                 "authenticate-cognito",
                 "fixed-response",
             ]:
-                redirect_action = {"type": action_type}
-                key = (
-                    underscores_to_camelcase(action_type.capitalize().replace("-", "_"))
-                    + "Config"
-                )
-                for redirect_config_key, redirect_config_value in action[key].items():
-                    # need to match the output of _get_list_prefix
-                    redirect_action[
-                        camelcase_to_underscores(key)
-                        + "._"
-                        + camelcase_to_underscores(redirect_config_key)
-                    ] = redirect_config_value
-                default_actions.append(redirect_action)
+                default_actions.append(action)
             else:
                 raise InvalidActionTypeError(action_type, i + 1)
         return default_actions
@@ -1040,7 +988,7 @@ def create_listener(
         balancer.listeners[listener.arn] = listener
         for action in default_actions:
             if action.type == "forward":
-                target_group = self.target_groups[action.data["target_group_arn"]]
+                target_group = self.target_groups[action.data["TargetGroupArn"]]
                 target_group.load_balancer_arns.append(load_balancer_arn)
 
         return listener
@@ -1473,7 +1421,7 @@ def _any_listener_using(self, target_group_arn):
             for listener in load_balancer.listeners.values():
                 for rule in listener.rules.values():
                     for action in rule.actions:
-                        if action.data.get("target_group_arn") == target_group_arn:
+                        if action.data.get("TargetGroupArn") == target_group_arn:
                             return True
         return False
 
diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py
--- a/moto/elbv2/responses.py
+++ b/moto/elbv2/responses.py
@@ -165,12 +165,11 @@ def create_load_balancer(self):
     @amzn_request_id
     def create_rule(self):
         params = self._get_params()
-        actions = self._get_list_prefix("Actions.member")
         rules = self.elbv2_backend.create_rule(
             listener_arn=params["ListenerArn"],
             conditions=params["Conditions"],
             priority=params["Priority"],
-            actions=actions,
+            actions=params["Actions"],
         )
         template = self.response_template(CREATE_RULE_TEMPLATE)
         return template.render(rules=rules)
@@ -214,6 +213,7 @@ def create_target_group(self):
 
     @amzn_request_id
     def create_listener(self):
+        params = self._get_params()
         load_balancer_arn = self._get_param("LoadBalancerArn")
         protocol = self._get_param("Protocol")
         port = self._get_param("Port")
@@ -223,7 +223,7 @@ def create_listener(self):
             certificate = certificates[0].get("certificate_arn")
         else:
             certificate = None
-        default_actions = self._get_list_prefix("DefaultActions.member")
+        default_actions = params.get("DefaultActions", [])
 
         listener = self.elbv2_backend.create_listener(
             load_balancer_arn=load_balancer_arn,
@@ -357,7 +357,7 @@ def modify_rule(self):
         rule_arn = self._get_param("RuleArn")
         params = self._get_params()
         conditions = params["Conditions"]
-        actions = self._get_list_prefix("Actions.member")
+        actions = params.get("Actions", [])
         rules = self.elbv2_backend.modify_rule(
             rule_arn=rule_arn, conditions=conditions, actions=actions
         )


Test patch:
diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py
--- a/tests/test_elbv2/test_elbv2.py
+++ b/tests/test_elbv2/test_elbv2.py
@@ -434,6 +434,83 @@ def test_create_target_group_without_non_required_parameters():
     target_group.should_not.be.none
 
 
+@mock_ec2
+@mock_elbv2
+def test_create_rule_forward_config_as_second_arg():
+    # https://github.com/spulec/moto/issues/4123
+    # Necessary because there was some convoluted way of parsing arguments
+    # Actions with type=forward had to be the first action specified
+    response, vpc, security_group, subnet1, subnet2, elbv2 = create_load_balancer()
+
+    load_balancer_arn = response.get("LoadBalancers")[0].get("LoadBalancerArn")
+
+    response = elbv2.create_listener(
+        LoadBalancerArn=load_balancer_arn, Protocol="HTTP", Port=80, DefaultActions=[],
+    )
+    http_listener_arn = response.get("Listeners")[0]["ListenerArn"]
+
+    priority = 100
+
+    response = elbv2.create_target_group(
+        Name="a-target",
+        Protocol="HTTP",
+        Port=8080,
+        VpcId=vpc.id,
+        HealthCheckProtocol="HTTP",
+        HealthCheckPort="8080",
+        HealthCheckPath="/",
+        Matcher={"HttpCode": "200"},
+    )
+    target_group = response.get("TargetGroups")[0]
+
+    # No targets registered yet
+    target_group_arn = target_group.get("TargetGroupArn")
+    elbv2.create_rule(
+        ListenerArn=http_listener_arn,
+        Conditions=[
+            {"Field": "path-pattern", "PathPatternConfig": {"Values": [f"/sth*",]},},
+        ],
+        Priority=priority,
+        Actions=[
+            {
+                "Type": "authenticate-cognito",
+                "Order": 1,
+                "AuthenticateCognitoConfig": {
+                    "UserPoolArn": "?1",
+                    "UserPoolClientId": "?2",
+                    "UserPoolDomain": "?2",
+                    "SessionCookieName": "AWSELBAuthSessionCookie",
+                    "Scope": "openid",
+                    "SessionTimeout": 604800,
+                    "OnUnauthenticatedRequest": "authenticate",
+                },
+            },
+            {
+                "Type": "forward",
+                "Order": 2,
+                "ForwardConfig": {
+                    "TargetGroups": [
+                        {"TargetGroupArn": target_group_arn, "Weight": 1},
+                    ],
+                    "TargetGroupStickinessConfig": {"Enabled": False,},
+                },
+            },
+        ],
+    )
+    all_rules = elbv2.describe_rules(ListenerArn=http_listener_arn)["Rules"]
+    our_rule = all_rules[0]
+    actions = our_rule["Actions"]
+    forward_action = [a for a in actions if "ForwardConfig" in a.keys()][0]
+    forward_action.should.equal(
+        {
+            "ForwardConfig": {
+                "TargetGroups": [{"TargetGroupArn": target_group_arn, "Weight": 1}]
+            },
+            "Type": "forward",
+        }
+    )
+
+
 @mock_elbv2
 @mock_ec2
 def test_create_invalid_target_group():
diff --git a/tests/test_elbv2/test_elbv2_cloudformation.py b/tests/test_elbv2/test_elbv2_cloudformation.py
--- a/tests/test_elbv2/test_elbv2_cloudformation.py
+++ b/tests/test_elbv2/test_elbv2_cloudformation.py
@@ -1,5 +1,6 @@
 import boto3
 import json
+import sure  # noqa
 
 from moto import mock_elbv2, mock_ec2, mock_cloudformation
 from moto.core import ACCOUNT_ID