diff --git a/localstack-core/localstack/services/events/provider.py b/localstack-core/localstack/services/events/provider.py index 67e5e1bd9763e..644129e220511 100644 --- a/localstack-core/localstack/services/events/provider.py +++ b/localstack-core/localstack/services/events/provider.py @@ -758,7 +758,29 @@ def list_rule_names_by_target( limit: LimitMax100 = None, **kwargs, ) -> ListRuleNamesByTargetResponse: - raise NotImplementedError + region = context.region + account_id = context.account_id + store = self.get_store(region, account_id) + event_bus_name = extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + + # Find all rules that have a target with the specified ARN + matching_rule_names = [] + for rule_name, rule in event_bus.rules.items(): + for target_id, target in rule.targets.items(): + if target["Arn"] == target_arn: + matching_rule_names.append(rule_name) + break # Found a match in this rule, no need to check other targets + + limited_rules, next_token = self._get_limited_list_and_next_token( + matching_rule_names, next_token, limit + ) + + response = ListRuleNamesByTargetResponse(RuleNames=limited_rules) + if next_token is not None: + response["NextToken"] = next_token + + return response @handler("PutRule") def put_rule( @@ -1514,6 +1536,24 @@ def _get_limited_dict_and_next_token( ) return limited_dict, next_token + def _get_limited_list_and_next_token( + self, input_list: list, next_token: NextToken | None, limit: LimitMax100 | None + ) -> tuple[list, NextToken]: + """Return a slice of the given list starting from next_token with length of limit + and new last index encoded as a next_token for pagination.""" + input_list_len = len(input_list) + start_index = decode_next_token(next_token) if next_token is not None else 0 + end_index = start_index + limit if limit is not None else input_list_len + limited_list = input_list[start_index:end_index] + + next_token = ( + encode_next_token(end_index) + # return a next_token (encoded integer of next starting index) if not all items are returned + if end_index < input_list_len + else None + ) + return limited_list, next_token + def _check_resource_exists( self, resource_arn: Arn, resource_type: ResourceType, store: EventsStore ) -> None: diff --git a/tests/aws/services/events/test_events.py b/tests/aws/services/events/test_events.py index 5d7f2a731b0b6..cb748eb832c1c 100644 --- a/tests/aws/services/events/test_events.py +++ b/tests/aws/services/events/test_events.py @@ -1742,6 +1742,211 @@ def test_process_pattern_to_single_matching_rules_single_target( ) snapshot.match(f"events-{num_events}", events) + @markers.aws.validated + @pytest.mark.parametrize("bus_name", ["custom", "default"]) + def test_list_rule_names_by_target( + self, + bus_name, + events_create_event_bus, + events_put_rule, + sqs_create_queue, + sqs_get_queue_arn, + aws_client, + snapshot, + clean_up, + ): + """Test the ListRuleNamesByTarget API to verify it correctly returns rules associated with a target.""" + # Create an SQS queue to use as a target + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + queue_arn = sqs_get_queue_arn(queue_url) + + # Create an event bus if using custom bus + if bus_name == "custom": + bus_name = f"bus-{short_uid()}" + events_create_event_bus(Name=bus_name) + + # Create multiple rules targeting the same SQS queue + rule_prefix = f"rule-{short_uid()}-" + snapshot.add_transformer(snapshot.transform.regex(rule_prefix, "")) + rule_names = [] + + # Create 3 rules all targeting the same SQS queue + for i in range(3): + rule_name = f"{rule_prefix}{i}" + rule_names.append(rule_name) + events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps({"source": [f"source-{i}"]}), + ) + + # Add the SQS queue as a target for this rule + target_id = f"target-{i}" + aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[{"Id": target_id, "Arn": queue_arn}], + ) + + # Create a rule targeting a different resource (to verify filtering) + other_rule = f"{rule_prefix}other" + events_put_rule( + Name=other_rule, + EventBusName=bus_name, + EventPattern=json.dumps({"source": ["other-source"]}), + ) + + # Test the ListRuleNamesByTarget API + response = aws_client.events.list_rule_names_by_target( + TargetArn=queue_arn, + EventBusName=bus_name, + ) + + # The response should contain all rules that target our queue + snapshot.match("list_rule_names_by_target", response) + + @markers.aws.validated + @pytest.mark.parametrize("bus_name", ["custom", "default"]) + def test_list_rule_names_by_target_with_limit( + self, + bus_name, + events_create_event_bus, + events_put_rule, + sqs_create_queue, + sqs_get_queue_arn, + aws_client, + snapshot, + clean_up, + ): + """Test the ListRuleNamesByTarget API with pagination to verify it correctly handles limits and next tokens.""" + # Create an SQS queue to use as a target + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + queue_arn = sqs_get_queue_arn(queue_url) + + # Create an event bus if using custom bus + if bus_name == "custom": + bus_name = f"bus-{short_uid()}" + events_create_event_bus(Name=bus_name) + + # Create multiple rules targeting the same SQS queue + rule_prefix = f"rule-{short_uid()}-" + snapshot.add_transformer(snapshot.transform.regex(rule_prefix, "")) + rule_names = [] + + # Create 5 rules all targeting the same SQS queue + for i in range(5): + rule_name = f"{rule_prefix}{i}" + rule_names.append(rule_name) + events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps({"source": [f"source-{i}"]}), + ) + + # Add the SQS queue as a target for this rule + target_id = f"target-{i}" + aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[{"Id": target_id, "Arn": queue_arn}], + ) + + # Test pagination with limit=2 + all_rule_names = [] + next_token = None + + # First page + response = aws_client.events.list_rule_names_by_target( + TargetArn=queue_arn, + EventBusName=bus_name, + Limit=2, + ) + # Store the original NextToken value before replacing it for snapshot comparison + next_token = response["NextToken"] + snapshot.add_transformer( + snapshot.transform.jsonpath( + jsonpath="$..NextToken", + value_replacement="", + reference_replacement=True, + ) + ) + + snapshot.match("first_page", response) + all_rule_names.extend(response["RuleNames"]) + + # Second page + response = aws_client.events.list_rule_names_by_target( + TargetArn=queue_arn, + EventBusName=bus_name, + Limit=2, + NextToken=next_token, + ) + # Store the original NextToken value before replacing it for snapshot comparison + next_token = response["NextToken"] + snapshot.match("second_page", response) + all_rule_names.extend(response["RuleNames"]) + + # Third page (should have 1 remaining) + response = aws_client.events.list_rule_names_by_target( + TargetArn=queue_arn, + EventBusName=bus_name, + Limit=2, + NextToken=next_token, + ) + snapshot.match("third_page", response) + all_rule_names.extend(response["RuleNames"]) + + @markers.aws.validated + @pytest.mark.parametrize("bus_name", ["custom", "default"]) + def test_list_rule_names_by_target_no_matches( + self, + bus_name, + events_create_event_bus, + events_put_rule, + sqs_create_queue, + sqs_get_queue_arn, + aws_client, + snapshot, + clean_up, + ): + """Test that ListRuleNamesByTarget returns empty result when no rules match the target.""" + # Create two SQS queues + search_queue_url = sqs_create_queue() + search_queue_arn = sqs_get_queue_arn(search_queue_url) + + target_queue_url = sqs_create_queue() + target_queue_arn = sqs_get_queue_arn(target_queue_url) + + # Create event bus if needed + if bus_name == "custom": + bus_name = f"bus-{short_uid()}" + events_create_event_bus(Name=bus_name) + + # Create rules targeting the target queue, but none targeting the search queue + rule_name = f"rule-{short_uid()}" + events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps({"source": ["test-source"]}), + ) + + # Add the target + aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[{"Id": "target-1", "Arn": target_queue_arn}], + ) + + # Test the ListRuleNamesByTarget API with the search queue ARN + response = aws_client.events.list_rule_names_by_target( + TargetArn=search_queue_arn, + EventBusName=bus_name, + ) + + snapshot.match("list_rule_names_by_target_no_matches", response) + class TestEventPattern: @markers.aws.validated diff --git a/tests/aws/services/events/test_events.snapshot.json b/tests/aws/services/events/test_events.snapshot.json index a3805f60e72c3..668c13edfb4fe 100644 --- a/tests/aws/services/events/test_events.snapshot.json +++ b/tests/aws/services/events/test_events.snapshot.json @@ -2573,5 +2573,133 @@ } } } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target[custom]": { + "recorded-date": "19-05-2025, 07:53:33", + "recorded-content": { + "list_rule_names_by_target": { + "RuleNames": [ + "0", + "1", + "2" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target[default]": { + "recorded-date": "19-05-2025, 07:53:34", + "recorded-content": { + "list_rule_names_by_target": { + "RuleNames": [ + "0", + "1", + "2" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target_with_limit[custom]": { + "recorded-date": "19-05-2025, 07:54:06", + "recorded-content": { + "first_page": { + "NextToken": "<:1>", + "RuleNames": [ + "0", + "1" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "second_page": { + "NextToken": "<:2>", + "RuleNames": [ + "2", + "3" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "third_page": { + "RuleNames": [ + "4" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target_with_limit[default]": { + "recorded-date": "19-05-2025, 07:54:07", + "recorded-content": { + "first_page": { + "NextToken": "<:1>", + "RuleNames": [ + "0", + "1" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "second_page": { + "NextToken": "<:2>", + "RuleNames": [ + "2", + "3" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "third_page": { + "RuleNames": [ + "4" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target_no_matches[custom]": { + "recorded-date": "19-05-2025, 07:54:49", + "recorded-content": { + "list_rule_names_by_target_no_matches": { + "RuleNames": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target_no_matches[default]": { + "recorded-date": "19-05-2025, 07:54:50", + "recorded-content": { + "list_rule_names_by_target_no_matches": { + "RuleNames": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/events/test_events.validation.json b/tests/aws/services/events/test_events.validation.json index 7eea542d2272c..a28109be3f564 100644 --- a/tests/aws/services/events/test_events.validation.json +++ b/tests/aws/services/events/test_events.validation.json @@ -86,6 +86,24 @@ "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[default]": { "last_validated_date": "2025-01-08T15:28:56+00:00" }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target[custom]": { + "last_validated_date": "2025-05-19T07:53:33+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target[default]": { + "last_validated_date": "2025-05-19T07:53:34+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target_no_matches[custom]": { + "last_validated_date": "2025-05-19T07:54:49+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target_no_matches[default]": { + "last_validated_date": "2025-05-19T07:54:50+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target_with_limit[custom]": { + "last_validated_date": "2025-05-19T07:54:06+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_names_by_target_with_limit[default]": { + "last_validated_date": "2025-05-19T07:54:07+00:00" + }, "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_with_limit": { "last_validated_date": "2025-01-08T15:28:51+00:00" },