Skip to content

Commit

Permalink
support anything-but fully
Browse files Browse the repository at this point in the history
  • Loading branch information
bentsku committed May 15, 2024
1 parent d233174 commit fa53c0b
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 16 deletions.
31 changes: 26 additions & 5 deletions localstack/services/sns/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,13 @@ def _evaluate_condition(self, value, condition, field_exists: bool):
# the remaining conditions require the value to not be None
return False
elif anything_but := condition.get("anything-but"):
# TODO: anything-but can be combined with prefix (and maybe others) by putting another condition in
# > "event":[{"anything-but": {"prefix": "order-"}}]
# > https://docs.aws.amazon.com/sns/latest/dg/string-value-matching.html#string-anything-but-matching-prefix
return value not in anything_but
if isinstance(anything_but, dict):
not_prefix = anything_but.get("prefix")
return not value.startswith(not_prefix)
elif isinstance(anything_but, list):
return value not in anything_but
else:
return value != anything_but
elif prefix := condition.get("prefix"):
return value.startswith(prefix)
elif suffix := condition.get("suffix"):
Expand Down Expand Up @@ -369,7 +372,6 @@ def _validate_rule(self, rule: t.Any) -> None:
operator, value = k, v

if operator in (
"anything-but",
"equals-ignore-case",
"prefix",
"suffix",
Expand All @@ -380,6 +382,25 @@ def _validate_rule(self, rule: t.Any) -> None:
)
return

elif operator == "anything-but":
# anything-but can actually contain any kind of simple rule (str, number, and list)
if isinstance(value, list):
for v in value:
self._validate_rule(v)

return

# or have a nested `prefix` pattern
elif isinstance(value, dict):
for inner_operator in value.keys():
if inner_operator != "prefix":
raise InvalidParameterException(
f"{self.error_prefix}FilterPolicy: Unsupported anything-but pattern: {inner_operator}"
)

self._validate_rule(value)
return

