diff --git a/moto/core/responses.py b/moto/core/responses.py index c8680aa1a..8f6a6c946 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -557,6 +557,81 @@ class BaseResponse(_TemplateEnvironmentMixin, ActionAuthenticatorMixin): ] return params + def _get_params(self): + """ + Given a querystring of + { + 'Action': ['CreatRule'], + 'Conditions.member.1.Field': ['http-header'], + 'Conditions.member.1.HttpHeaderConfig.HttpHeaderName': ['User-Agent'], + 'Conditions.member.1.HttpHeaderConfig.Values.member.1': ['curl'], + 'Actions.member.1.FixedResponseConfig.StatusCode': ['200'], + 'Actions.member.1.FixedResponseConfig.ContentType': ['text/plain'], + 'Actions.member.1.Type': ['fixed-response'] + } + + returns + { + 'Action': 'CreatRule', + 'Conditions': [ + { + 'Field': 'http-header', + 'HttpHeaderConfig': { + 'HttpHeaderName': 'User-Agent', + 'Values': ['curl'] + } + } + ], + 'Actions': [ + { + 'Type': 'fixed-response', + 'FixedResponseConfig': { + 'StatusCode': '200', + 'ContentType': 'text/plain' + } + } + ] + } + """ + params = {} + for k, v in sorted(self.querystring.items()): + self._parse_param(k, v[0], params) + return params + + def _parse_param(self, key, value, params): + keylist = key.split(".") + obj = params + for i, key in enumerate(keylist[:-1]): + if key in obj: + # step into + parent = obj + obj = obj[key] + else: + if key == "member": + if not isinstance(obj, list): + # initialize list + # reset parent + obj = [] + parent[keylist[i - 1]] = obj + elif key.isdigit(): + index = int(key) - 1 + if len(obj) <= index: + # initialize list element + obj.insert(index, {}) + # step into + parent = obj + obj = obj[index] + else: + # initialize dict + obj[key] = {} + # step into + parent = obj + obj = obj[key] + if isinstance(obj, list): + obj.append(value) + else: + obj[keylist[-1]] = value + def _get_list_prefix(self, param_prefix): """ Given a query dict like diff --git a/moto/elbv2/models.py b/moto/elbv2/models.py index 3033680ec..222344d40 100644 --- a/moto/elbv2/models.py +++ b/moto/elbv2/models.py @@ -326,7 +326,6 @@ class FakeListenerRule(CloudFormationModel): conditions = properties.get("Conditions") actions = elbv2_backend.convert_and_validate_action_properties(properties) - conditions = elbv2_backend.convert_and_validate_condition_properties(properties) listener_rule = elbv2_backend.create_rule( listener_arn, conditions, priority, actions ) @@ -343,7 +342,6 @@ class FakeListenerRule(CloudFormationModel): conditions = properties.get("Conditions") actions = elbv2_backend.convert_and_validate_action_properties(properties) - conditions = elbv2_backend.convert_and_validate_condition_properties(properties) listener_rule = elbv2_backend.modify_rule( original_resource.arn, conditions, actions ) @@ -660,15 +658,6 @@ class ELBv2Backend(BaseBackend): raise InvalidActionTypeError(action_type, i + 1) return default_actions - def convert_and_validate_condition_properties(self, properties): - - conditions = [] - for i, condition in enumerate(properties["Conditions"]): - conditions.append( - {"field": condition["Field"], "values": condition["Values"],} - ) - return conditions - def create_rule(self, listener_arn, conditions, priority, actions): actions = [FakeAction(action) for action in actions] listeners = self.describe_listeners(None, [listener_arn]) @@ -677,22 +666,7 @@ class ELBv2Backend(BaseBackend): listener = listeners[0] # validate conditions - for condition in conditions: - if "field" in condition: - field = condition["field"] - if field not in ["path-pattern", "host-header"]: - raise InvalidConditionFieldError(field) - - values = condition["values"] - if len(values) == 0: - raise InvalidConditionValueError( - "A condition value must be specified" - ) - if len(values) > 1: - raise InvalidConditionValueError( - "The '%s' field contains too many values; the limit is '1'" - % field - ) + self._validate_conditions(conditions) # TODO: check pattern of value for 'host-header' # TODO: check pattern of value for 'path-pattern' @@ -714,6 +688,136 @@ class ELBv2Backend(BaseBackend): listener.register(arn, rule) return rule + def _validate_conditions(self, conditions): + for condition in conditions: + if "Field" in condition: + field = condition["Field"] + if field not in [ + "host-header", + "http-header", + "http-request-method", + "path-pattern", + "query-string", + "source-ip", + ]: + raise InvalidConditionFieldError(field) + if "Values" in condition and field not in [ + "host-header", + "path-pattern", + ]: + raise InvalidConditionValueError( + "The 'Values' field is not compatible with '%s'" % field + ) + else: + method_name = "_validate_" + field.replace("-", "_") + "_condition" + func = getattr(self, method_name) + func(condition) + + def _validate_host_header_condition(self, condition): + values = None + if "HostHeaderConfig" in condition: + values = condition["HostHeaderConfig"]["Values"] + elif "Values" in condition: + values = condition["Values"] + if len(values) > 1: + raise InvalidConditionValueError( + "The 'host-header' field contains too many values; the limit is '1'" + ) + if values is None or len(values) == 0: + raise InvalidConditionValueError("A condition value must be specified") + for value in values: + if len(value) > 128: + raise InvalidConditionValueError( + "The 'host-header' value is too long; the limit is '128'" + ) + + def _validate_http_header_condition(self, condition): + if "HttpHeaderConfig" in condition: + config = condition["HttpHeaderConfig"] + name = config.get("HttpHeaderName") + if len(name) > 40: + raise InvalidConditionValueError( + "The 'HttpHeaderName' value is too long; the limit is '40'" + ) + values = config["Values"] + for value in values: + if len(value) > 128: + raise InvalidConditionValueError( + "The 'http-header' value is too long; the limit is '128'" + ) + else: + raise InvalidConditionValueError( + "A 'HttpHeaderConfig' must be specified with 'http-header'" + ) + + def _validate_http_request_method_condition(self, condition): + if "HttpRequestMethodConfig" in condition: + for value in condition["HttpRequestMethodConfig"]["Values"]: + if len(value) > 40: + raise InvalidConditionValueError( + "The 'http-request-method' value is too long; the limit is '40'" + ) + if not re.match("[A-Z_-]+", value): + raise InvalidConditionValueError( + "The 'http-request-method' value is invalid; the allowed characters are A-Z, hyphen and underscore" + ) + else: + raise InvalidConditionValueError( + "A 'HttpRequestMethodConfig' must be specified with 'http-request-method'" + ) + + def _validate_path_pattern_condition(self, condition): + values = None + if "PathPatternConfig" in condition: + values = condition["PathPatternConfig"]["Values"] + elif "Values" in condition: + values = condition["Values"] + if len(values) > 1: + raise InvalidConditionValueError( + "The 'path-pattern' field contains too many values; the limit is '1'" + ) + if values is None or len(values) == 0: + raise InvalidConditionValueError("A condition value must be specified") + for value in values: + if len(value) > 128: + raise InvalidConditionValueError( + "The 'path-pattern' value is too long; the limit is '128'" + ) + + def _validate_source_ip_condition(self, condition): + if "SourceIpConfig" in condition: + values = condition["SourceIpConfig"].get("Values", []) + if len(values) == 0: + raise InvalidConditionValueError( + "A 'source-ip' value must be specified" + ) + else: + raise InvalidConditionValueError( + "A 'SourceIpConfig' must be specified with 'source-ip'" + ) + + def _validate_query_string_condition(self, condition): + if "QueryStringConfig" in condition: + config = condition["QueryStringConfig"] + values = config["Values"] + for value in values: + if "Value" not in value: + raise InvalidConditionValueError( + "A 'Value' must be specified in 'QueryStringKeyValuePair'" + ) + if "Key" in value and len(value["Key"]) > 128: + raise InvalidConditionValueError( + "The 'Key' value is too long; the limit is '128'" + ) + if len(value["Value"]) > 128: + raise InvalidConditionValueError( + "The 'Value' value is too long; the limit is '128'" + ) + else: + raise InvalidConditionValueError( + "A 'QueryStringConfig' must be specified with 'query-string'" + ) + def _validate_actions(self, actions): # validate Actions target_group_arns = [ @@ -1063,24 +1167,9 @@ Member must satisfy regular expression pattern: {}".format( raise RuleNotFoundError() rule = rules[0] - if conditions: - for condition in conditions: - field = condition["field"] - if field not in ["path-pattern", "host-header"]: - raise InvalidConditionFieldError(field) - - values = condition["values"] - if len(values) == 0: - raise InvalidConditionValueError( - "A condition value must be specified" - ) - if len(values) > 1: - raise InvalidConditionValueError( - "The '%s' field contains too many values; the limit is '1'" - % field - ) - # TODO: check pattern of value for 'host-header' - # TODO: check pattern of value for 'path-pattern' + self._validate_conditions(conditions) + # TODO: check pattern of value for 'host-header' + # TODO: check pattern of value for 'path-pattern' # validate Actions self._validate_actions(actions) diff --git a/moto/elbv2/responses.py b/moto/elbv2/responses.py index b3d24cee3..049047dfc 100644 --- a/moto/elbv2/responses.py +++ b/moto/elbv2/responses.py @@ -151,24 +151,12 @@ class ELBV2Response(BaseResponse): @amzn_request_id def create_rule(self): - listener_arn = self._get_param("ListenerArn") - _conditions = self._get_list_prefix("Conditions.member") - conditions = [] - for _condition in _conditions: - condition = {} - condition["field"] = _condition["field"] - values = sorted( - [e for e in _condition.items() if "values.member" in e[0]], - key=lambda x: x[0], - ) - condition["values"] = [e[1] for e in values] - conditions.append(condition) - priority = self._get_int_param("Priority") + params = self._get_params() actions = self._get_list_prefix("Actions.member") rules = self.elbv2_backend.create_rule( - listener_arn=listener_arn, - conditions=conditions, - priority=priority, + listener_arn=params["ListenerArn"], + conditions=params["Conditions"], + priority=params["Priority"], actions=actions, ) template = self.response_template(CREATE_RULE_TEMPLATE) @@ -350,17 +338,8 @@ class ELBV2Response(BaseResponse): @amzn_request_id def modify_rule(self): rule_arn = self._get_param("RuleArn") - _conditions = self._get_list_prefix("Conditions.member") - conditions = [] - for _condition in _conditions: - condition = {} - condition["field"] = _condition["field"] - values = sorted( - [e for e in _condition.items() if "values.member" in e[0]], - key=lambda x: x[0], - ) - condition["values"] = [e[1] for e in values] - conditions.append(condition) + params = self._get_params() + conditions = params["Conditions"] actions = self._get_list_prefix("Actions.member") rules = self.elbv2_backend.modify_rule( rule_arn=rule_arn, conditions=conditions, actions=actions @@ -725,12 +704,72 @@ CREATE_RULE_TEMPLATE = """ + {{ condition["Field"] }} + {% if "Values" in condition %} - {% for value in condition["values"] %} + {% for value in condition["Values"] %} {{ value }} {% endfor %} + {% endif %} + {% if "HttpHeaderConfig" in condition %} + + {{ condition["HttpHeaderConfig"]["HttpHeaderName"] }} + + {% for value in condition["HttpHeaderConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "HttpRequestMethodConfig" in condition %} + + + {% for value in condition["HttpRequestMethodConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "QueryStringConfig" in condition %} + + + {% for value in condition["QueryStringConfig"]["Values"] %} + + {{ value["Key"] }} + {{ value["Value"] }} + + {% endfor %} + + + {% endif %} + {% if "SourceIpConfig" in condition %} + + + {% for value in condition["SourceIpConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "PathPatternConfig" in condition %} + + + {% for value in condition["PathPatternConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "HostHeaderConfig" in condition %} + + + {% for value in condition["HostHeaderConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} {% endfor %} @@ -912,12 +951,72 @@ DESCRIBE_RULES_TEMPLATE = """ + {{ condition["Field"] }} + {% if "HttpHeaderConfig" in condition %} + + {{ condition["HttpHeaderConfig"]["HttpHeaderName"] }} + + {% for value in condition["HttpHeaderConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "HttpRequestMethodConfig" in condition %} + + + {% for value in condition["HttpRequestMethodConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "QueryStringConfig" in condition %} + + + {% for value in condition["QueryStringConfig"]["Values"] %} + + {{ value["Key"] }} + {{ value["Value"] }} + + {% endfor %} + + + {% endif %} + {% if "SourceIpConfig" in condition %} + + + {% for value in condition["SourceIpConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "PathPatternConfig" in condition %} + + + {% for value in condition["PathPatternConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "HostHeaderConfig" in condition %} + + + {% for value in condition["HostHeaderConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "Values" in condition %} - {% for value in condition["values"] %} + {% for value in condition["Values"] %} {{ value }} {% endfor %} + {% endif %} {% endfor %} @@ -1053,12 +1152,72 @@ MODIFY_RULE_TEMPLATE = """ + {{ condition["Field"] }} + {% if "PathPatternConfig" in condition %} + + + {% for value in condition["PathPatternConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "HostHeaderConfig" in condition %} + + + {% for value in condition["HostHeaderConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "HttpHeaderConfig" in condition %} + + {{ condition["HttpHeaderConfig"]["HttpHeaderName"] }} + + {% for value in condition["HttpHeaderConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "HttpRequestMethodConfig" in condition %} + + + {% for value in condition["HttpRequestMethodConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "QueryStringConfig" in condition %} + + + {% for value in condition["QueryStringConfig"]["Values"] %} + + {{ value["Key"] }} + {{ value["Value"] }} + + {% endfor %} + + + {% endif %} + {% if "SourceIpConfig" in condition %} + + + {% for value in condition["SourceIpConfig"]["Values"] %} + {{ value }} + {% endfor %} + + + {% endif %} + {% if "Values" in condition %} - {% for value in condition["values"] %} + {% for value in condition["Values"] %} {{ value }} {% endfor %} + {% endif %} {% endfor %} diff --git a/tests/test_core/test_responses.py b/tests/test_core/test_responses.py index 587e3584b..73f024b9e 100644 --- a/tests/test_core/test_responses.py +++ b/tests/test_core/test_responses.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals import sure # noqa +from moto.compat import OrderedDict + from botocore.awsrequest import AWSPreparedRequest from moto.core.responses import AWSServiceSpec, BaseResponse @@ -92,3 +94,56 @@ def test_parse_qs_unicode_decode_error(): body = b'{"key": "%D0"}, "C": "#0 = :0"}' request = AWSPreparedRequest("GET", "http://request", {"foo": "bar"}, body, False) BaseResponse().setup_class(request, request.url, request.headers) + + +def test_get_params(): + subject = BaseResponse() + subject.querystring = OrderedDict( + [ + ("Action", ["CreateRule"]), + ("Version", ["2015-12-01"]), + ( + "ListenerArn", + [ + "arn:aws:elasticloadbalancing:us-east-1:1:listener/my-lb/50dc6c495c0c9188/80139731473870416" + ], + ), + ("Priority", ["100"]), + ("Conditions.member.1.Field", ["http-header"]), + ("Conditions.member.1.HttpHeaderConfig.HttpHeaderName", ["User-Agent"]), + ("Conditions.member.1.HttpHeaderConfig.Values.member.2", ["curl"]), + ("Conditions.member.1.HttpHeaderConfig.Values.member.1", ["Mozilla"]), + ("Actions.member.1.FixedResponseConfig.StatusCode", ["200"]), + ("Actions.member.1.FixedResponseConfig.ContentType", ["text/plain"]), + ("Actions.member.1.Type", ["fixed-response"]), + ] + ) + + result = subject._get_params() + + result.should.equal( + { + "Action": "CreateRule", + "Version": "2015-12-01", + "ListenerArn": "arn:aws:elasticloadbalancing:us-east-1:1:listener/my-lb/50dc6c495c0c9188/80139731473870416", + "Priority": "100", + "Conditions": [ + { + "Field": "http-header", + "HttpHeaderConfig": { + "HttpHeaderName": "User-Agent", + "Values": ["Mozilla", "curl"], + }, + } + ], + "Actions": [ + { + "Type": "fixed-response", + "FixedResponseConfig": { + "StatusCode": "200", + "ContentType": "text/plain", + }, + } + ], + } + ) diff --git a/tests/test_elbv2/test_elbv2.py b/tests/test_elbv2/test_elbv2.py index 1f1d8a74c..896671dc1 100644 --- a/tests/test_elbv2/test_elbv2.py +++ b/tests/test_elbv2/test_elbv2.py @@ -1213,7 +1213,7 @@ def test_handle_listener_rules(): obtained_rule = rules["Rules"][0] obtained_rule["Conditions"][0]["Values"][0].should.equal(new_host) obtained_rule["Conditions"][1]["Values"][0].should.equal(new_path_pattern) - obtained_rule["Conditions"][2]["Values"][0].should.equal( + obtained_rule["Conditions"][2]["PathPatternConfig"]["Values"][0].should.equal( new_pathpatternconfig_pattern ) obtained_rule["Actions"][0]["TargetGroupArn"].should.equal( @@ -1253,7 +1253,7 @@ def test_handle_listener_rules(): obtained_rule = rules["Rules"][2] obtained_rule["Conditions"][0]["Values"][0].should.equal(new_host_2) obtained_rule["Conditions"][1]["Values"][0].should.equal(new_path_pattern_2) - obtained_rule["Conditions"][2]["Values"][0].should.equal( + obtained_rule["Conditions"][2]["PathPatternConfig"]["Values"][0].should.equal( new_pathpatternconfig_pattern_2 ) obtained_rule["Actions"][0]["TargetGroupArn"].should.equal( diff --git a/tests/test_elbv2/test_elbv2_listener_rules.py b/tests/test_elbv2/test_elbv2_listener_rules.py new file mode 100644 index 000000000..b98b705a0 --- /dev/null +++ b/tests/test_elbv2/test_elbv2_listener_rules.py @@ -0,0 +1,297 @@ +from __future__ import unicode_literals + +import boto3 +import botocore +from botocore.exceptions import ClientError +import pytest +import sure # noqa + +from moto import mock_elbv2, mock_ec2 + +default_action = { + "FixedResponseConfig": {"StatusCode": "200", "ContentType": "text/plain"}, + "Type": "fixed-response", +} + + +def setup_listener(conn): + ec2 = boto3.resource("ec2", region_name="us-east-1") + + security_group = ec2.create_security_group( + GroupName="a-security-group", Description="First One" + ) + vpc = ec2.create_vpc(CidrBlock="172.28.7.0/24", InstanceTenancy="default") + subnet1 = ec2.create_subnet( + VpcId=vpc.id, CidrBlock="172.28.7.192/26", AvailabilityZone="us-east-1a" + ) + subnet2 = ec2.create_subnet( + VpcId=vpc.id, CidrBlock="172.28.7.0/26", AvailabilityZone="us-east-1b" + ) + + response = conn.create_load_balancer( + Name="my-lb", + Subnets=[subnet1.id, subnet2.id], + SecurityGroups=[security_group.id], + Scheme="internal", + Tags=[{"Key": "key_name", "Value": "a_value"}], + ) + + load_balancer_arn = response.get("LoadBalancers")[0].get("LoadBalancerArn") + + # Plain HTTP listener + response = conn.create_listener( + LoadBalancerArn=load_balancer_arn, + Protocol="HTTP", + Port=80, + DefaultActions=[ + { + "Type": "fixed-response", + "FixedResponseConfig": { + "StatusCode": "503", + "ContentType": "text/plain", + }, + } + ], + ) + listener = response.get("Listeners")[0] + http_listener_arn = listener.get("ListenerArn") + return http_listener_arn + + +@mock_elbv2 +@mock_ec2 +@pytest.mark.parametrize( + "condition", + [ + {"Field": "host-header", "Values": ["example.com"]}, + { + "Field": "host-header", + "HostHeaderConfig": {"Values": ["example.com", "www.example.com"]}, + }, + { + "Field": "http-header", + "HttpHeaderConfig": { + "HttpHeaderName": "User-Agent", + "Values": ["Mozilla"], + }, + }, + { + "Field": "http-request-method", + "HttpRequestMethodConfig": {"Values": ["GET", "POST"]}, + }, + {"Field": "path-pattern", "Values": ["/home"]}, + { + "Field": "path-pattern", + "PathPatternConfig": {"Values": ["/home", "/about"]}, + }, + { + "Field": "query-string", + "QueryStringConfig": {"Values": [{"Key": "hello", "Value": "world"}]}, + }, + {"Field": "source-ip", "SourceIpConfig": {"Values": ["172.28.7.0/24"]}}, + ], +) +def test_create_rule_condition(condition): + conn = boto3.client("elbv2", region_name="us-east-1") + + http_listener_arn = setup_listener(conn) + + # create_rule + response = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=100, + Conditions=[condition], + Actions=[default_action], + ) + + # assert create_rule response + response["Rules"].should.have.length_of(1) + rule = response.get("Rules")[0] + rule["Priority"].should.equal("100") + rule["Conditions"].should.equal([condition]) + + # assert describe_rules response + response = conn.describe_rules(ListenerArn=http_listener_arn) + response["Rules"].should.have.length_of(2) # including the default rule + + # assert describe_rules with arn filter response + rule = response["Rules"][0] + rule["Conditions"].should.equal([condition]) + response = conn.describe_rules(RuleArns=[rule["RuleArn"]]) + response["Rules"].should.equal([rule]) + + +@mock_elbv2 +@mock_ec2 +@pytest.mark.parametrize( + "create_condition,modify_condition", + [ + ( + {"Field": "host-header", "Values": ["example.com"]}, + { + "Field": "host-header", + "HostHeaderConfig": {"Values": ["example.com", "www.example.com"]}, + }, + ), + ( + { + "Field": "http-header", + "HttpHeaderConfig": { + "HttpHeaderName": "User-Agent", + "Values": ["Mozilla"], + }, + }, + { + "Field": "http-header", + "HttpHeaderConfig": { + "HttpHeaderName": "User-Agent", + "Values": ["Mozilla", "curl"], + }, + }, + ), + ( + {"Field": "path-pattern", "Values": ["/home"]}, + { + "Field": "path-pattern", + "PathPatternConfig": {"Values": ["/home", "/about"]}, + }, + ), + ], +) +def test_modify_rule_condition(create_condition, modify_condition): + conn = boto3.client("elbv2", region_name="us-east-1") + + http_listener_arn = setup_listener(conn) + response = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=100, + Conditions=[create_condition], + Actions=[default_action], + ) + rule = response.get("Rules")[0] + + # modify_rule + response = conn.modify_rule(RuleArn=rule["RuleArn"], Conditions=[modify_condition]) + response["Rules"].should.have.length_of(1) + modified_rule = response.get("Rules")[0] + modified_rule["Conditions"].should.equal([modify_condition]) + + +@mock_elbv2 +@mock_ec2 +@pytest.mark.parametrize( + "condition,expected_message", + [ + ( + {"Field": "host-header", "Values": ["x" * 256]}, + "The 'host-header' value is too long; the limit is '128'", + ), + ( + {"Field": "host-header", "HostHeaderConfig": {"Values": ["x" * 256]}}, + "The 'host-header' value is too long; the limit is '128'", + ), + ( + {"Field": "host-header", "Values": ["one", "two"]}, + "The 'host-header' field contains too many values; the limit is '1'", + ), + ({"Field": "host-header"}, "A condition value must be specified"), + ( + {"Field": "host-header", "HostHeaderConfig": {"Values": []}}, + "A condition value must be specified", + ), + ( + {"Field": "path-pattern", "Values": ["x" * 256]}, + "The 'path-pattern' value is too long; the limit is '128'", + ), + ( + {"Field": "path-pattern", "PathPatternConfig": {"Values": ["x" * 256]}}, + "The 'path-pattern' value is too long; the limit is '128'", + ), + ( + {"Field": "path-pattern", "Values": ["one", "two"]}, + "The 'path-pattern' field contains too many values; the limit is '1'", + ), + ({"Field": "path-pattern"}, "A condition value must be specified"), + ( + {"Field": "path-pattern", "PathPatternConfig": {"Values": []}}, + "A condition value must be specified", + ), + ( + { + "Field": "http-header", + "HttpHeaderConfig": {"HttpHeaderName": "x" * 50, "Values": ["y"]}, + }, + "The 'HttpHeaderName' value is too long; the limit is '40'", + ), + ( + { + "Field": "http-header", + "HttpHeaderConfig": {"HttpHeaderName": "x", "Values": ["y" * 256]}, + }, + "The 'http-header' value is too long; the limit is '128'", + ), + ( + { + "Field": "http-request-method", + "HttpRequestMethodConfig": {"Values": ["get"]}, + }, + "The 'http-request-method' value is invalid; the allowed characters are A-Z, hyphen and underscore", + ), + ( + { + "Field": "http-request-method", + "HttpRequestMethodConfig": {"Values": ["X" * 50]}, + }, + "The 'http-request-method' value is too long; the limit is '40'", + ), + ( + {"Field": "http-request-method"}, + "A 'HttpRequestMethodConfig' must be specified with 'http-request-method'", + ), + ( + { + "Field": "query-string", + "QueryStringConfig": {"Values": [{"Key": "x" * 256, "Value": "world"}]}, + }, + "The 'Key' value is too long; the limit is '128'", + ), + ( + { + "Field": "query-string", + "QueryStringConfig": {"Values": [{"Key": "hello", "Value": "x" * 256}]}, + }, + "The 'Value' value is too long; the limit is '128'", + ), + ( + { + "Field": "query-string", + "QueryStringConfig": {"Values": [{"Key": "hello"}]}, + }, + "A 'Value' must be specified in 'QueryStringKeyValuePair'", + ), + ( + {"Field": "source-ip", "SourceIpConfig": {"Values": []}}, + "A 'source-ip' value must be specified", + ), + ( + {"Field": "source-ip",}, + "A 'SourceIpConfig' must be specified with 'source-ip'", + ), + ], +) +def test_create_rule_validate_condition(condition, expected_message): + conn = boto3.client("elbv2", region_name="us-east-1") + + http_listener_arn = setup_listener(conn) + + with pytest.raises(ClientError) as ex: + response = conn.create_rule( + ListenerArn=http_listener_arn, + Priority=100, + Conditions=[condition], + Actions=[default_action], + ) + + err = ex.value.response["Error"] + err["Code"].should.equal("ValidationError") + err["Message"].should.equal(expected_message)