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:
parent
f7c4d779c5
commit
30c2aeab29
@ -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":
|
||||
|
@ -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"}}})
|
||||
|
||||
|
@ -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"},
|
||||
],
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user