elif operator == "exists":
if not isinstance(value, bool):
raise InvalidParameterException(
Expand Down
81 changes: 79 additions & 2 deletions tests/aws/services/sns/test_sns_filter_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1067,7 +1067,6 @@ def get_messages(_queue_url: str, _received_messages: list):
snapshot.match("messages", {"Messages": received_messages})

@markers.aws.validated
@pytest.mark.skip("Not yet supported by LocalStack")
def test_filter_policy_on_message_body_or_attribute(
self,
sqs_create_queue,
Expand Down Expand Up @@ -1237,7 +1236,7 @@ def test_validate_policy_string_operators(
topic_arn = sns_create_topic()["TopicArn"]

def _subscribe(policy: dict):
sns_subscription(
return sns_subscription(
TopicArn=topic_arn,
Protocol="sms",
Endpoint=phone_number,
Expand All @@ -1262,6 +1261,18 @@ def _subscribe(policy: dict):
self._add_normalized_field_to_snapshot(e.value.response)
snapshot.match("error-condition-is-not-list-and-operator", e.value.response)

with pytest.raises(ClientError) as e:
filter_policy = {"key": [{"suffix": []}]}
_subscribe(filter_policy)
self._add_normalized_field_to_snapshot(e.value.response)
snapshot.match("error-condition-empty-list", e.value.response)

with pytest.raises(ClientError) as e:
filter_policy = {"key": [{"suffix": ["test", "test2"]}]}
_subscribe(filter_policy)
self._add_normalized_field_to_snapshot(e.value.response)
snapshot.match("error-condition-list-wrong-type", e.value.response)

with pytest.raises(ClientError) as e:
filter_policy = {"key": {"suffix": "value", "prefix": "value"}}
_subscribe(filter_policy)
Expand Down Expand Up @@ -1413,6 +1424,72 @@ def _subscribe(policy: dict):
self._add_normalized_field_to_snapshot(e.value.response)
snapshot.match("error-condition-string", e.value.response)

@markers.aws.validated
@markers.snapshot.skip_snapshot_verify(paths=["$..Error.Message"])
def test_validate_policy_nested_anything_but_operator(
self,
sns_create_topic,
sns_subscription,
snapshot,
aws_client,
):
phone_number = "+123123123"
topic_arn = sns_create_topic()["TopicArn"]

def _subscribe(policy: dict):
return sns_subscription(
TopicArn=topic_arn,
Protocol="sms",
Endpoint=phone_number,
Attributes={"FilterPolicy": json.dumps(policy)},
)

with pytest.raises(ClientError) as e:
filter_policy = {"key": [{"anything-but": {"wrong-operator": None}}]}
_subscribe(filter_policy)
self._add_normalized_field_to_snapshot(e.value.response)
snapshot.match("error-condition-wrong-operator", e.value.response)

with pytest.raises(ClientError) as e:
filter_policy = {"key": [{"anything-but": {"suffix": "test"}}]}
_subscribe(filter_policy)
self._add_normalized_field_to_snapshot(e.value.response)
snapshot.match("error-condition-anything-but-suffix", e.value.response)

with pytest.raises(ClientError) as e:
filter_policy = {"key": [{"anything-but": {"exists": False}}]}
_subscribe(filter_policy)
self._add_normalized_field_to_snapshot(e.value.response)
snapshot.match("error-condition-anything-but-exists", e.value.response)

with pytest.raises(ClientError) as e:
filter_policy = {"key": [{"anything-but": {"prefix": False}}]}
_subscribe(filter_policy)
self._add_normalized_field_to_snapshot(e.value.response)
snapshot.match("error-condition-anything-but-prefix-wrong-type", e.value.response)

# positive testing
filter_policy = {"key": [{"anything-but": {"prefix": "test-"}}]}
response = _subscribe(filter_policy)
assert "SubscriptionArn" in response
subscription_arn = response["SubscriptionArn"]

filter_policy = {"key": [{"anything-but": ["test", "test2"]}]}
response = aws_client.sns.set_subscription_attributes(
SubscriptionArn=subscription_arn,
AttributeName="FilterPolicy",
AttributeValue=json.dumps(filter_policy),
)
assert response["ResponseMetadata"]["HTTPStatusCode"] == 200

filter_policy = {"key": [{"anything-but": "test"}]}
response = aws_client.sns.set_subscription_attributes(
SubscriptionArn=subscription_arn,
AttributeName="FilterPolicy",
AttributeValue=json.dumps(filter_policy),
)
assert response["ResponseMetadata"]["HTTPStatusCode"] == 200

@markers.aws.validated
def test_policy_complexity(
self,
Expand Down
79 changes: 78 additions & 1 deletion tests/aws/services/sns/test_sns_filter_policy.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
}
},
"tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_string_operators": {
"recorded-date": "14-05-2024, 16:51:03",
"recorded-date": "15-05-2024, 14:39:23",
"recorded-content": {
"error-condition-is-numeric": {
"Error": {
Expand Down Expand Up @@ -79,6 +79,30 @@
"HTTPStatusCode": 400
}
},
"error-condition-empty-list": {
"Error": {
"Code": "InvalidParameter",
"Message": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string\n at [Source: (String)\"{\"key\":[{\"suffix\":[]}]}\"; line: 1, column: 20]",
"Type": "Sender",
"_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 400
}
},
"error-condition-list-wrong-type": {
"Error": {
"Code": "InvalidParameter",
"Message": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string\n at [Source: (String)\"{\"key\":[{\"suffix\":[\"test\",\"test2\"]}]}\"; line: 1, column: 20]",
"Type": "Sender",
"_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 400
}
},
"error-condition-is-not-list-two-ops": {
"Error": {
"Code": "InvalidParameter",
Expand Down Expand Up @@ -1529,5 +1553,58 @@
"tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_or_attribute": {
"recorded-date": "14-05-2024, 16:51:02",
"recorded-content": {}
},
"tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_nested_anything_but_operator": {
"recorded-date": "15-05-2024, 14:39:32",
"recorded-content": {
"error-condition-wrong-operator": {
"Error": {
"Code": "InvalidParameter",
"Message": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: wrong-operator\n at [Source: (String)\"{\"key\":[{\"anything-but\":{\"wrong-operator\":null}}]}\"; line: 1, column: 47]",
"Type": "Sender",
"_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: wrong-operator"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 400
}
},
"error-condition-anything-but-suffix": {
"Error": {
"Code": "InvalidParameter",
"Message": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: suffix\n at [Source: (String)\"{\"key\":[{\"anything-but\":{\"suffix\":\"test\"}}]}\"; line: 1, column: 36]",
"Type": "Sender",
"_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: suffix"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 400
}
},
"error-condition-anything-but-exists": {
"Error": {
"Code": "InvalidParameter",
"Message": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: exists\n at [Source: (String)\"{\"key\":[{\"anything-but\":{\"exists\":false}}]}\"; line: 1, column: 40]",
"Type": "Sender",
"_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: exists"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 400
}
},
"error-condition-anything-but-prefix-wrong-type": {
"Error": {
"Code": "InvalidParameter",
"Message": "Invalid parameter: Attributes Reason: FilterPolicy: prefix match pattern must be a string\n at [Source: (String)\"{\"key\":[{\"anything-but\":{\"prefix\":false}}]}\"; line: 1, column: 40]",
"Type": "Sender",
"_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: prefix match pattern must be a string"
},
"ResponseMetadata": {
"HTTPHeaders": {},
"HTTPStatusCode": 400
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@
"tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_exists_operator": {
"last_validated_date": "2024-05-14T16:51:06+00:00"
},
"tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_nested_anything_but_operator": {
"last_validated_date": "2024-05-15T14:39:32+00:00"
},
"tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_numeric_operator": {
"last_validated_date": "2024-05-14T16:51:06+00:00"
},
"tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_string_operators": {
"last_validated_date": "2024-05-14T16:51:03+00:00"
"last_validated_date": "2024-05-15T14:39:23+00:00"
},
"tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_set_subscription_filter_policy_scope": {
"last_validated_date": "2024-05-14T16:49:11+00:00"
Expand Down
52 changes: 45 additions & 7 deletions tests/unit/test_sns.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,30 @@ def test_filter_policy(self):
{"filter": {"Type": "String", "Value": "type2"}},
True,
),
(
"anything-but list filter with match",
{"filter": [{"anything-but": ["type1", "type2"]}]},
{"filter": {"Type": "String", "Value": "type1"}},
False,
),
(
"anything-but list filter with no match",
{"filter": [{"anything-but": ["type1", "type3"]}]},
{"filter": {"Type": "String", "Value": "type2"}},
True,
),
(
"anything-but string filter with prefix match",
{"filter": [{"anything-but": {"prefix": "type"}}]},
{"filter": {"Type": "String", "Value": "type1"}},
False,
),
(
"anything-but string filter with no prefix match",
{"filter": [{"anything-but": {"prefix": "type-"}}]},
{"filter": {"Type": "String", "Value": "type1"}},
True,
),
(
"prefix string filter with match",
{"filter": [{"prefix": "typ"}]},
Expand Down Expand Up @@ -905,11 +929,25 @@ def test_filter_policy_complexity(self):
)
def test_filter_flatten_payload(self, payload, expected):
sub_filter = SubscriptionFilter()
# payload = {"f3": ["v3"], "f1": {"f2": "v2"}}
# expected = [
# {
# "f3": "v3",
# "f1.f2": "v2",
# }
# ]
assert sub_filter.flatten_payload(payload) == expected

@pytest.mark.parametrize(
"policy,expected",
[
(
{"filter": [{"anything-but": {"prefix": "type"}}]},
[{"filter": [{"anything-but": {"prefix": "type"}}]}],
),
(
{"field1": {"field2": {"field3": "val1", "field4": "val2"}}},
[{"field1.field2.field3": "val1", "field1.field2.field4": "val2"}],
),
(
{"$or": [{"field1": "val1"}, {"field2": "val2"}], "field3": "val3"},
[{"field1": "val1", "field3": "val3"}, {"field2": "val2", "field3": "val3"}],
),
],
)
def test_filter_flatten_policy(self, policy, expected):
sub_filter = SubscriptionFilter()
assert sub_filter.flatten_policy(policy) == expected

0 comments on commit fa53c0b

Please sign in to comment.