From d145471b3fc001ab537d19bc1ec4ed1e38b7a78a Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Wed, 31 Aug 2022 11:12:14 +0000 Subject: [PATCH] SNS:subscribe() now has increased support for the FilterPolicy-argument (#5436) --- moto/sns/models.py | 71 ++++++++++- tests/test_sns/test_publishing_boto3.py | 162 ++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 5 deletions(-) diff --git a/moto/sns/models.py b/moto/sns/models.py index e024afce8..55eea122b 100644 --- a/moto/sns/models.py +++ b/moto/sns/models.py @@ -265,8 +265,6 @@ class Subscription(BaseModel): ) def _matches_filter_policy(self, message_attributes): - # TODO: support Anything-but matching, prefix matching and - # numeric value matching. if not self._filter_policy: return True @@ -311,12 +309,75 @@ class Subscription(BaseModel): return True if isinstance(rule, dict): keyword = list(rule.keys())[0] - attributes = list(rule.values())[0] + value = list(rule.values())[0] if keyword == "exists": - if attributes and field in message_attributes: + if value and field in message_attributes: return True - elif not attributes and field not in message_attributes: + elif not value and field not in message_attributes: return True + elif keyword == "prefix" and isinstance(value, str): + if field in message_attributes: + attr = message_attributes[field] + if attr["Type"] == "String" and attr["Value"].startswith( + value + ): + return True + elif keyword == "anything-but": + if field not in message_attributes: + continue + attr = message_attributes[field] + if isinstance(value, dict): + # We can combine anything-but with the prefix-filter + anything_but_key = list(value.keys())[0] + anything_but_val = list(value.values())[0] + if anything_but_key != "prefix": + return False + if attr["Type"] == "String": + actual_values = [attr["Value"]] + else: + actual_values = [v for v in attr["Value"]] + if all( + [ + not v.startswith(anything_but_val) + for v in actual_values + ] + ): + return True + else: + undesired_values = ( + [value] if isinstance(value, str) else value + ) + if attr["Type"] == "Number": + actual_values = [str(attr["Value"])] + elif attr["Type"] == "String": + actual_values = [attr["Value"]] + else: + actual_values = [v for v in attr["Value"]] + if all([v not in undesired_values for v in actual_values]): + return True + elif keyword == "numeric" and isinstance(value, list): + # [(< x), (=, y), (>=, z)] + numeric_ranges = zip(value[0::2], value[1::2]) + if ( + message_attributes.get(field, {}).get("Type", "") + == "Number" + ): + msg_value = message_attributes[field]["Value"] + matches = [] + for operator, test_value in numeric_ranges: + test_value = int(test_value) + if operator == ">": + matches.append((msg_value > test_value)) + if operator == ">=": + matches.append((msg_value >= test_value)) + if operator == "=": + matches.append((msg_value == test_value)) + if operator == "<": + matches.append((msg_value < test_value)) + if operator == "<=": + matches.append((msg_value <= test_value)) + return all(matches) + attr = message_attributes[field] return False return all( diff --git a/tests/test_sns/test_publishing_boto3.py b/tests/test_sns/test_publishing_boto3.py index 3f71cf600..7968b2825 100644 --- a/tests/test_sns/test_publishing_boto3.py +++ b/tests/test_sns/test_publishing_boto3.py @@ -1120,3 +1120,165 @@ def test_filtering_all_AND_matching_no_match(): message_bodies.should.equal([]) message_attributes = [json.loads(m.body)["MessageAttributes"] for m in messages] message_attributes.should.equal([]) + + +@mock_sqs +@mock_sns +def test_filtering_prefix(): + topic, queue = _setup_filter_policy_test( + {"customer_interests": [{"prefix": "bas"}]} + ) + + for interest, idx in [("basketball", "1"), ("rugby", "2"), ("baseball", "3")]: + topic.publish( + Message=f"match{idx}", + MessageAttributes={ + "customer_interests": {"DataType": "String", "StringValue": interest}, + }, + ) + + messages = queue.receive_messages(MaxNumberOfMessages=5) + message_bodies = [json.loads(m.body)["Message"] for m in messages] + set(message_bodies).should.equal({"match1", "match3"}) + + +@mock_sqs +@mock_sns +def test_filtering_anything_but(): + topic, queue = _setup_filter_policy_test( + {"customer_interests": [{"anything-but": "basketball"}]} + ) + + for interest, idx in [("basketball", "1"), ("rugby", "2"), ("baseball", "3")]: + topic.publish( + Message=f"match{idx}", + MessageAttributes={ + "customer_interests": {"DataType": "String", "StringValue": interest}, + }, + ) + + messages = queue.receive_messages(MaxNumberOfMessages=5) + message_bodies = [json.loads(m.body)["Message"] for m in messages] + set(message_bodies).should.equal({"match2", "match3"}) + + +@mock_sqs +@mock_sns +def test_filtering_anything_but_multiple_values(): + topic, queue = _setup_filter_policy_test( + {"customer_interests": [{"anything-but": ["basketball", "rugby"]}]} + ) + + for interest, idx in [("basketball", "1"), ("rugby", "2"), ("baseball", "3")]: + topic.publish( + Message=f"match{idx}", + MessageAttributes={ + "customer_interests": {"DataType": "String", "StringValue": interest}, + }, + ) + + messages = queue.receive_messages(MaxNumberOfMessages=5) + message_bodies = [json.loads(m.body)["Message"] for m in messages] + set(message_bodies).should.equal({"match3"}) + + +@mock_sqs +@mock_sns +def test_filtering_anything_but_prefix(): + topic, queue = _setup_filter_policy_test( + {"customer_interests": [{"anything-but": {"prefix": "bas"}}]} + ) + + for interest, idx in [("basketball", "1"), ("rugby", "2"), ("baseball", "3")]: + topic.publish( + Message=f"match{idx}", + MessageAttributes={ + "customer_interests": {"DataType": "String", "StringValue": interest}, + }, + ) + + # This should match rugby only + messages = queue.receive_messages(MaxNumberOfMessages=5) + message_bodies = [json.loads(m.body)["Message"] for m in messages] + set(message_bodies).should.equal({"match2"}) + + +@mock_sqs +@mock_sns +def test_filtering_anything_but_unknown(): + topic, queue = _setup_filter_policy_test( + {"customer_interests": [{"anything-but": {"unknown": "bas"}}]} + ) + + for interest, idx in [("basketball", "1"), ("rugby", "2"), ("baseball", "3")]: + topic.publish( + Message=f"match{idx}", + MessageAttributes={ + "customer_interests": {"DataType": "String", "StringValue": interest}, + }, + ) + + # This should match rugby only + messages = queue.receive_messages(MaxNumberOfMessages=5) + message_bodies = [json.loads(m.body)["Message"] for m in messages] + message_bodies.should.equal([]) + + +@mock_sqs +@mock_sns +def test_filtering_anything_but_numeric(): + topic, queue = _setup_filter_policy_test( + {"customer_interests": [{"anything-but": ["100"]}]} + ) + + for nr, idx in [("50", "1"), ("100", "2"), ("150", "3")]: + topic.publish( + Message=f"match{idx}", + MessageAttributes={ + "customer_interests": {"DataType": "Number", "StringValue": nr}, + }, + ) + + messages = queue.receive_messages(MaxNumberOfMessages=5) + message_bodies = [json.loads(m.body)["Message"] for m in messages] + set(message_bodies).should.equal({"match1", "match3"}) + + +@mock_sqs +@mock_sns +def test_filtering_numeric_match(): + topic, queue = _setup_filter_policy_test( + {"customer_interests": [{"numeric": ["=", "100"]}]} + ) + + for nr, idx in [("50", "1"), ("100", "2"), ("150", "3")]: + topic.publish( + Message=f"match{idx}", + MessageAttributes={ + "customer_interests": {"DataType": "Number", "StringValue": nr}, + }, + ) + + messages = queue.receive_messages(MaxNumberOfMessages=5) + message_bodies = [json.loads(m.body)["Message"] for m in messages] + set(message_bodies).should.equal({"match2"}) + + +@mock_sqs +@mock_sns +def test_filtering_numeric_range(): + topic, queue = _setup_filter_policy_test( + {"customer_interests": [{"numeric": [">", "49", "<=", "100"]}]} + ) + + for nr, idx in [("50", "1"), ("100", "2"), ("150", "3")]: + topic.publish( + Message=f"match{idx}", + MessageAttributes={ + "customer_interests": {"DataType": "Number", "StringValue": nr}, + }, + ) + + messages = queue.receive_messages(MaxNumberOfMessages=5) + message_bodies = [json.loads(m.body)["Message"] for m in messages] + set(message_bodies).should.equal({"match1", "match2"})