From 15eda737d66a517c38a8909abb47d086db607943 Mon Sep 17 00:00:00 2001 From: Tom Noble <53005340+TSNoble@users.noreply.github.com> Date: Fri, 2 Apr 2021 18:32:01 +0100 Subject: [PATCH] Add exists filtering functionality to `Archive` (#3832) * Add exists filtering functionality to Archive. Add test case and refactor existing Archive EventPattern test cases * Apply black formatting * Change NotImplementedError to warning * Simplify unimplemented warning for filters * Change str check to six.string_types check for python2.7 Co-authored-by: Tom Noble --- moto/events/models.py | 54 +++++++++++----- tests/test_events/test_events.py | 108 +++++++++++++++++++------------ 2 files changed, 107 insertions(+), 55 deletions(-) diff --git a/moto/events/models.py b/moto/events/models.py index b17d21a6b..8925f9b7c 100644 --- a/moto/events/models.py +++ b/moto/events/models.py @@ -9,6 +9,7 @@ from datetime import datetime from enum import Enum, unique from boto3 import Session +from six import string_types from moto.core.exceptions import JsonRESTError from moto.core import ACCOUNT_ID, BaseBackend, CloudFormationModel, BaseModel @@ -93,23 +94,46 @@ class Rule(CloudFormationModel): if index is not None: self.targets.pop(index) - def _does_event_match_pattern(self, event, pattern): - if not pattern: + def _does_event_match_filter(self, event, filter): + if not filter: return True - event_pattern_pairs = [(event.get(k), v) for k, v in pattern.items()] - for event_item, pattern_item in event_pattern_pairs: - if not self._does_event_item_match_pattern_item(event_item, pattern_item): - return False - return True + items_and_filters = [(event.get(k), v) for k, v in filter.items()] + nested_filter_matches = [ + self._does_event_match_filter(item, nested_filter) + for item, nested_filter in items_and_filters + if isinstance(nested_filter, dict) + ] + filter_list_matches = [ + self._does_item_match_filters(item, filter_list) + for item, filter_list in items_and_filters + if isinstance(filter_list, list) + ] + return all(nested_filter_matches + filter_list_matches) - def _does_event_item_match_pattern_item(self, event_item, pattern_item): - # Only supports "key: [value]" filters currently - if not event_item: + def _does_item_match_filters(self, item, filters): + allowed_values = [value for value in filters if isinstance(value, string_types)] + allowed_values_match = item in allowed_values if allowed_values else True + print(item, filters, allowed_values) + named_filter_matches = [ + self._does_item_match_named_filter(item, filter) + for filter in filters + if isinstance(filter, dict) + ] + return allowed_values_match and all(named_filter_matches) + + def _does_item_match_named_filter(self, item, filter): + filter_name, filter_value = list(filter.items())[0] + if filter_name == "exists": + item_exists = item is not None + should_exist = filter_value + return item_exists if should_exist else not item_exists + else: + warnings.warn( + "'{}' filter logic unimplemented. defaulting to True".format( + filter_name + ) + ) return False - if isinstance(pattern_item, list): - return event_item in pattern_item - if isinstance(pattern_item, dict): - return self._does_event_match_pattern(event_item, pattern_item) def send_to_targets(self, event_bus_name, event): event_bus_name = event_bus_name.split("/")[-1] @@ -210,7 +234,7 @@ class Rule(CloudFormationModel): if archive.uuid == archive_uuid: event = json.loads(json.dumps(event)) pattern = json.loads(pattern) if pattern else None - if self._does_event_match_pattern(event, pattern): + if self._does_event_match_filter(event, pattern): archive.events.append(event) def _send_to_sqs_queue(self, resource_id, event, group_id=None): diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index 43580e888..7fdc1b0f1 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -1518,56 +1518,84 @@ def test_archive_event_with_bus_arn(): @mock_events -def test_event_not_routed_to_archive_when_detail_does_not_match_pattern(): - # given +def test_archive_with_allowed_values_event_filter(): client = boto3.client("events", "eu-central-1") event_bus_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format( ACCOUNT_ID ) client.create_archive( - ArchiveName="archive-with-dict-filter", - EventSourceArn=event_bus_arn, - EventPattern=json.dumps({"detail": {"foo": ["bar"]}}), - ) - client.create_archive( - ArchiveName="archive-with-list-filter", + ArchiveName="with-allowed-values-filter", EventSourceArn=event_bus_arn, EventPattern=json.dumps({"source": ["foo", "bar"]}), ) - - # when - event_matching_detail = { - "Source": "source", - "DetailType": "type", - "Detail": '{"foo": "bar"}', - } - event_not_matching_detail = { - "Source": "source", - "DetailType": "type", - "Detail": '{"foo": "baz"}', - } - event_matching_source_foo = {"Source": "foo", "DetailType": "type", "Detail": "{}"} - event_matching_source_bar = {"Source": "bar", "DetailType": "type", "Detail": "{}"} - event_not_matching_source = {"Source": "baz", "DetailType": "type", "Detail": "{}"} - - client.put_events( - Entries=[ - event_matching_detail, - event_not_matching_detail, - event_matching_source_foo, - event_matching_source_bar, - event_not_matching_source, - ] + matching_foo_event = {"Source": "foo", "DetailType": "", "Detail": "{}"} + matching_bar_event = {"Source": "bar", "DetailType": "", "Detail": "{}"} + non_matching_event = {"Source": "baz", "DetailType": "", "Detail": "{}"} + response = client.put_events( + Entries=[matching_foo_event, matching_bar_event, non_matching_event] ) - - # then - response = client.describe_archive(ArchiveName="archive-with-dict-filter") - response["EventCount"].should.equal(1) - response["SizeBytes"].should.be.greater_than(0) - - response = client.describe_archive(ArchiveName="archive-with-list-filter") + response["FailedEntryCount"].should.equal(0) + response = client.describe_archive(ArchiveName="with-allowed-values-filter") + response["EventCount"].should.equal(2) + + +@mock_events +def test_archive_with_nested_event_filter(): + client = boto3.client("events", "eu-central-1") + event_bus_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format( + ACCOUNT_ID + ) + client.create_archive( + ArchiveName="with-nested-filter", + EventSourceArn=event_bus_arn, + EventPattern=json.dumps({"detail": {"foo": ["bar"]}}), + ) + matching_event = {"Source": "", "DetailType": "", "Detail": '{"foo": "bar"}'} + not_matching_event = {"Source": "", "DetailType": "", "Detail": '{"foo": "baz"}'} + response = client.put_events(Entries=[matching_event, not_matching_event]) + response["FailedEntryCount"].should.equal(0) + response = client.describe_archive(ArchiveName="with-nested-filter") + response["EventCount"].should.equal(1) + + +@mock_events +def test_archive_with_exists_event_filter(): + client = boto3.client("events", "eu-central-1") + event_bus_arn = "arn:aws:events:eu-central-1:{}:event-bus/default".format( + ACCOUNT_ID + ) + client.create_archive( + ArchiveName="foo-exists-filter", + EventSourceArn=event_bus_arn, + EventPattern=json.dumps({"detail": {"foo": [{"exists": True}]}}), + ) + client.create_archive( + ArchiveName="foo-not-exists-filter", + EventSourceArn=event_bus_arn, + EventPattern=json.dumps({"detail": {"foo": [{"exists": False}]}}), + ) + client.create_archive( + ArchiveName="bar-exists-filter", + EventSourceArn=event_bus_arn, + EventPattern=json.dumps({"detail": {"bar": [{"exists": True}]}}), + ) + client.create_archive( + ArchiveName="bar-not-exists-filter", + EventSourceArn=event_bus_arn, + EventPattern=json.dumps({"detail": {"bar": [{"exists": False}]}}), + ) + foo_exists_event = {"Source": "", "DetailType": "", "Detail": '{"foo": "bar"}'} + foo_not_exists_event = {"Source": "", "DetailType": "", "Detail": "{}"} + response = client.put_events(Entries=[foo_exists_event, foo_not_exists_event]) + response["FailedEntryCount"].should.equal(0) + response = client.describe_archive(ArchiveName="foo-exists-filter") + response["EventCount"].should.equal(1) + response = client.describe_archive(ArchiveName="foo-not-exists-filter") + response["EventCount"].should.equal(1) + response = client.describe_archive(ArchiveName="bar-exists-filter") + response["EventCount"].should.equal(0) + response = client.describe_archive(ArchiveName="bar-not-exists-filter") response["EventCount"].should.equal(2) - response["SizeBytes"].should.be.greater_than(0) @mock_events