Fix Exists pattern matching with None values (#5134)

Exists matching works on the presence or absence of a field in the JSON of the event.
The value of the field is not considered when evaluating a match.[1]

* Add sentinel to distinguish between missing fields and existing fields with None values
* Update event pattern matching tests to cover this specific use-case.
* Add integrated test to cover this scenario within a larger context.

[1]: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html
This commit is contained in:
Brian Pandola 2022-05-13 15:06:48 -07:00 committed by GitHub
parent f7c4d779c5
commit 30c2aeab29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 2 deletions

View File

@ -33,6 +33,9 @@ from uuid import uuid4
from .utils import PAGINATION_MODEL
# Sentinel to signal the absence of a field for `Exists` pattern matching
UNDEFINED = object()
class Rule(CloudFormationModel):
Arn = namedtuple("Arn", ["service", "resource_type", "resource_id"])
@ -827,7 +830,7 @@ class EventPattern:
return self._does_event_match(event, self._pattern)
def _does_event_match(self, event, pattern):
items_and_filters = [(event.get(k), v) for k, v in pattern.items()]
items_and_filters = [(event.get(k, UNDEFINED), v) for k, v in pattern.items()]
nested_filter_matches = [
self._does_event_match(item, nested_filter)
for item, nested_filter in items_and_filters
@ -856,7 +859,7 @@ class EventPattern:
filter_name, filter_value = list(pattern.items())[0]
if filter_name == "exists":
is_leaf_node = not isinstance(item, dict)
leaf_exists = is_leaf_node and item is not None
leaf_exists = is_leaf_node and item is not UNDEFINED
should_exist = filter_value
return leaf_exists if should_exist else not leaf_exists
if filter_name == "prefix":

View File

@ -23,6 +23,7 @@ def test_event_pattern_with_nested_event_filter():
def test_event_pattern_with_exists_event_filter():
foo_exists = EventPattern.load(json.dumps({"detail": {"foo": [{"exists": True}]}}))
assert foo_exists.matches_event({"detail": {"foo": "bar"}})
assert foo_exists.matches_event({"detail": {"foo": None}})
assert not foo_exists.matches_event({"detail": {}})
# exists filters only match leaf nodes of an event
assert not foo_exists.matches_event({"detail": {"foo": {"bar": "baz"}}})
@ -31,6 +32,7 @@ def test_event_pattern_with_exists_event_filter():
json.dumps({"detail": {"foo": [{"exists": False}]}})
)
assert not foo_not_exists.matches_event({"detail": {"foo": "bar"}})
assert not foo_not_exists.matches_event({"detail": {"foo": None}})
assert foo_not_exists.matches_event({"detail": {}})
assert foo_not_exists.matches_event({"detail": {"foo": {"bar": "baz"}}})

View File

@ -249,3 +249,72 @@ def test_send_to_sqs_queue_with_custom_event_bus():
body = json.loads(response["Messages"][0]["Body"])
body["detail"].should.equal({"key": "value"})
@mock_events
@mock_logs
def test_moto_matches_none_value_with_exists_filter():
pattern = {
"source": ["test-source"],
"detail-type": ["test-detail-type"],
"detail": {
"foo": [{"exists": True}],
"bar": [{"exists": True}],
},
}
logs_client = boto3.client("logs", region_name="eu-west-1")
log_group_name = "test-log-group"
logs_client.create_log_group(logGroupName=log_group_name)
events_client = boto3.client("events", region_name="eu-west-1")
event_bus_name = "test-event-bus"
events_client.create_event_bus(Name=event_bus_name)
rule_name = "test-event-rule"
events_client.put_rule(
Name=rule_name,
State="ENABLED",
EventPattern=json.dumps(pattern),
EventBusName=event_bus_name,
)
events_client.put_targets(
Rule=rule_name,
EventBusName=event_bus_name,
Targets=[
{
"Id": "123",
"Arn": f"arn:aws:logs:eu-west-1:{ACCOUNT_ID}:log-group:{log_group_name}",
}
],
)
events_client.put_events(
Entries=[
{
"EventBusName": event_bus_name,
"Source": "test-source",
"DetailType": "test-detail-type",
"Detail": json.dumps({"foo": "123", "bar": "123"}),
},
{
"EventBusName": event_bus_name,
"Source": "test-source",
"DetailType": "test-detail-type",
"Detail": json.dumps({"foo": None, "bar": "123"}),
},
]
)
events = sorted(
logs_client.filter_log_events(logGroupName=log_group_name)["events"],
key=lambda x: x["eventId"],
)
event_details = [json.loads(x["message"])["detail"] for x in events]
event_details.should.equal(
[
{"foo": "123", "bar": "123"},
{"foo": None, "bar": "123"},
],
)