Repository: getmoto/moto

Issue:
mock_events: `Archive` `EventPattern` does not support "exists" filter
**How to reproduce:**
```py
@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}]}}),
    )
    foo_exists_event = {"Source": "", "DetailType": "", "Detail": '{"foo": "bar"}'}
    response = client.put_events(Entries=[foo_exists_event])
    response["FailedEntryCount"].should.equal(0)
    response = client.describe_archive(ArchiveName="foo-exists-filter")
    response["EventCount"].should.equal(1)
```

**Expected behaviour:**
The test should pass, as the event contains the key `detail.foo` and should therefore be routed to the Archive, in line with https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html

**Actual behaviour:**
```
During handling of the above exception, another exception occurred:

    @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}]}}),
        )
        foo_exists_event = {"Source": "", "DetailType": "", "Detail": '{"foo": "bar"}'}
        response = client.put_events(Entries=[foo_exists_event])
        response["FailedEntryCount"].should.equal(0)
        response = client.describe_archive(ArchiveName="foo-exists-filter")
>       response["EventCount"].should.equal(1)

tests/test_events/test_events.py:1559: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <sure.AssertionBuilder object at 0x10e058970>, args = (1,), kw = {}

    @wraps(func)
    def wrapper(self, *args, **kw):
        try:
            value = func(self, *args, **kw)
        except AssertionError as e:
>           raise AssertionError(e)
E           AssertionError: given
E           X = 0
E               and
E           Y = 1
E           X is 0 whereas Y is 1

venv/lib/python3.8/site-packages/sure/__init__.py:387: AssertionError

```


Discussion:

Resolving patch:
diff --git a/moto/events/models.py b/moto/events/models.py
--- a/moto/events/models.py
+++ b/moto/events/models.py
@@ -9,6 +9,7 @@
 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 @@ def remove_targets(self, ids):
             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
-
-    def _does_event_item_match_pattern_item(self, event_item, pattern_item):
-        #  Only supports "key: [value]" filters currently
-        if not event_item:
+        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_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 @@ def _send_to_events_archive(self, resource_id, event):
         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):


Test patch:
diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py
--- 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",
+        ArchiveName="with-allowed-values-filter",
         EventSourceArn=event_bus_arn,
-        EventPattern=json.dumps({"detail": {"foo": ["bar"]}}),
+        EventPattern=json.dumps({"source": ["foo", "bar"]}),
+    )
+    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]
+    )
+    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="archive-with-list-filter",
+        ArchiveName="with-nested-filter",
         EventSourceArn=event_bus_arn,
-        EventPattern=json.dumps({"source": ["foo", "bar"]}),
+        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)
 
-    # 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,
-        ]
+@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
     )
-
-    # then
-    response = client.describe_archive(ArchiveName="archive-with-dict-filter")
+    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["SizeBytes"].should.be.greater_than(0)
-
-    response = client.describe_archive(ArchiveName="archive-with-list-filter")
+    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