Repository: getmoto/moto
Pull request #7847 (2024-07-13 10:54:11)

Events: list_targets_by_rule() pagination
Supercedes #7842 

We now use the `@paginate`-decorator, instead of relying on custom pagination logic. Also adds a test to verify this feature now works properly.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/getmoto/moto/pull/7847?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 94.34%. Comparing base [(`752ee27`)](https://app.codecov.io/gh/getmoto/moto/commit/752ee274646ccb6f1133995839d0b963eb36f2c7?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) to head [(`2263c49`)](https://app.codecov.io/gh/getmoto/moto/commit/2263c495aa9c7bf692cd683588e246fa9df13d97?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##           master    #7847   +/-   ##
=======================================
  Coverage   94.34%   94.34%           
=======================================
  Files        1113     1113           
  Lines       94417    94394   -23     
=======================================
- Hits        89074    89060   -14     
+ Misses       5343     5334    -9     
```

| [Flag](https://app.codecov.io/gh/getmoto/moto/pull/7847/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | Coverage Δ | |
|---|---|---|
| [servertests](https://app.codecov.io/gh/getmoto/moto/pull/7847/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `29.25% <50.00%> (+<0.01%)` | :arrow_up: |
| [unittests](https://app.codecov.io/gh/getmoto/moto/pull/7847/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto) | `94.31% <100.00%> (+<0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/getmoto/moto/pull/7847?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=getmoto).


Patch:
diff --git a/moto/events/models.py b/moto/events/models.py
index 8bacd7bf8..c21e15fba 100644
--- a/moto/events/models.py
+++ b/moto/events/models.py
@@ -1,6 +1,5 @@
 import copy
 import json
-import os
 import re
 import sys
 import warnings
@@ -8,7 +7,7 @@ from collections import OrderedDict
 from enum import Enum, unique
 from json import JSONDecodeError
 from operator import eq, ge, gt, le, lt
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any, Dict, List, Optional
 
 import requests
 
@@ -1032,32 +1031,6 @@ class EventsBackend(BaseBackend):
             self.account_id, self.region_name, "default"
         )
 
-    def _gen_next_token(self, index: int) -> str:
-        token = os.urandom(128).encode("base64")  # type: ignore
-        self.next_tokens[token] = index
-        return token
-
-    def _process_token_and_limits(
-        self,
-        array_len: int,
-        next_token: Optional[str] = None,
-        limit: Optional[str] = None,
-    ) -> Tuple[int, int, Optional[str]]:
-        start_index = 0
-        end_index = array_len
-        new_next_token: Optional[str] = None
-
-        if next_token:
-            start_index = self.next_tokens.pop(next_token, 0)
-
-        if limit is not None:
-            new_end_index = start_index + int(limit)
-            if new_end_index < end_index:
-                end_index = new_end_index
-                new_next_token = self._gen_next_token(end_index)
-
-        return start_index, end_index, new_next_token
-
     def _get_event_bus(self, name: str) -> EventBus:
         event_bus_name = name.split(f"{self.account_id}:event-bus/")[-1]
 
@@ -1211,34 +1184,16 @@ class EventsBackend(BaseBackend):
 
         return matching_rules
 
-    def list_targets_by_rule(
-        self,
-        rule_id: str,
-        event_bus_arn: Optional[str],
-        next_token: Optional[str] = None,
-        limit: Optional[str] = None,
-    ) -> Dict[str, Any]:
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_targets_by_rule(  # type: ignore[misc]
+        self, rule_id: str, event_bus_arn: Optional[str]
+    ) -> List[Dict[str, Any]]:
         # We'll let a KeyError exception be thrown for response to handle if
         # rule doesn't exist.
         event_bus_name = self._normalize_event_bus_arn(event_bus_arn)
         event_bus = self._get_event_bus(event_bus_name)
         rule = event_bus.rules[rule_id]
-
-        start_index, end_index, new_next_token = self._process_token_and_limits(
-            len(rule.targets), next_token, limit
-        )
-
-        returned_targets: List[Dict[str, Any]] = []
-        return_obj: Dict[str, Any] = {}
-
-        for i in range(start_index, end_index):
-            returned_targets.append(rule.targets[i])
-
-        return_obj["Targets"] = returned_targets
-        if new_next_token is not None:
-            return_obj["NextToken"] = new_next_token
-
-        return return_obj
+        return rule.targets
 
     def put_targets(
         self, name: str, event_bus_arn: Optional[str], targets: List[Dict[str, Any]]
diff --git a/moto/events/responses.py b/moto/events/responses.py
index 1e5787320..bca839ccb 100644
--- a/moto/events/responses.py
+++ b/moto/events/responses.py
@@ -159,9 +159,10 @@ class EventsHandler(BaseResponse):
             return self.error("ValidationException", "Parameter Rule is required.")
 
         try:
-            targets = self.events_backend.list_targets_by_rule(
-                rule_name, event_bus_arn, next_token, limit
+            target_page, token = self.events_backend.list_targets_by_rule(
+                rule_name, event_bus_arn, next_token=next_token, limit=limit
             )
+            targets = {"Targets": target_page, "NextToken": token}
         except KeyError:
             return self.error(
                 "ResourceNotFoundException", "Rule " + rule_name + " does not exist."
diff --git a/moto/events/utils.py b/moto/events/utils.py
index ea90f9022..e84c68548 100644
--- a/moto/events/utils.py
+++ b/moto/events/utils.py
@@ -38,6 +38,13 @@ PAGINATION_MODEL = {
         "unique_attribute": "arn",
         "fail_on_invalid_token": False,
     },
+    "list_targets_by_rule": {
+        "input_token": "next_token",
+        "limit_key": "limit",
+        "limit_default": 50,
+        "unique_attribute": "Arn",
+        "fail_on_invalid_token": False,
+    },
 }
 
 _BASE_EVENT_MESSAGE: EventMessageType = {


Test patch:
diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py
index 2e28caff4..aef843735 100644
--- a/tests/test_events/test_events.py
+++ b/tests/test_events/test_events.py
@@ -355,6 +355,21 @@ def test_list_targets_by_rule():
     assert len(targets["Targets"]) == len(expected_targets)
 
 
+@mock_aws
+def test_list_targets_by_rule_pagination():
+    rule_name = "test1"
+    client = generate_environment()
+    page1 = client.list_targets_by_rule(Rule=rule_name, Limit=1)
+    assert len(page1["Targets"]) == 1
+    assert "NextToken" in page1
+
+    page2 = client.list_targets_by_rule(Rule=rule_name, NextToken=page1["NextToken"])
+    assert len(page2["Targets"]) == 5
+
+    large_page = client.list_targets_by_rule(Rule=rule_name, Limit=4)
+    assert len(large_page["Targets"]) == 4
+
+
 @mock_aws
 def test_list_targets_by_rule_for_different_event_bus():
     client = generate_environment()