diff --git a/moto/core/exceptions.py b/moto/core/exceptions.py index 547ac0377..579024f7c 100644 --- a/moto/core/exceptions.py +++ b/moto/core/exceptions.py @@ -9,7 +9,7 @@ import json SINGLE_ERROR_RESPONSE = """ {{error_type}} - {{message}} + {% block extra %}{% endblock %} <{{request_id_tag}}>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE @@ -19,7 +19,7 @@ WRAPPED_SINGLE_ERROR_RESPONSE = """ {{error_type}} - {{message}} + {% block extra %}{% endblock %} <{{request_id_tag}}>7a62c49f-347e-4fc4-9331-6e8eEXAMPLE diff --git a/moto/sns/models.py b/moto/sns/models.py index bdb5fe8f3..97e4d837f 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -301,10 +301,11 @@ class Subscription(BaseModel): else: return False - for attribute_values in attribute_values: + for attribute_value in attribute_values: + attribute_value = float(attribute_value) # Even the official documentation states a 5 digits of accuracy after the decimal point for numerics, in reality it is 6 # https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html#subscription-filter-policy-constraints - if int(attribute_values * 1000000) == int(rule * 1000000): + if int(attribute_value * 1000000) == int(rule * 1000000): return True if isinstance(rule, dict): keyword = list(rule.keys())[0] @@ -347,7 +348,7 @@ class Subscription(BaseModel): [value] if isinstance(value, str) else value ) if attr["Type"] == "Number": - actual_values = [str(attr["Value"])] + actual_values = [float(attr["Value"])] elif attr["Type"] == "String": actual_values = [attr["Value"]] else: @@ -361,10 +362,10 @@ class Subscription(BaseModel): message_attributes.get(field, {}).get("Type", "") == "Number" ): - msg_value = message_attributes[field]["Value"] + msg_value = float(message_attributes[field]["Value"]) matches = [] for operator, test_value in numeric_ranges: - test_value = int(test_value) + test_value = test_value if operator == ">": matches.append((msg_value > test_value)) if operator == ">=": @@ -376,7 +377,6 @@ class Subscription(BaseModel): if operator == "<=": matches.append((msg_value <= test_value)) return all(matches) - attr = message_attributes[field] return False return all( @@ -832,6 +832,70 @@ class SNSBackend(BaseBackend): ) continue elif keyword == "numeric": + # TODO: All of the exceptions listed below contain column pointing where the error is (in AWS response) + # Example: 'Value of < must be numeric\n at [Source: (String)"{"price":[{"numeric":["<","100"]}]}"; line: 1, column: 28]' + # While it probably can be implemented, it doesn't feel as important as the general parameter checking + + attributes_copy = attributes[:] + if not attributes_copy: + raise SNSInvalidParameter( + "Invalid parameter: Attributes Reason: FilterPolicy: Invalid member in numeric match: ]\n at ..." + ) + + operator = attributes_copy.pop(0) + + if not isinstance(operator, str): + raise SNSInvalidParameter( + f"Invalid parameter: Attributes Reason: FilterPolicy: Invalid member in numeric match: {(str(operator))}\n at ..." + ) + + if operator not in ("<", "<=", "=", ">", ">="): + raise SNSInvalidParameter( + f"Invalid parameter: Attributes Reason: FilterPolicy: Unrecognized numeric range operator: {(str(operator))}\n at ..." + ) + + try: + value = attributes_copy.pop(0) + except IndexError: + value = None + + if value is None or not isinstance(value, (int, float)): + raise SNSInvalidParameter( + f"Invalid parameter: Attributes Reason: FilterPolicy: Value of {(str(operator))} must be numeric\n at ..." + ) + + if not attributes_copy: + continue + + if operator not in (">", ">="): + raise SNSInvalidParameter( + "Invalid parameter: Attributes Reason: FilterPolicy: Too many elements in numeric expression\n at ..." + ) + + second_operator = attributes_copy.pop(0) + + if second_operator not in ("<", "<="): + raise SNSInvalidParameter( + f"Invalid parameter: Attributes Reason: FilterPolicy: Bad numeric range operator: {(str(second_operator))}\n at ..." + ) + + try: + second_value = attributes_copy.pop(0) + except IndexError: + second_value = None + + if second_value is None or not isinstance( + second_value, (int, float) + ): + raise SNSInvalidParameter( + f"Invalid parameter: Attributes Reason: FilterPolicy: Value of {(str(second_operator))} must be numeric\n at ..." + ) + + if second_value <= value: + raise SNSInvalidParameter( + "Invalid parameter: Attributes Reason: FilterPolicy: Bottom must be less than top\n at ..." + ) + continue elif keyword == "prefix": continue diff --git a/moto/sns/responses.py b/moto/sns/responses.py index 7d343deb8..9e6d26c9a 100644 --- a/moto/sns/responses.py +++ b/moto/sns/responses.py @@ -66,20 +66,19 @@ class SNSResponse(BaseResponse): transform_value = None if "StringValue" in value: + transform_value = value["StringValue"] if data_type == "Number": try: - transform_value = int(value["StringValue"]) + int(transform_value) except ValueError: try: - transform_value = float(value["StringValue"]) + float(transform_value) except ValueError: raise InvalidParameterValue( "An error occurred (ParameterValueInvalid) " "when calling the Publish operation: " f"Could not cast message attribute '{name}' value to number." ) - else: - transform_value = value["StringValue"] elif "BinaryValue" in value: transform_value = value["BinaryValue"] if transform_value == "": diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index b73217a17..76c441167 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -230,7 +230,7 @@ def test_publish_to_sqs_msg_attr_number_type(): message = json.loads(queue.receive_messages()[0].body) message["Message"].should.equal("test message") message["MessageAttributes"].should.equal( - {"retries": {"Type": "Number", "Value": 0}} + {"retries": {"Type": "Number", "Value": "0"}} ) message = queue_raw.receive_messages()[0] @@ -731,7 +731,7 @@ def test_filtering_exact_number_int(): message_bodies = [json.loads(m.body)["Message"] for m in messages] message_bodies.should.equal(["match"]) message_attributes = [json.loads(m.body)["MessageAttributes"] for m in messages] - message_attributes.should.equal([{"price": {"Type": "Number", "Value": 100}}]) + message_attributes.should.equal([{"price": {"Type": "Number", "Value": "100"}}]) @mock_sqs @@ -748,7 +748,7 @@ def test_filtering_exact_number_float(): message_bodies = [json.loads(m.body)["Message"] for m in messages] message_bodies.should.equal(["match"]) message_attributes = [json.loads(m.body)["MessageAttributes"] for m in messages] - message_attributes.should.equal([{"price": {"Type": "Number", "Value": 100.1}}]) + message_attributes.should.equal([{"price": {"Type": "Number", "Value": "100.1"}}]) @mock_sqs @@ -759,7 +759,7 @@ def test_filtering_exact_number_float_accuracy(): topic.publish( Message="match", MessageAttributes={ - "price": {"DataType": "Number", "StringValue": "100.1234561"} + "price": {"DataType": "Number", "StringValue": "100.1234567"} }, ) @@ -768,7 +768,7 @@ def test_filtering_exact_number_float_accuracy(): message_bodies.should.equal(["match"]) message_attributes = [json.loads(m.body)["MessageAttributes"] for m in messages] message_attributes.should.equal( - [{"price": {"Type": "Number", "Value": 100.1234561}}] + [{"price": {"Type": "Number", "Value": "100.1234567"}}] ) @@ -892,7 +892,7 @@ def test_filtering_string_array_with_number_float_accuracy_match(): MessageAttributes={ "price": { "DataType": "String.Array", - "StringValue": json.dumps([100.1234561, 50]), + "StringValue": json.dumps([100.1234567, 50]), } }, ) @@ -902,7 +902,7 @@ def test_filtering_string_array_with_number_float_accuracy_match(): message_bodies.should.equal(["match"]) message_attributes = [json.loads(m.body)["MessageAttributes"] for m in messages] message_attributes.should.equal( - [{"price": {"Type": "String.Array", "Value": json.dumps([100.1234561, 50])}}] + [{"price": {"Type": "String.Array", "Value": json.dumps([100.1234567, 50])}}] ) @@ -1083,7 +1083,7 @@ def test_filtering_all_AND_matching_match(): "Type": "String.Array", "Value": json.dumps(["basketball", "rugby"]), }, - "price": {"Type": "Number", "Value": 100}, + "price": {"Type": "Number", "Value": "100"}, } ] ) @@ -1228,7 +1228,7 @@ def test_filtering_anything_but_unknown(): @mock_sns def test_filtering_anything_but_numeric(): topic, queue = _setup_filter_policy_test( - {"customer_interests": [{"anything-but": ["100"]}]} + {"customer_interests": [{"anything-but": [100]}]} ) for nr, idx in [("50", "1"), ("100", "2"), ("150", "3")]: @@ -1248,7 +1248,7 @@ def test_filtering_anything_but_numeric(): @mock_sns def test_filtering_numeric_match(): topic, queue = _setup_filter_policy_test( - {"customer_interests": [{"numeric": ["=", "100"]}]} + {"customer_interests": [{"numeric": ["=", 100]}]} ) for nr, idx in [("50", "1"), ("100", "2"), ("150", "3")]: @@ -1268,7 +1268,7 @@ def test_filtering_numeric_match(): @mock_sns def test_filtering_numeric_range(): topic, queue = _setup_filter_policy_test( - {"customer_interests": [{"numeric": [">", "49", "<=", "100"]}]} + {"customer_interests": [{"numeric": [">", 49, "<=", 100]}]} ) for nr, idx in [("50", "1"), ("100", "2"), ("150", "3")]: diff --git a/tests/test_sns/test_subscriptions_boto3.py b/tests/test_sns/test_subscriptions_boto3.py index 25d9136be..9066c3cae 100644 --- a/tests/test_sns/test_subscriptions_boto3.py +++ b/tests/test_sns/test_subscriptions_boto3.py @@ -416,7 +416,7 @@ def test_subscribe_invalid_filter_policy(): response = conn.list_topics() topic_arn = response["Topics"][0]["TopicArn"] - try: + with pytest.raises(ClientError) as err_info: conn.subscribe( TopicArn=topic_arn, Protocol="http", @@ -425,60 +425,221 @@ def test_subscribe_invalid_filter_policy(): "FilterPolicy": json.dumps({"store": [str(i) for i in range(151)]}) }, ) - except ClientError as err: - err.response["Error"]["Code"].should.equal("InvalidParameter") - err.response["Error"]["Message"].should.equal( - "Invalid parameter: FilterPolicy: Filter policy is too complex" - ) - try: + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: FilterPolicy: Filter policy is too complex" + ) + + with pytest.raises(ClientError) as err_info: conn.subscribe( TopicArn=topic_arn, Protocol="http", Endpoint="http://example.com/", Attributes={"FilterPolicy": json.dumps({"store": [["example_corp"]]})}, ) - except ClientError as err: - err.response["Error"]["Code"].should.equal("InvalidParameter") - err.response["Error"]["Message"].should.equal( - "Invalid parameter: FilterPolicy: Match value must be String, number, true, false, or null" - ) - try: + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: FilterPolicy: Match value must be String, number, true, false, or null" + ) + + with pytest.raises(ClientError) as err_info: conn.subscribe( TopicArn=topic_arn, Protocol="http", Endpoint="http://example.com/", Attributes={"FilterPolicy": json.dumps({"store": [{"exists": None}]})}, ) - except ClientError as err: - err.response["Error"]["Code"].should.equal("InvalidParameter") - err.response["Error"]["Message"].should.equal( - "Invalid parameter: FilterPolicy: exists match pattern must be either true or false." - ) - try: + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: FilterPolicy: exists match pattern must be either true or false." + ) + + with pytest.raises(ClientError) as err_info: conn.subscribe( TopicArn=topic_arn, Protocol="http", Endpoint="http://example.com/", Attributes={"FilterPolicy": json.dumps({"store": [{"error": True}]})}, ) - except ClientError as err: - err.response["Error"]["Code"].should.equal("InvalidParameter") - err.response["Error"]["Message"].should.equal( - "Invalid parameter: FilterPolicy: Unrecognized match type error" - ) - try: + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: FilterPolicy: Unrecognized match type error" + ) + + with pytest.raises(ClientError) as err_info: conn.subscribe( TopicArn=topic_arn, Protocol="http", Endpoint="http://example.com/", Attributes={"FilterPolicy": json.dumps({"store": [1000000001]})}, ) - except ClientError as err: - err.response["Error"]["Code"].should.equal("InternalFailure") + + err = err_info.value + err.response["Error"]["Code"].should.equal("InternalFailure") + + with pytest.raises(ClientError) as err_info: + conn.subscribe( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/", + Attributes={ + "FilterPolicy": json.dumps({"price": [{"numeric": ["<", "100"]}]}) + }, + ) + + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: Attributes Reason: FilterPolicy: Value of < must be numeric\n at ..." + ) + + with pytest.raises(ClientError) as err_info: + conn.subscribe( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/", + Attributes={ + "FilterPolicy": json.dumps( + {"price": [{"numeric": [">", 50, "<=", "100"]}]} + ) + }, + ) + + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: Attributes Reason: FilterPolicy: Value of <= must be numeric\n at ..." + ) + + with pytest.raises(ClientError) as err_info: + conn.subscribe( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/", + Attributes={"FilterPolicy": json.dumps({"price": [{"numeric": []}]})}, + ) + + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: Attributes Reason: FilterPolicy: Invalid member in numeric match: ]\n at ..." + ) + + with pytest.raises(ClientError) as err_info: + conn.subscribe( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/", + Attributes={ + "FilterPolicy": json.dumps({"price": [{"numeric": [50, "<=", "100"]}]}) + }, + ) + + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: Attributes Reason: FilterPolicy: Invalid member in numeric match: 50\n at ..." + ) + + with pytest.raises(ClientError) as err_info: + conn.subscribe( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/", + Attributes={"FilterPolicy": json.dumps({"price": [{"numeric": ["<"]}]})}, + ) + + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: Attributes Reason: FilterPolicy: Value of < must be numeric\n at ..." + ) + + with pytest.raises(ClientError) as err_info: + conn.subscribe( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/", + Attributes={"FilterPolicy": json.dumps({"price": [{"numeric": ["0"]}]})}, + ) + + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: Attributes Reason: FilterPolicy: Unrecognized numeric range operator: 0\n at ..." + ) + + with pytest.raises(ClientError) as err_info: + conn.subscribe( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/", + Attributes={ + "FilterPolicy": json.dumps({"price": [{"numeric": ["<", 20, ">", 1]}]}) + }, + ) + + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: Attributes Reason: FilterPolicy: Too many elements in numeric expression\n at ..." + ) + + with pytest.raises(ClientError) as err_info: + conn.subscribe( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/", + Attributes={ + "FilterPolicy": json.dumps({"price": [{"numeric": [">", 20, ">", 1]}]}) + }, + ) + + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: Attributes Reason: FilterPolicy: Bad numeric range operator: >\n at ..." + ) + + with pytest.raises(ClientError) as err_info: + conn.subscribe( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/", + Attributes={ + "FilterPolicy": json.dumps({"price": [{"numeric": [">", 20, "<", 1]}]}) + }, + ) + + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: Attributes Reason: FilterPolicy: Bottom must be less than top\n at ..." + ) + + with pytest.raises(ClientError) as err_info: + conn.subscribe( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com/", + Attributes={ + "FilterPolicy": json.dumps({"price": [{"numeric": [">", 20, "<"]}]}) + }, + ) + + err = err_info.value + err.response["Error"]["Code"].should.equal("InvalidParameter") + err.response["Error"]["Message"].should.equal( + "Invalid parameter: Attributes Reason: FilterPolicy: Value of < must be numeric\n at ..." + ) @mock_sns