8000 fixed failing test · localstack/localstack@fd34e22 · GitHub
[go: up one dir, main page]

Skip to content

Commit fd34e22

Browse files
committed
fixed failing test
1 parent dec41a2 commit fd34e22

File tree

6 files changed

+266
-72
lines changed

6 files changed

+266
-72
lines changed

localstack-core/localstack/services/events/provider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@
131131
)
132132
from localstack.services.plugins import ServiceLifecycleHook
133133
from localstack.utils.common import truncate
134-
from localstack.utils.event_matcher import matches_event as matches_rule
134+
from localstack.utils.event_matcher import matches_event
135135
from localstack.utils.strings import long_uid
136136
from localstack.utils.time import TIMESTAMP_FORMAT_TZ, timestamp
137137

@@ -489,7 +489,7 @@ def test_event_pattern(
489489
https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html
490490
"""
491491
try:
492-
result = matches_rule(event, event_pattern)
492+
result = matches_event(event_pattern, event)
493493
except InternalInvalidEventPatternException as e:
494494
raise InvalidEventPatternException(e.message) from e
495495

@@ -1396,7 +1396,7 @@ def _process_rules(
13961396
) -> None:
13971397
event_pattern = rule.event_pattern
13981398
event_str = to_json_str(event_formatted)
1399-
if matches_rule(event_str, event_pattern):
1399+
if matches_event(event_pattern, event_str):
14001400
if not rule.targets:
14011401
LOG.info(
14021402
json.dumps(

localstack-core/localstack/services/events/v1/provider.py

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
EventBusNameOrArn,
2525
EventPattern,
2626
EventsApi,
27-
InvalidEventPatternException,
2827
PutRuleResponse,
2928
PutTargetsResponse,
3029
RoleArn,
@@ -40,20 +39,16 @@
4039
from localstack.constants import APPLICATION_AMZ_JSON_1_1
4140
from localstack.http import route
4241
from localstack.services.edge import ROUTER
43-
from localstack.services.events.models import (
44-
InvalidEventPatternException as InternalInvalidEventPatternException,
45-
)
4642
from localstack.services.events.scheduler import JobScheduler
4743
from localstack.services.events.v1.models import EventsStore, events_stores
48-
from localstack.services.events.v1.utils import matches_event
4944
from localstack.services.moto import call_moto
5045
from localstack.services.plugins import ServiceLifecycleHook
5146
from localstack.utils.aws.arns import event_bus_arn, parse_arn
5247
from localstack.utils.aws.client_types import ServicePrincipal
5348
from localstack.utils.aws.message_forwarding import send_event_to_target
5449
from localstack.utils.collections import pick_attributes
5550
from localstack.utils.common import TMP_FILES, mkdir, save_file, truncate
56-
from localstack.utils.event_matcher import matches_event as matches_rule
51+
from localstack.utils.event_matcher import matches_event
5752
from localstack.utils.json import extract_jsonpath
5853
from localstack.utils.strings import long_uid, short_uid
5954
from localstack.utils.time import TIMESTAMP_FORMAT_TZ, timestamp
@@ -115,44 +110,7 @@ def test_event_pattern(
115110
"""Test event pattern uses EventBridge event pattern matching:
116111
https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html
117112
"""
118-
if config.EVENT_RULE_ENGINE == "java":
119-
try:
120-
result = matches_rule(event, event_pattern)
121-
except InternalInvalidEventPatternException as e:
122-
raise InvalidEventPatternException(e.message) from e
123-
else:
124-
event_pattern_dict = json.loads(event_pattern)
125-
event_dict = json.loads(event)
126-
result = matches_event(event_pattern_dict, event_dict)
127-
128-
# TODO: unify the different implementations below:
129-
# event_pattern_dict = json.loads(event_pattern)
130-
# event_dict = json.loads(event)
131-
132-
# EventBridge:
133-
# result = matches_event(event_pattern_dict, event_dict)
134-
135-
# Lambda EventSourceMapping:
136-
# from localstack.services.lambda_.event_source_listeners.utils import does_match_event
137-
#
138-
# result = does_match_event(event_pattern_dict, event_dict)
139-
140-
# moto-ext EventBridge:
141-
# from moto.events.models import EventPattern as EventPatternMoto
142-
#
143-
# event_pattern = EventPatternMoto.load(event_pattern)
144-
# result = event_pattern.matches_event(event_dict)
145-
146-
# SNS: The SNS rule engine seems to differ slightly, for example not allowing the wildcard pattern.
147-
# from localstack.services.sns.publisher import SubscriptionFilter
148-
# subscription_filter = SubscriptionFilter()
149-
# result = subscription_filter._evaluate_nested_filter_policy_on_dict(event_pattern_dict, event_dict)
150-
151-
# moto-ext SNS:
152-
# from moto.sns.utils import FilterPolicyMatcher
153-
# filter_policy_matcher = FilterPolicyMatcher(event_pattern_dict, "MessageBody")
154-
# result = filter_policy_matcher._body_based_match(event_dict)
155-
113+
result = matches_event(event_pattern, event)
156114
return TestEventPatternResponse(Result=result)
157115

158116
@staticmethod
@@ -430,13 +388,7 @@ def filter_event_based_on_event_format(
430388
return False
431389
if rule_information.event_pattern._pattern:
432390
event_pattern = rule_information.event_pattern._pattern
433-
if config.EVENT_RULE_ENGINE == "java":
434-
event_str = json.dumps(event)
435-
event_pattern_str = json.dumps(event_pattern)
436-
match_result = matches_rule(event_str, event_pattern_str)
437-
else:
438-
match_result = matches_event(event_pattern, event)
439-
if not match_result:
391+
if not matches_event(event_pattern, event):
440392
return False
441393
return True
442394

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import json
2+
import logging
3+
import re
4+
5+
from localstack import config
6+
from localstack.aws.api.lambda_ import FilterCriteria
7+
from localstack.utils.event_matcher import matches_event
8+
from localstack.utils.strings import first_char_to_lower
9+
10+
LOG = logging.getLogger(__name__)
11+
12+
13+
class InvalidEventPatternException(Exception):
14+
reason: str
15+
16+
def __init__(self, reason=None, message=None) -> None:
17+
self.reason = reason
18+
self.message = message or f"Event pattern is not valid. Reason: {reason}"
19+
20+
21+
def filter_stream_records(records, filters: list[FilterCriteria]):
22+
filtered_records = []
23+
for record in records:
24+
for filter in filters:
25+
for rule in filter["Filters"]:
26+
if config.EVENT_RULE_ENGINE == "java":
27+
event_str = json.dumps(record)
28+
event_pattern_str = rule["Pattern"]
29+
match_result = matches_event(event_pattern_str, event_str)
30+
else:
31+
filter_pattern: dict[str, any] = json.loads(rule["Pattern"])
32+
match_result = does_match_event(filter_pattern, record)
33+
if match_result:
34+
filtered_records.append(record)
35+
break
36+
return filtered_records
37+
38+
39+
def does_match_event(event_pattern: dict[str, any], event: dict[str, any]) -> bool:
40+
"""Decides whether an event pattern matches an event or not.
41+
Returns True if the `event_pattern` matches the given `event` and False otherwise.
42+
43+
Implements "Amazon EventBridge event patterns":
44+
https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html
45+
Used in different places:
46+
* EventBridge: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html
47+
* Lambda ESM: https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html
48+
* EventBridge Pipes: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes-event-filtering.html
49+
* SNS: https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html
50+
51+
Open source AWS rule engine: https://github.com/aws/event-ruler
52+
"""
53+
# TODO: test this conditional: https://coveralls.io/builds/66584026/source?filename=localstack%2Fservices%2Flambda_%2Fevent_source_listeners%2Futils.py#L25
54+
if not event_pattern:
55+
return True
56+
does_match_results = []
57+
for key, value in event_pattern.items():
58+
# check if rule exists in event
59+
event_value = event.get(key) if isinstance(event, dict) else None
60+
does_pattern_match = False
61+
if event_value is not None:
62+
# check if filter rule value is a list (leaf of rule tree) or a dict (recursively call function)
63+
if isinstance(value, list):
64+
if len(value) > 0:
65+
if isinstance(value[0], (str, int)):
66+
does_pattern_match = event_value in value
67+
if isinstance(value[0], dict):
68+
does_pattern_match = verify_dict_filter(event_value, value[0])
69+
else:
70+
LOG.warning("Empty lambda filter: %s", key)
71+
elif isinstance(value, dict):
72+
does_pattern_match = does_match_event(value, event_value)
73+
else:
74+
# special case 'exists'
75+
def _filter_rule_value_list(val):
76+
if isinstance(val[0], dict):
77+
return not val[0].get("exists", True)
78+
elif val[0] is None:
79+
# support null filter
80+
return True
81+
82+
def _filter_rule_value_dict(val):
83+
for k, v in val.items():
84+
return (
85+
_filter_rule_value_list(val[k])
86+
if isinstance(val[k], list)
87+
else _filter_rule_value_dict(val[k])
88+
)
89+
return True
90+
91+
if isinstance(value, list) and len(value) > 0:
92+
does_pattern_match = _filter_rule_value_list(value)
93+
elif isinstance(value, dict):
94+
# special case 'exists' for S type, e.g. {"S": [{"exists": false}]}
95+
# https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.Tutorial2.html
96+
does_pattern_match = _filter_rule_value_dict(value)
97+
98+
does_match_results.append(does_pattern_match)
99+
return all(does_match_results)
100+
101+
102+
def verify_dict_filter(record_value: any, dict_filter: dict[str, any]) -> bool:
103+
# https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html#filtering-syntax
104+
does_match_filter = False
105+
for key, filter_value in dict_filter.items():
106+
if key == "anything-but":
107+
does_match_filter = record_value not in filter_value
108+
elif key == "numeric":
109+
does_match_filter = handle_numeric_conditions(record_value, filter_value)
110+
elif key == "exists":
111+
does_match_filter = bool(
112+
filter_value
113+
) # exists means that the key exists in the event record
114+
elif key == "prefix":
115+
if not isinstance(record_value, str):
116+
LOG.warning("Record Value %s does not seem to be a valid string.", record_value)
117+
does_match_filter = isinstance(record_value, str) and record_value.startswith(
118+
str(filter_value)
119+
)
120+
if does_match_filter:
121+
return True
122+
123+
return does_match_filter
124+
125+
126+
def handle_numeric_conditions(
127+
first_operand: int | float, conditions: list[str | int | float]
128+
) -> bool:
129+
"""Implements numeric matching for a given list of conditions.
130+
Example: { "numeric": [ ">", 0, "<=", 5 ] }
131+
132+
Numeric matching works with values that are JSON numbers.
133+
It is limited to values between -5.0e9 and +5.0e9 inclusive, with 15 digits of precision,
134+
or six digits to the right of the decimal point.
135+
https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matchinghttps://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matching
136+
"""
137+
# Invalid example for uneven list: { "numeric": [ ">", 0, "<" ] }
138+
if len(conditions) % 2 > 0:
139+
raise InvalidEventPatternException("Bad numeric range operator")
140+
141+
if not isinstance(first_operand, (int, float)):
142+
raise InvalidEventPatternException(
143+
f"The value {first_operand} for the numeric comparison {conditions} is not a valid number"
144+
)
145+
146+
for i in range(0, len(conditions), 2):
147+
operator = conditions[i]
148+
second_operand_str = conditions[i + 1]
149+
try:
150+
second_operand = float(second_operand_str)
151+
except ValueError:
152+
raise InvalidEventPatternException(
153+
f"Could not convert filter value {second_operand_str} to a valid number"
154+
) from ValueError
155+
156+
if operator == ">" and not (first_operand > second_operand):
157+
return False
158+
if operator == ">=" and not (first_operand >= second_operand):
159+
return False
160+
if operator == "=" and not (first_operand == second_operand):
161+
return False
162+
if operator == "<" and not (first_operand < second_operand):
163+
return False
164+
if operator == "<=" and not (first_operand <= second_operand):
165+
return False
166+
return True
167+
168+
169+
def contains_list(filter: dict) -> bool:
170+
if isinstance(filter, dict):
171+
for key, value in filter.items():
172+
if isinstance(value, list) and len(value) > 0:
173+
return True
174+
return contains_list(value)
175+
return False
176+
177+
178+
def validate_filters(filter: FilterCriteria) -> bool:
179+
# filter needs to be json serializeable
180+
for rule in filter["Filters"]:
181+
try:
182+
if not (filter_pattern := json.loads(rule["Pattern"])):
183+
return False
184+
return contains_list(filter_pattern)
185+
except json.JSONDecodeError:
186+
return False
187+
# needs to contain on what to filter (some list with citerias)
188+
# https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html#filtering-syntax
189+
190+
return True
191+
192+
193+
def message_attributes_to_lower(message_attrs):
194+
"""Convert message attribute details (first characters) to lower case (e.g., stringValue, dataType)."""
195+
message_attrs = message_attrs or {}
196+
for _, attr in message_attrs.items():
197+
if not isinstance(attr, dict):
198+
continue
199+
for key, value in dict(attr).items():
200+
attr[first_char_to_lower(key)] = attr.pop(key)
201+
return message_attrs
202+
203+
204+
def event_source_arn_matches(mapped: str, searched: str) -> bool:
205+
if not mapped:
206+
return False
207+
if not searched or mapped == searched:
208+
return True
209+
# Some types of ARNs can end with a path separated by slashes, for
210+
# example the ARN of a DynamoDB stream is tableARN/stream/ID. It's
211+
# a little counterintuitive that a more specific mapped ARN can
212+
# match a less specific ARN on the event, but some integration tests
213+
# rely on it for things like subscribing to a stream and matching an
214+
# event labeled with the table ARN.
215+
if re.match(r"^%s$" % searched, mapped):
216+
return True
217+
if mapped.startswith(searched):
218+
suffix = mapped[len(searched) :]
219+
return suffix[0] == "/"
220+
return False
221+
222+
223+
def has_data_filter_criteria(filters: list[FilterCriteria]) -> bool:
224+
for filter in filters:
225+
for rule in filter.get("Filters", []):
226+
parsed_pattern = json.loads(rule["Pattern"])
227+
if "data" in parsed_pattern:
228+
return True
229+
return False
230+
231+
232+
def has_data_filter_criteria_parsed(parsed_filters: list[dict]) -> bool:
233+
for filter in parsed_filters:
234+
if "data" in filter:
235+
return True
236+
return False

0 commit comments

Comments
 (0)
0