Repository: getmoto/moto
Pull request #7323 (2024-02-08 20:02:05)

Add ASG Scheduled Scaling Action batch APIs
Add support for batch put and batch delete APIs for Scheduled Scaling Actions on AutoScaling Groups. Delete nonexistent action now fails. Also adds support for the `TimeZone` property for actions. The behavior changes I've made in this PR are to align with the behavior I can observe by experimenting with the API.

- [Batch put API reference](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_BatchPutScheduledUpdateGroupAction.html)
- [Batch delete API reference](https://docs.aws.amazon.com/autoscaling/ec2/APIReference/API_BatchDeleteScheduledAction.html)


Patch:
diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index d7cb91469..cb973e06d 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -2523,7 +2523,7 @@
 - [X] modify_hosts
 - [ ] modify_id_format
 - [ ] modify_identity_id_format
-- [ ] modify_image_attribute
+- [X] modify_image_attribute
 - [X] modify_instance_attribute
 - [ ] modify_instance_capacity_reservation_attributes
 - [ ] modify_instance_credit_specification
diff --git a/docs/docs/services/ec2.rst b/docs/docs/services/ec2.rst
index 4398c475c..2750ebd76 100644
--- a/docs/docs/services/ec2.rst
+++ b/docs/docs/services/ec2.rst
@@ -545,7 +545,7 @@ ec2
 - [X] modify_hosts
 - [ ] modify_id_format
 - [ ] modify_identity_id_format
-- [ ] modify_image_attribute
+- [X] modify_image_attribute
 - [X] modify_instance_attribute
 - [ ] modify_instance_capacity_reservation_attributes
 - [ ] modify_instance_credit_specification
diff --git a/moto/amp/models.py b/moto/amp/models.py
index 3aed63a82..9ce1edfa2 100644
--- a/moto/amp/models.py
+++ b/moto/amp/models.py
@@ -118,7 +118,7 @@ class PrometheusServiceBackend(BaseBackend):
         """
         self.workspaces.pop(workspace_id, None)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_workspaces(self, alias: str) -> List[Workspace]:
         if alias:
             return [w for w in self.workspaces.values() if w.alias == alias]
@@ -175,7 +175,7 @@ class PrometheusServiceBackend(BaseBackend):
         ns.update(data)
         return ns
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_rule_groups_namespaces(
         self, name: str, workspace_id: str
     ) -> List[RuleGroupNamespace]:
diff --git a/moto/athena/models.py b/moto/athena/models.py
index 3c64299ec..bdba60cc0 100644
--- a/moto/athena/models.py
+++ b/moto/athena/models.py
@@ -340,7 +340,7 @@ class AthenaBackend(BaseBackend):
         self.data_catalogs[name] = data_catalog
         return data_catalog
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_named_queries(self, work_group: str) -> List[str]:
         named_query_ids = [
             q.id for q in self.named_queries.values() if q.workgroup.name == work_group
diff --git a/moto/autoscaling/models.py b/moto/autoscaling/models.py
index c65884f89..e835e71fa 100644
--- a/moto/autoscaling/models.py
+++ b/moto/autoscaling/models.py
@@ -321,11 +321,11 @@ class FakeScheduledAction(CloudFormationModel):
         max_size: Optional[int],
         min_size: Optional[int],
         scheduled_action_name: str,
-        start_time: str,
-        end_time: str,
-        recurrence: str,
+        start_time: Optional[str],
+        end_time: Optional[str],
+        recurrence: Optional[str],
+        timezone: Optional[str],
     ):
-
         self.name = name
         self.desired_capacity = desired_capacity
         self.max_size = max_size
@@ -334,6 +334,7 @@ class FakeScheduledAction(CloudFormationModel):
         self.end_time = end_time
         self.recurrence = recurrence
         self.scheduled_action_name = scheduled_action_name
+        self.timezone = timezone
 
     @staticmethod
     def cloudformation_name_type() -> str:
@@ -353,7 +354,6 @@ class FakeScheduledAction(CloudFormationModel):
         region_name: str,
         **kwargs: Any,
     ) -> "FakeScheduledAction":
-
         properties = cloudformation_json["Properties"]
 
         backend = autoscaling_backends[account_id][region_name]
@@ -373,10 +373,24 @@ class FakeScheduledAction(CloudFormationModel):
             start_time=properties.get("StartTime"),
             end_time=properties.get("EndTime"),
             recurrence=properties.get("Recurrence"),
+            timezone=properties.get("TimeZone"),
         )
         return scheduled_action
 
 
+class FailedScheduledUpdateGroupActionRequest:
+    def __init__(
+        self,
+        *,
+        scheduled_action_name: str,
+        error_code: Optional[str] = None,
+        error_message: Optional[str] = None,
+    ) -> None:
+        self.scheduled_action_name = scheduled_action_name
+        self.error_code = error_code
+        self.error_message = error_message
+
+
 def set_string_propagate_at_launch_booleans_on_tags(
     tags: List[Dict[str, Any]]
 ) -> List[Dict[str, Any]]:
@@ -953,9 +967,10 @@ class AutoScalingBackend(BaseBackend):
         max_size: Union[None, str, int],
         min_size: Union[None, str, int],
         scheduled_action_name: str,
-        start_time: str,
-        end_time: str,
-        recurrence: str,
+        start_time: Optional[str],
+        end_time: Optional[str],
+        recurrence: Optional[str],
+        timezone: Optional[str],
     ) -> FakeScheduledAction:
         max_size = make_int(max_size)
         min_size = make_int(min_size)
@@ -970,11 +985,39 @@ class AutoScalingBackend(BaseBackend):
             start_time=start_time,
             end_time=end_time,
             recurrence=recurrence,
+            timezone=timezone,
         )
 
         self.scheduled_actions[scheduled_action_name] = scheduled_action
         return scheduled_action
 
+    def batch_put_scheduled_update_group_action(
+        self, name: str, actions: List[Dict[str, Any]]
+    ) -> List[FailedScheduledUpdateGroupActionRequest]:
+        result = []
+        for action in actions:
+            try:
+                self.put_scheduled_update_group_action(
+                    name=name,
+                    desired_capacity=action.get("DesiredCapacity"),
+                    max_size=action.get("MaxSize"),
+                    min_size=action.get("MinSize"),
+                    scheduled_action_name=action["ScheduledActionName"],
+                    start_time=action.get("StartTime"),
+                    end_time=action.get("EndTime"),
+                    recurrence=action.get("Recurrence"),
+                    timezone=action.get("TimeZone"),
+                )
+            except AutoscalingClientError as err:
+                result.append(
+                    FailedScheduledUpdateGroupActionRequest(
+                        scheduled_action_name=action["ScheduledActionName"],
+                        error_code=err.error_type,
+                        error_message=err.message,
+                    )
+                )
+        return result
+
     def describe_scheduled_actions(
         self,
         autoscaling_group_name: Optional[str] = None,
@@ -1002,6 +1045,27 @@ class AutoScalingBackend(BaseBackend):
         )
         if scheduled_action:
             self.scheduled_actions.pop(scheduled_action_name, None)
+        else:
+            raise ValidationError("Scheduled action name not found")
+
+    def batch_delete_scheduled_action(
+        self, auto_scaling_group_name: str, scheduled_action_names: List[str]
+    ) -> List[FailedScheduledUpdateGroupActionRequest]:
+        result = []
+        for scheduled_action_name in scheduled_action_names:
+            try:
+                self.delete_scheduled_action(
+                    auto_scaling_group_name, scheduled_action_name
+                )
+            except AutoscalingClientError as err:
+                result.append(
+                    FailedScheduledUpdateGroupActionRequest(
+                        scheduled_action_name=scheduled_action_name,
+                        error_code=err.error_type,
+                        error_message=err.message,
+                    )
+                )
+        return result
 
     def create_auto_scaling_group(
         self,
diff --git a/moto/autoscaling/responses.py b/moto/autoscaling/responses.py
index f977ca4b5..d83ca039e 100644
--- a/moto/autoscaling/responses.py
+++ b/moto/autoscaling/responses.py
@@ -118,10 +118,23 @@ class AutoScalingResponse(BaseResponse):
             start_time=self._get_param("StartTime"),
             end_time=self._get_param("EndTime"),
             recurrence=self._get_param("Recurrence"),
+            timezone=self._get_param("TimeZone"),
         )
         template = self.response_template(PUT_SCHEDULED_UPDATE_GROUP_ACTION_TEMPLATE)
         return template.render()
 
+    def batch_put_scheduled_update_group_action(self) -> str:
+        failed_actions = (
+            self.autoscaling_backend.batch_put_scheduled_update_group_action(
+                name=self._get_param("AutoScalingGroupName"),
+                actions=self._get_multi_param("ScheduledUpdateGroupActions.member"),
+            )
+        )
+        template = self.response_template(
+            BATCH_PUT_SCHEDULED_UPDATE_GROUP_ACTION_TEMPLATE
+        )
+        return template.render(failed_actions=failed_actions)
+
     def describe_scheduled_actions(self) -> str:
         scheduled_actions = self.autoscaling_backend.describe_scheduled_actions(
             autoscaling_group_name=self._get_param("AutoScalingGroupName"),
@@ -140,6 +153,16 @@ class AutoScalingResponse(BaseResponse):
         template = self.response_template(DELETE_SCHEDULED_ACTION_TEMPLATE)
         return template.render()
 
+    def batch_delete_scheduled_action(self) -> str:
+        auto_scaling_group_name = self._get_param("AutoScalingGroupName")
+        scheduled_action_names = self._get_multi_param("ScheduledActionNames.member")
+        failed_actions = self.autoscaling_backend.batch_delete_scheduled_action(
+            auto_scaling_group_name=auto_scaling_group_name,
+            scheduled_action_names=scheduled_action_names,
+        )
+        template = self.response_template(BATCH_DELETE_SCHEDULED_ACTION_TEMPLATE)
+        return template.render(failed_actions=failed_actions)
+
     def describe_scaling_activities(self) -> str:
         template = self.response_template(DESCRIBE_SCALING_ACTIVITIES_TEMPLATE)
         return template.render()
@@ -631,13 +654,35 @@ PUT_SCHEDULED_UPDATE_GROUP_ACTION_TEMPLATE = """<PutScheduledUpdateGroupActionRe
 </ResponseMetadata>
 </PutScheduledUpdateGroupActionResponse>"""
 
+
+BATCH_PUT_SCHEDULED_UPDATE_GROUP_ACTION_TEMPLATE = """<BatchPutScheduledUpdateGroupActionResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
+  <BatchPutScheduledUpdateGroupActionResult>
+    <FailedScheduledUpdateGroupActions>
+      {% for failed_action in failed_actions %}
+      <member>
+        <ScheduledActionName>{{ failed_action.scheduled_action_name }}</ScheduledActionName>
+        {% if failed_action.error_code %}
+        <ErrorCode>{{ failed_action.error_code }}</ErrorCode>
+        {% endif %}
+        {% if failed_action.error_message %}
+        <ErrorMessage>{{ failed_action.error_message }}</ErrorMessage>
+        {% endif %}
+      </member>
+      {% endfor %}
+    </FailedScheduledUpdateGroupActions>
+  </BatchPutScheduledUpdateGroupActionResult>
+  <ResponseMetadata>
+    <RequestId></RequestId>
+  </ResponseMetadata>
+</BatchPutScheduledUpdateGroupActionResponse>"""
+
 DESCRIBE_SCHEDULED_ACTIONS = """<DescribeScheduledActionsResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
   <DescribeScheduledActionsResult>
     <ScheduledUpdateGroupActions>
       {% for scheduled_action in scheduled_actions %}
       <member>
         <AutoScalingGroupName>{{ scheduled_action.name }}</AutoScalingGroupName>
-        <ScheduledActionName> {{ scheduled_action.scheduled_action_name }}</ScheduledActionName>
+        <ScheduledActionName>{{ scheduled_action.scheduled_action_name }}</ScheduledActionName>
         {% if scheduled_action.start_time %}
         <StartTime>{{ scheduled_action.start_time }}</StartTime>
         {% endif %}
@@ -647,9 +692,18 @@ DESCRIBE_SCHEDULED_ACTIONS = """<DescribeScheduledActionsResponse xmlns="http://
         {% if scheduled_action.recurrence %}
         <Recurrence>{{ scheduled_action.recurrence }}</Recurrence>
         {% endif %}
+        {% if scheduled_action.min_size is not none %}
         <MinSize>{{ scheduled_action.min_size }}</MinSize>
+        {% endif %}
+        {% if scheduled_action.max_size is not none %}
         <MaxSize>{{ scheduled_action.max_size }}</MaxSize>
+        {% endif %}
+        {% if scheduled_action.desired_capacity is not none %}
         <DesiredCapacity>{{ scheduled_action.desired_capacity }}</DesiredCapacity>
+        {% endif %}
+        {% if scheduled_action.timezone %}
+        <TimeZone>{{ scheduled_action.timezone }}</TimeZone>
+        {% endif %}
       </member>
       {% endfor %}
     </ScheduledUpdateGroupActions>
@@ -663,6 +717,27 @@ DELETE_SCHEDULED_ACTION_TEMPLATE = """<DeleteScheduledActionResponse xmlns="http
   </ResponseMetadata>
 </DeleteScheduledActionResponse>"""
 
+BATCH_DELETE_SCHEDULED_ACTION_TEMPLATE = """<BatchDeleteScheduledActionResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
+  <BatchDeleteScheduledActionResult>
+    <FailedScheduledActions>
+      {% for failed_action in failed_actions %}
+      <member>
+        <ScheduledActionName>{{ failed_action.scheduled_action_name }}</ScheduledActionName>
+        {% if failed_action.error_code %}
+        <ErrorCode>{{ failed_action.error_code }}</ErrorCode>
+        {% endif %}
+        {% if failed_action.error_message %}
+        <ErrorMessage>{{ failed_action.error_message }}</ErrorMessage>
+        {% endif %}
+      </member>
+      {% endfor %}
+    </FailedScheduledActions>
+  </BatchDeleteScheduledActionResult>
+  <ResponseMetadata>
+    <RequestId></RequestId>
+  </ResponseMetadata>
+</BatchDeleteScheduledActionResponse>"""
+
 ATTACH_LOAD_BALANCER_TARGET_GROUPS_TEMPLATE = """<AttachLoadBalancerTargetGroupsResponse xmlns="http://autoscaling.amazonaws.com/doc/2011-01-01/">
 <AttachLoadBalancerTargetGroupsResult>
 </AttachLoadBalancerTargetGroupsResult>
diff --git a/moto/cognitoidp/models.py b/moto/cognitoidp/models.py
index 4058e2785..5ee48d371 100644
--- a/moto/cognitoidp/models.py
+++ b/moto/cognitoidp/models.py
@@ -973,7 +973,7 @@ class CognitoIdpBackend(BaseBackend):
             "MfaConfiguration": user_pool.mfa_config,
         }
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_user_pools(self) -> List[CognitoIdpUserPool]:
         return list(self.user_pools.values())
 
@@ -1046,7 +1046,7 @@ class CognitoIdpBackend(BaseBackend):
         user_pool.clients[user_pool_client.id] = user_pool_client
         return user_pool_client
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_user_pool_clients(
         self, user_pool_id: str
     ) -> List[CognitoIdpUserPoolClient]:
@@ -1095,7 +1095,7 @@ class CognitoIdpBackend(BaseBackend):
         user_pool.identity_providers[name] = identity_provider
         return identity_provider
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_identity_providers(
         self, user_pool_id: str
     ) -> List[CognitoIdpIdentityProvider]:
@@ -1163,7 +1163,7 @@ class CognitoIdpBackend(BaseBackend):
 
         return user_pool.groups[group_name]
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_groups(self, user_pool_id: str) -> List[CognitoIdpGroup]:
         user_pool = self.describe_user_pool(user_pool_id)
 
@@ -1204,7 +1204,7 @@ class CognitoIdpBackend(BaseBackend):
         group.users.add(user)
         user.groups.add(group)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_users_in_group(
         self, user_pool_id: str, group_name: str
     ) -> List[CognitoIdpUser]:
@@ -1342,7 +1342,7 @@ class CognitoIdpBackend(BaseBackend):
                 return user
         raise NotAuthorizedError("Invalid token")
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_users(self, user_pool_id: str) -> List[CognitoIdpUser]:
         user_pool = self.describe_user_pool(user_pool_id)
 
@@ -1759,7 +1759,7 @@ class CognitoIdpBackend(BaseBackend):
 
         return resource_server
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_resource_servers(self, user_pool_id: str) -> List[CognitoResourceServer]:
         user_pool = self.user_pools[user_pool_id]
         resource_servers = list(user_pool.resource_servers.values())
diff --git a/moto/core/responses.py b/moto/core/responses.py
index 6b6306d2b..fb88e837b 100644
--- a/moto/core/responses.py
+++ b/moto/core/responses.py
@@ -143,6 +143,14 @@ class _TemplateEnvironmentMixin(object):
 class ActionAuthenticatorMixin(object):
     request_count: ClassVar[int] = 0
 
+    PUBLIC_OPERATIONS = [
+        "AWSCognitoIdentityProviderService.ConfirmSignUp",
+        "AWSCognitoIdentityProviderService.GetUser",
+        "AWSCognitoIdentityProviderService.ForgotPassword",
+        "AWSCognitoIdentityProviderService.InitiateAuth",
+        "AWSCognitoIdentityProviderService.SignUp",
+    ]
+
     def _authenticate_and_authorize_action(
         self, iam_request_cls: type, resource: str = "*"
     ) -> None:
@@ -150,6 +158,11 @@ class ActionAuthenticatorMixin(object):
             ActionAuthenticatorMixin.request_count
             >= settings.INITIAL_NO_AUTH_ACTION_COUNT
         ):
+            if (
+                self.headers.get("X-Amz-Target")  # type: ignore[attr-defined]
+                in ActionAuthenticatorMixin.PUBLIC_OPERATIONS
+            ):
+                return
             parsed_url = urlparse(self.uri)  # type: ignore[attr-defined]
             path = parsed_url.path
             if parsed_url.query:
diff --git a/moto/databrew/models.py b/moto/databrew/models.py
index e96c53c88..b43854dda 100644
--- a/moto/databrew/models.py
+++ b/moto/databrew/models.py
@@ -131,7 +131,7 @@ class DataBrewBackend(BaseBackend):
         recipe = self.recipes[recipe_name]
         recipe.update(recipe_description, recipe_steps)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_recipes(
         self, recipe_version: Optional[str] = None
     ) -> List["FakeRecipeVersion"]:
@@ -148,7 +148,7 @@ class DataBrewBackend(BaseBackend):
         recipes = [getattr(self.recipes[key], version) for key in self.recipes]
         return [r for r in recipes if r is not None]
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_recipe_versions(self, recipe_name: str) -> List["FakeRecipeVersion"]:
         # https://docs.aws.amazon.com/databrew/latest/dg/API_ListRecipeVersions.html
         self.validate_length(recipe_name, "name", 255)
@@ -252,7 +252,7 @@ class DataBrewBackend(BaseBackend):
             raise RulesetNotFoundException(ruleset_name)
         return self.rulesets[ruleset_name]
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_rulesets(self) -> List["FakeRuleset"]:
         return list(self.rulesets.values())
 
@@ -287,7 +287,7 @@ class DataBrewBackend(BaseBackend):
         self.datasets[dataset_name] = dataset
         return dataset
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_datasets(self) -> List["FakeDataset"]:
         return list(self.datasets.values())
 
@@ -404,7 +404,7 @@ class DataBrewBackend(BaseBackend):
         # https://docs.aws.amazon.com/databrew/latest/dg/API_UpdateProfileJob.html
         return self.update_job(**kwargs)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_jobs(
         self, dataset_name: Optional[str] = None, project_name: Optional[str] = None
     ) -> List["FakeJob"]:
diff --git a/moto/dax/models.py b/moto/dax/models.py
index cca90b813..d12e067d0 100644
--- a/moto/dax/models.py
+++ b/moto/dax/models.py
@@ -212,7 +212,7 @@ class DAXBackend(BaseBackend):
         self.clusters[cluster_name].delete()
         return self.clusters[cluster_name]
 
-    @paginate(PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(PAGINATION_MODEL)
     def describe_clusters(self, cluster_names: Iterable[str]) -> List[DaxCluster]:
         clusters = self.clusters
         if not cluster_names:
diff --git a/moto/ds/models.py b/moto/ds/models.py
index 3ff10dd4c..53a8ccf95 100644
--- a/moto/ds/models.py
+++ b/moto/ds/models.py
@@ -477,7 +477,7 @@ class DirectoryServiceBackend(BaseBackend):
         directory = self.directories[directory_id]
         directory.enable_sso(True)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def describe_directories(
         self, directory_ids: Optional[List[str]] = None
     ) -> List[Directory]:
@@ -533,7 +533,7 @@ class DirectoryServiceBackend(BaseBackend):
         self._validate_directory_id(resource_id)
         self.tagger.untag_resource_using_names(resource_id, tag_keys)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_tags_for_resource(self, resource_id: str) -> List[Dict[str, str]]:
         """List all tags on a directory."""
         self._validate_directory_id(resource_id)
diff --git a/moto/ec2/models/amis.py b/moto/ec2/models/amis.py
index 12028f2cd..862b97536 100644
--- a/moto/ec2/models/amis.py
+++ b/moto/ec2/models/amis.py
@@ -101,11 +101,10 @@ class Ami(TaggedEC2Resource):
             if not description:
                 self.description = source_ami.description
 
-        self.launch_permission_groups: Set[str] = set()
-        self.launch_permission_users: Set[str] = set()
+        self.launch_permissions: List[Dict[str, str]] = []
 
         if public:
-            self.launch_permission_groups.add("all")
+            self.launch_permissions.append({"Group": "all"})
 
         # AWS auto-creates these, we should reflect the same.
         volume = self.ec2_backend.create_volume(size=15, zone_name=region_name)
@@ -119,7 +118,7 @@ class Ami(TaggedEC2Resource):
 
     @property
     def is_public(self) -> bool:
-        return "all" in self.launch_permission_groups
+        return {"Group": "all"} in self.launch_permissions
 
     @property
     def is_public_string(self) -> str:
@@ -273,8 +272,9 @@ class AmiBackend:
                 tmp_images = []
                 for ami in images:
                     for user_id in exec_users:
-                        if user_id in ami.launch_permission_users:
-                            tmp_images.append(ami)
+                        for lp in ami.launch_permissions:
+                            if lp.get("UserId") == user_id:
+                                tmp_images.append(ami)
                 images = tmp_images
 
             # Limit by owner ids
@@ -305,38 +305,39 @@ class AmiBackend:
         else:
             raise InvalidAMIIdError(ami_id)
 
-    def validate_permission_targets(
-        self, user_ids: Optional[List[str]] = None, group: Optional[str] = None
-    ) -> None:
-        # If anything is invalid, nothing is added. (No partial success.)
-        if user_ids:
-            """
-            AWS docs:
-              "The AWS account ID is a 12-digit number, such as 123456789012, that you use to construct Amazon Resource Names (ARNs)."
-              http://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html
-            """
-            for user_id in user_ids:
-                if len(user_id) != 12 or not user_id.isdigit():
-                    raise InvalidAMIAttributeItemValueError("userId", user_id)
+    def validate_permission_targets(self, permissions: List[Dict[str, str]]) -> None:
+        for perm in permissions:
+            # If anything is invalid, nothing is added. (No partial success.)
+            # AWS docs:
+            # The AWS account ID is a 12-digit number, such as 123456789012, that you use to construct Amazon Resource Names (ARNs)."
+            # http://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html
+
+            if "UserId" in perm and (
+                len(perm["UserId"]) != 12 or not perm["UserId"].isdigit()
+            ):
+                raise InvalidAMIAttributeItemValueError("userId", perm["UserId"])
 
-        if group and group != "all":
-            raise InvalidAMIAttributeItemValueError("UserGroup", group)
+            if "Group" in perm and perm["Group"] != "all":
+                raise InvalidAMIAttributeItemValueError("UserGroup", perm["Group"])
 
-    def add_launch_permission(
+    def modify_image_attribute(
         self,
         ami_id: str,
-        user_ids: Optional[List[str]] = None,
-        group: Optional[str] = None,
+        launch_permissions_to_add: List[Dict[str, str]],
+        launch_permissions_to_remove: List[Dict[str, str]],
     ) -> None:
         ami = self.describe_images(ami_ids=[ami_id])[0]
-        self.validate_permission_targets(user_ids=user_ids, group=group)
-
-        if user_ids:
-            for user_id in user_ids:
-                ami.launch_permission_users.add(user_id)
-
-        if group:
-            ami.launch_permission_groups.add(group)
+        self.validate_permission_targets(launch_permissions_to_add)
+        self.validate_permission_targets(launch_permissions_to_remove)
+        for lp in launch_permissions_to_add:
+            if lp not in ami.launch_permissions:
+                ami.launch_permissions.append(lp)
+        for lp in launch_permissions_to_remove:
+            try:
+                ami.launch_permissions.remove(lp)
+            except ValueError:
+                # The LaunchPermission may not exist
+                pass
 
     def register_image(
         self, name: Optional[str] = None, description: Optional[str] = None
@@ -353,21 +354,5 @@ class AmiBackend:
         self.amis[ami_id] = ami
         return ami
 
-    def remove_launch_permission(
-        self,
-        ami_id: str,
-        user_ids: Optional[List[str]] = None,
-        group: Optional[str] = None,
-    ) -> None:
-        ami = self.describe_images(ami_ids=[ami_id])[0]
-        self.validate_permission_targets(user_ids=user_ids, group=group)
-
-        if user_ids:
-            for user_id in user_ids:
-                ami.launch_permission_users.discard(user_id)
-
-        if group:
-            ami.launch_permission_groups.discard(group)
-
     def describe_image_attribute(self, ami_id: str, attribute_name: str) -> Any:
         return self.amis[ami_id].__getattribute__(attribute_name)
diff --git a/moto/ec2/responses/amis.py b/moto/ec2/responses/amis.py
index c9e36607d..7238c32e5 100644
--- a/moto/ec2/responses/amis.py
+++ b/moto/ec2/responses/amis.py
@@ -65,10 +65,7 @@ class AmisResponse(EC2BaseResponse):
             "description": "description",
             "kernel": "kernel_id",
             "ramdisk": "ramdisk",
-            "launchPermission": {
-                "groups": "launch_permission_groups",
-                "users": "launch_permission_users",
-            },
+            "launchPermission": "launch_permissions",
             "productCodes": "product_codes",
             "blockDeviceMapping": "bdm",
             "sriovNetSupport": "sriov",
@@ -85,45 +82,63 @@ class AmisResponse(EC2BaseResponse):
             # https://github.com/aws/aws-cli/issues/1083
             raise AuthFailureRestricted
 
-        groups = None
-        users = None
         attribute_value = None
+        launch_permissions = None
         if attribute_name == "launchPermission":
-            groups = self.ec2_backend.describe_image_attribute(
-                ami_id, valid_atrributes_list[attribute_name]["groups"]  # type: ignore[index]
-            )
-            users = self.ec2_backend.describe_image_attribute(
-                ami_id, valid_atrributes_list[attribute_name]["users"]  # type: ignore[index]
+            launch_permissions = self.ec2_backend.describe_image_attribute(
+                ami_id, valid_atrributes_list[attribute_name]
             )
         else:
             attribute_value = self.ec2_backend.describe_image_attribute(
                 ami_id, valid_atrributes_list[attribute_name]
             )
+
         template = self.response_template(DESCRIBE_IMAGE_ATTRIBUTES_RESPONSE)
         return template.render(
             ami_id=ami_id,
-            users=users,
-            groups=groups,
+            launch_permissions=launch_permissions,
             attribute_name=attribute_name,
             attribute_value=attribute_value,
         )
 
     def modify_image_attribute(self) -> str:
         ami_id = self._get_param("ImageId")
+        launch_permissions_to_add = list(
+            self._get_params().get("LaunchPermission", {}).get("Add", {}).values()
+        )
+        launch_permissions_to_remove = list(
+            self._get_params().get("LaunchPermission", {}).get("Remove", {}).values()
+        )
+        # If only one OperationType is added, the other attributes are submitted as different variables
         operation_type = self._get_param("OperationType")
-        group = self._get_param("UserGroup.1")
-        user_ids = self._get_multi_param("UserId")
+        if operation_type in ["add", "remove"]:
+            group = self._get_param("UserGroup.1")
+            lp = (
+                launch_permissions_to_add
+                if operation_type == "add"
+                else launch_permissions_to_remove
+            )
+            if group:
+                lp.append({"Group": group})
+
+            for user_id in self._get_multi_param("UserId"):
+                lp.append({"UserId": user_id})
+
+            org_arn = self._get_param("OrganizationArn.1")
+            if org_arn:
+                lp.append({"OrganizationArn": org_arn})
+
+            ou_arn = self._get_param("OrganizationalUnitArn.1")
+            if ou_arn:
+                lp.append({"OrganizationalUnitArn": ou_arn})
 
         self.error_on_dryrun()
 
-        if operation_type == "add":
-            self.ec2_backend.add_launch_permission(
-                ami_id, user_ids=user_ids, group=group
-            )
-        elif operation_type == "remove":
-            self.ec2_backend.remove_launch_permission(
-                ami_id, user_ids=user_ids, group=group
-            )
+        self.ec2_backend.modify_image_attribute(
+            ami_id=ami_id,
+            launch_permissions_to_add=launch_permissions_to_add,
+            launch_permissions_to_remove=launch_permissions_to_remove,
+        )
         return MODIFY_IMAGE_ATTRIBUTE_RESPONSE
 
     def register_image(self) -> str:
@@ -232,17 +247,13 @@ DESCRIBE_IMAGE_ATTRIBUTES_RESPONSE = """
         {% endfor %}
     {% endif %}
     {% if attribute_name == 'launchPermission' %}
-         {% if groups %}
-            {% for group in groups %}
-               <item>
-                  <group>{{ group }}</group>
-               </item>
-            {% endfor %}
-         {% endif %}
-         {% if users %}
-            {% for user in users %}
+         {% if launch_permissions %}
+            {% for lp in launch_permissions %}
                <item>
-                  <userId>{{ user }}</userId>
+                  {% if lp['UserId'] %}<userId>{{ lp['UserId'] }}</userId>{% endif %}
+                  {% if lp['Group'] %}<group>{{ lp['Group'] }}</group>{% endif %}
+                  {% if lp['OrganizationArn'] %}<organizationArn>{{ lp['OrganizationArn'] }}</organizationArn>{% endif %}
+                  {% if lp['OrganizationalUnitArn'] %}<organizationalUnitArn>{{ lp['OrganizationalUnitArn'] }}</organizationalUnitArn>{% endif %}
                </item>
             {% endfor %}
          {% endif %}
diff --git a/moto/events/models.py b/moto/events/models.py
index ad78fac7f..a3d9e6a61 100644
--- a/moto/events/models.py
+++ b/moto/events/models.py
@@ -1183,7 +1183,7 @@ class EventsBackend(BaseBackend):
 
         return False
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_rule_names_by_target(
         self, target_arn: str, event_bus_arn: Optional[str]
     ) -> List[Rule]:
@@ -1198,7 +1198,7 @@ class EventsBackend(BaseBackend):
 
         return matching_rules
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_rules(
         self, prefix: Optional[str] = None, event_bus_arn: Optional[str] = None
     ) -> List[Rule]:
diff --git a/moto/glue/models.py b/moto/glue/models.py
index 4a8c076c1..74155237b 100644
--- a/moto/glue/models.py
+++ b/moto/glue/models.py
@@ -123,7 +123,10 @@ class GlueBackend(BaseBackend):
         )
 
     def create_database(
-        self, database_name: str, database_input: Dict[str, Any]
+        self,
+        database_name: str,
+        database_input: Dict[str, Any],
+        tags: Optional[Dict[str, str]] = None,
     ) -> "FakeDatabase":
         if database_name in self.databases:
             raise DatabaseAlreadyExistsException()
@@ -132,6 +135,8 @@ class GlueBackend(BaseBackend):
             database_name, database_input, catalog_id=self.account_id
         )
         self.databases[database_name] = database
+        resource_arn = f"arn:aws:glue:{self.region_name}:{self.account_id}:database/{database_name}"
+        self.tag_resource(resource_arn, tags)
         return database
 
     def get_database(self, database_name: str) -> "FakeDatabase":
@@ -331,7 +336,7 @@ class GlueBackend(BaseBackend):
     def get_crawlers(self) -> List["FakeCrawler"]:
         return [self.crawlers[key] for key in self.crawlers] if self.crawlers else []
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_crawlers(self) -> List["FakeCrawler"]:
         return [crawler for _, crawler in self.crawlers.items()]
 
@@ -406,7 +411,7 @@ class GlueBackend(BaseBackend):
         except KeyError:
             raise JobNotFoundException(name)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def get_jobs(self) -> List["FakeJob"]:
         return [job for _, job in self.jobs.items()]
 
@@ -418,7 +423,7 @@ class GlueBackend(BaseBackend):
         job = self.get_job(name)
         return job.get_job_run(run_id)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_jobs(self) -> List["FakeJob"]:
         return [job for _, job in self.jobs.items()]
 
@@ -429,7 +434,7 @@ class GlueBackend(BaseBackend):
     def get_tags(self, resource_id: str) -> Dict[str, str]:
         return self.tagger.get_tag_dict_for_resource(resource_id)
 
-    def tag_resource(self, resource_arn: str, tags: Dict[str, str]) -> None:
+    def tag_resource(self, resource_arn: str, tags: Optional[Dict[str, str]]) -> None:
         tag_list = TaggingService.convert_dict_to_tags_input(tags or {})
         self.tagger.tag_resource(resource_arn, tag_list)
 
@@ -829,7 +834,7 @@ class GlueBackend(BaseBackend):
         except KeyError:
             raise SessionNotFoundException(session_id)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_sessions(self) -> List["FakeSession"]:
         return [session for _, session in self.sessions.items()]
 
@@ -884,7 +889,7 @@ class GlueBackend(BaseBackend):
         trigger = self.get_trigger(name)
         trigger.stop_trigger()
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def get_triggers(self, dependent_job_name: str) -> List["FakeTrigger"]:
         if dependent_job_name:
             triggers = []
@@ -898,7 +903,7 @@ class GlueBackend(BaseBackend):
 
         return list(self.triggers.values())
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_triggers(self, dependent_job_name: str) -> List["FakeTrigger"]:
         if dependent_job_name:
             triggers = []
diff --git a/moto/glue/responses.py b/moto/glue/responses.py
index f35417445..78a23a0b8 100644
--- a/moto/glue/responses.py
+++ b/moto/glue/responses.py
@@ -4,7 +4,14 @@ from typing import Any, Dict, List
 from moto.core.common_types import TYPE_RESPONSE
 from moto.core.responses import BaseResponse
 
-from .models import FakeCrawler, FakeJob, FakeSession, GlueBackend, glue_backends
+from .models import (
+    FakeCrawler,
+    FakeJob,
+    FakeSession,
+    FakeTrigger,
+    GlueBackend,
+    glue_backends,
+)
 
 
 class GlueResponse(BaseResponse):
@@ -24,7 +31,7 @@ class GlueResponse(BaseResponse):
         database_name = database_input.get("Name")  # type: ignore
         if "CatalogId" in self.parameters:
             database_input["CatalogId"] = self.parameters.get("CatalogId")  # type: ignore
-        self.glue_backend.create_database(database_name, database_input)  # type: ignore[arg-type]
+        self.glue_backend.create_database(database_name, database_input, self.parameters.get("Tags"))  # type: ignore[arg-type]
         return ""
 
     def get_database(self) -> str:
@@ -417,7 +424,7 @@ class GlueResponse(BaseResponse):
         return [job.get_name() for job in jobs if self.is_tags_match(job.arn, tags)]
 
     def filter_triggers_by_tags(
-        self, triggers: List[FakeJob], tags: Dict[str, str]
+        self, triggers: List[FakeTrigger], tags: Dict[str, str]
     ) -> List[str]:
         if not tags:
             return [trigger.get_name() for trigger in triggers]
diff --git a/moto/identitystore/models.py b/moto/identitystore/models.py
index d1991d065..170cffcb1 100644
--- a/moto/identitystore/models.py
+++ b/moto/identitystore/models.py
@@ -301,7 +301,7 @@ class IdentityStoreBackend(BaseBackend):
 
         return [m._asdict() for m in identity_store.groups.values()]
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_users(
         self, identity_store_id: str, filters: List[Dict[str, str]]
     ) -> List[Dict[str, str]]:
diff --git a/moto/kinesis/responses.py b/moto/kinesis/responses.py
index 5ba00cbba..994583437 100644
--- a/moto/kinesis/responses.py
+++ b/moto/kinesis/responses.py
@@ -157,9 +157,7 @@ class KinesisResponse(BaseResponse):
             limit=max_results,
             next_token=next_token,
         )
-        res = {"Shards": shards}
-        if token:
-            res["NextToken"] = token
+        res = {"Shards": shards, "NextToken": token}
         return json.dumps(res)
 
     def update_shard_count(self) -> str:
diff --git a/moto/organizations/models.py b/moto/organizations/models.py
index 639c9a28f..080caedd6 100644
--- a/moto/organizations/models.py
+++ b/moto/organizations/models.py
@@ -537,7 +537,7 @@ class OrganizationsBackend(BaseBackend):
             next_token = str(len(accounts_resp))
         return dict(CreateAccountStatuses=accounts_resp, NextToken=next_token)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_accounts(self) -> List[FakeAccount]:
         accounts = [account.describe() for account in self.accounts]
         return sorted(accounts, key=lambda x: x["JoinedTimestamp"])  # type: ignore
diff --git a/moto/panorama/models.py b/moto/panorama/models.py
index 4090fc241..b2cc62a69 100644
--- a/moto/panorama/models.py
+++ b/moto/panorama/models.py
@@ -251,7 +251,7 @@ class PanoramaBackend(BaseBackend):
             raise ValidationError(f"Device {device_id} not found")
         return device
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_devices(
         self,
         device_aggregated_status_filter: str,
diff --git a/moto/route53/models.py b/moto/route53/models.py
index e8f9047cd..e87d44366 100644
--- a/moto/route53/models.py
+++ b/moto/route53/models.py
@@ -723,7 +723,11 @@ class Route53Backend(BaseBackend):
                     the_zone.delete_rrset(record_set)
             the_zone.rr_changes.append(original_change)
 
+    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
     def list_hosted_zones(self) -> List[FakeZone]:
+        """
+        The parameters DelegationSetId and HostedZoneType are not yet implemented
+        """
         return list(self.zones.values())
 
     def list_hosted_zones_by_name(
@@ -733,7 +737,7 @@ class Route53Backend(BaseBackend):
             dnsname = dnsnames[0]
             if dnsname[-1] != ".":
                 dnsname += "."
-            zones = [zone for zone in self.list_hosted_zones() if zone.name == dnsname]
+            zones = [zone for zone in self.zones.values() if zone.name == dnsname]
         else:
             dnsname = None
             # sort by names, but with domain components reversed
@@ -745,8 +749,7 @@ class Route53Backend(BaseBackend):
                     domains = domains[-1:] + domains[:-1]
                 return ".".join(reversed(domains))
 
-            zones = self.list_hosted_zones()
-            zones = sorted(zones, key=sort_key)
+            zones = sorted(self.zones.values(), key=sort_key)
         return dnsname, zones
 
     def list_hosted_zones_by_vpc(self, vpc_id: str) -> List[Dict[str, Any]]:
@@ -754,7 +757,7 @@ class Route53Backend(BaseBackend):
         Pagination is not yet implemented
         """
         zone_list = []
-        for zone in self.list_hosted_zones():
+        for zone in self.zones.values():
             if zone.private_zone is True:
                 this_zone = self.get_hosted_zone(zone.id)
                 for vpc in this_zone.vpcs:
@@ -776,10 +779,10 @@ class Route53Backend(BaseBackend):
         return the_zone
 
     def get_hosted_zone_count(self) -> int:
-        return len(self.list_hosted_zones())
+        return len(self.zones.values())
 
     def get_hosted_zone_by_name(self, name: str) -> Optional[FakeZone]:
-        for zone in self.list_hosted_zones():
+        for zone in self.zones.values():
             if zone.name == name:
                 return zone
         return None
@@ -875,8 +878,7 @@ class Route53Backend(BaseBackend):
     ) -> QueryLoggingConfig:
         """Process the create_query_logging_config request."""
         # Does the hosted_zone_id exist?
-        response = self.list_hosted_zones()
-        zones = list(response) if response else []
+        zones = list(self.zones.values())
         for zone in zones:
             if zone.id == hosted_zone_id:
                 break
@@ -896,9 +898,8 @@ class Route53Backend(BaseBackend):
 
         from moto.logs import logs_backends  # pylint: disable=import-outside-toplevel
 
-        response = logs_backends[self.account_id][region].describe_log_groups()
-        log_groups = response[0] if response else []
-        for entry in log_groups:  # type: ignore
+        log_groups = logs_backends[self.account_id][region].describe_log_groups()
+        for entry in log_groups[0] if log_groups else []:
             if log_group_arn == entry["arn"]:
                 break
         else:
@@ -933,15 +934,14 @@ class Route53Backend(BaseBackend):
             raise NoSuchQueryLoggingConfig()
         return self.query_logging_configs[query_logging_config_id]
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_query_logging_configs(
         self, hosted_zone_id: Optional[str] = None
     ) -> List[QueryLoggingConfig]:
         """Return a list of query logging configs."""
         if hosted_zone_id:
             # Does the hosted_zone_id exist?
-            response = self.list_hosted_zones()
-            zones = list(response) if response else []
+            zones = list(self.zones.values())
             for zone in zones:
                 if zone.id == hosted_zone_id:
                     break
diff --git a/moto/route53/responses.py b/moto/route53/responses.py
index 6350c71bc..c7af8deb7 100644
--- a/moto/route53/responses.py
+++ b/moto/route53/responses.py
@@ -81,16 +81,27 @@ class Route53(BaseResponse):
                 vpcregion=vpcregion,
                 delegation_set_id=delegation_set_id,
             )
-            template = Template(CREATE_HOSTED_ZONE_RESPONSE)
+            template = Template(CREATE_HOSTED_ZONE_RESPONSE).render(zone=new_zone)
             headers = {
                 "Location": f"https://route53.amazonaws.com/2013-04-01/hostedzone/{new_zone.id}"
             }
-            return 201, headers, template.render(zone=new_zone)
+            return 201, headers, template
 
         elif request.method == "GET":
-            all_zones = self.backend.list_hosted_zones()
-            template = Template(LIST_HOSTED_ZONES_RESPONSE)
-            return 200, headers, template.render(zones=all_zones)
+            max_size = self.querystring.get("maxitems", [None])[0]
+            if max_size:
+                max_size = int(max_size)
+            marker = self.querystring.get("marker", [None])[0]
+            zone_page, next_marker = self.backend.list_hosted_zones(
+                marker=marker, max_size=max_size
+            )
+            template = Template(LIST_HOSTED_ZONES_RESPONSE).render(
+                zones=zone_page,
+                marker=marker,
+                next_marker=next_marker,
+                max_items=max_size,
+            )
+            return 200, headers, template
 
     def list_hosted_zones_by_name_response(
         self, request: Any, full_url: str, headers: Any
@@ -704,7 +715,10 @@ LIST_HOSTED_ZONES_RESPONSE = """<ListHostedZonesResponse xmlns="https://route53.
       </HostedZone>
       {% endfor %}
    </HostedZones>
-   <IsTruncated>false</IsTruncated>
+   {% if marker %}<Marker>{{ marker }}</Marker>{% endif %}
+   {%if next_marker %}<NextMarker>{{ next_marker }}</NextMarker>{% endif %}
+   {%if max_items %}<MaxItems>{{ max_items }}</MaxItems>{% endif %}
+   <IsTruncated>{{ 'true' if next_marker else 'false'}}</IsTruncated>
 </ListHostedZonesResponse>"""
 
 LIST_HOSTED_ZONES_BY_NAME_RESPONSE = """<ListHostedZonesByNameResponse xmlns="{{ xmlns }}">
diff --git a/moto/route53/utils.py b/moto/route53/utils.py
index 2e7ae4387..31060101a 100644
--- a/moto/route53/utils.py
+++ b/moto/route53/utils.py
@@ -2,6 +2,12 @@
 from .exceptions import InvalidPaginationToken
 
 PAGINATION_MODEL = {
+    "list_hosted_zones": {
+        "input_token": "marker",
+        "limit_key": "max_size",
+        "limit_default": 100,
+        "unique_attribute": "id",
+    },
     "list_query_logging_configs": {
         "input_token": "next_token",
         "limit_key": "max_results",
diff --git a/moto/route53resolver/models.py b/moto/route53resolver/models.py
index 4513e2512..4e9770d52 100644
--- a/moto/route53resolver/models.py
+++ b/moto/route53resolver/models.py
@@ -814,7 +814,7 @@ class Route53ResolverBackend(BaseBackend):
 
         return True
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_resolver_endpoints(self, filters: Any) -> List[ResolverEndpoint]:
         if not filters:
             filters = []
@@ -828,7 +828,7 @@ class Route53ResolverBackend(BaseBackend):
                 endpoints.append(endpoint)
         return endpoints
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_resolver_rules(self, filters: Any) -> List[ResolverRule]:
         if not filters:
             filters = []
@@ -842,7 +842,7 @@ class Route53ResolverBackend(BaseBackend):
                 rules.append(rule)
         return rules
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_resolver_rule_associations(
         self, filters: Any
     ) -> List[ResolverRuleAssociation]:
@@ -872,12 +872,10 @@ class Route53ResolverBackend(BaseBackend):
             f"Resolver endpoint with ID '{resource_arn}' does not exist"
         )
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
-    def list_tags_for_resource(
-        self, resource_arn: str
-    ) -> Optional[List[Dict[str, str]]]:
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_tags_for_resource(self, resource_arn: str) -> List[Dict[str, str]]:
         self._matched_arn(resource_arn)
-        return self.tagger.list_tags_for_resource(resource_arn).get("Tags")
+        return self.tagger.list_tags_for_resource(resource_arn)["Tags"]
 
     def tag_resource(self, resource_arn: str, tags: List[Dict[str, str]]) -> None:
         self._matched_arn(resource_arn)
diff --git a/moto/sagemaker/models.py b/moto/sagemaker/models.py
index 1e0697337..c7a406e89 100644
--- a/moto/sagemaker/models.py
+++ b/moto/sagemaker/models.py
@@ -1971,7 +1971,7 @@ class SageMakerModelBackend(BaseBackend):
         resource.tags.extend(tags)
         return resource.tags
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_tags(self, arn: str) -> List[Dict[str, str]]:
         resource = self._get_resource_from_arn(arn)
         return resource.tags
@@ -1980,7 +1980,7 @@ class SageMakerModelBackend(BaseBackend):
         resource = self._get_resource_from_arn(arn)
         resource.tags = [tag for tag in resource.tags if tag["Key"] not in tag_keys]
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_experiments(self) -> List["FakeExperiment"]:
         return list(self.experiments.values())
 
@@ -2150,7 +2150,7 @@ class SageMakerModelBackend(BaseBackend):
                 message=f"Could not find trial configuration '{arn}'."
             )
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_trials(
         self,
         experiment_name: Optional[str] = None,
@@ -2213,7 +2213,7 @@ class SageMakerModelBackend(BaseBackend):
     ) -> None:
         self.trial_components[trial_component_name].update(details_json)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_trial_components(
         self, trial_name: Optional[str] = None
     ) -> List["FakeTrialComponent"]:
@@ -2337,14 +2337,14 @@ class SageMakerModelBackend(BaseBackend):
             raise ValidationError(message=message)
         del self.notebook_instances[notebook_instance_name]
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_notebook_instances(
         self,
         sort_by: str,
         sort_order: str,
         name_contains: Optional[str],
         status: Optional[str],
-    ) -> Iterable[FakeSagemakerNotebookInstance]:
+    ) -> List[FakeSagemakerNotebookInstance]:
         """
         The following parameters are not yet implemented:
         CreationTimeBefore, CreationTimeAfter, LastModifiedTimeBefore, LastModifiedTimeAfter, NotebookInstanceLifecycleConfigNameContains, DefaultCodeRepositoryContains, AdditionalCodeRepositoryEquals
@@ -3283,8 +3283,8 @@ class SageMakerModelBackend(BaseBackend):
             return True
         raise ValueError(f"Invalid model package type: {model_package_type}")
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
-    def list_model_package_groups(  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_model_package_groups(
         self,
         creation_time_after: Optional[int],
         creation_time_before: Optional[int],
@@ -3346,8 +3346,8 @@ class SageMakerModelBackend(BaseBackend):
             )
         return model_package_group
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
-    def list_model_packages(  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_model_packages(
         self,
         creation_time_after: Optional[int],
         creation_time_before: Optional[int],
diff --git a/moto/sagemaker/responses.py b/moto/sagemaker/responses.py
index 948cf1bc7..8569a1651 100644
--- a/moto/sagemaker/responses.py
+++ b/moto/sagemaker/responses.py
@@ -322,11 +322,9 @@ class SageMakerResponse(BaseResponse):
 
         response = {
             "ExperimentSummaries": experiment_summaries,
+            "NextToken": next_token,
         }
 
-        if next_token:
-            response["NextToken"] = next_token
-
         return 200, {}, json.dumps(response)
 
     def delete_experiment(self) -> TYPE_RESPONSE:
@@ -368,12 +366,7 @@ class SageMakerResponse(BaseResponse):
             for trial_data in paged_results
         ]
 
-        response = {
-            "TrialSummaries": trial_summaries,
-        }
-
-        if next_token:
-            response["NextToken"] = next_token
+        response = {"TrialSummaries": trial_summaries, "NextToken": next_token}
 
         return 200, {}, json.dumps(response)
 
@@ -406,11 +399,9 @@ class SageMakerResponse(BaseResponse):
 
         response = {
             "TrialComponentSummaries": trial_component_summaries,
+            "NextToken": next_token,
         }
 
-        if next_token:
-            response["NextToken"] = next_token
-
         return 200, {}, json.dumps(response)
 
     def create_trial_component(self) -> TYPE_RESPONSE:
diff --git a/moto/ssoadmin/models.py b/moto/ssoadmin/models.py
index 820a1bbb4..c3744b6cc 100644
--- a/moto/ssoadmin/models.py
+++ b/moto/ssoadmin/models.py
@@ -337,7 +337,7 @@ class SSOAdminBackend(BaseBackend):
             message=f"Could not find PermissionSet with id {ps_id}"
         )
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_permission_sets(self, instance_arn: str) -> List[PermissionSet]:
         permission_sets = []
         for permission_set in self.permission_sets:
@@ -399,7 +399,7 @@ class SSOAdminBackend(BaseBackend):
         permissionset.managed_policies.append(managed_policy)
         permissionset.total_managed_policies_attached += 1
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_managed_policies_in_permission_set(
         self,
         instance_arn: str,
@@ -467,7 +467,7 @@ class SSOAdminBackend(BaseBackend):
         permissionset.customer_managed_policies.append(customer_managed_policy)
         permissionset.total_managed_policies_attached += 1
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_customer_managed_policy_references_in_permission_set(
         self, instance_arn: str, permission_set_arn: str
     ) -> List[CustomerManagedPolicy]:
diff --git a/moto/stepfunctions/models.py b/moto/stepfunctions/models.py
index 7cfa03f18..868bbc21f 100644
--- a/moto/stepfunctions/models.py
+++ b/moto/stepfunctions/models.py
@@ -1,7 +1,7 @@
 import json
 import re
 from datetime import datetime
-from typing import Any, Dict, Iterable, List, Optional, Pattern
+from typing import Any, Dict, List, Optional, Pattern
 
 from dateutil.tz import tzlocal
 
@@ -504,8 +504,8 @@ class StepFunctionBackend(BaseBackend):
             self.state_machines.append(state_machine)
             return state_machine
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
-    def list_state_machines(self) -> Iterable[StateMachine]:
+    @paginate(pagination_model=PAGINATION_MODEL)
+    def list_state_machines(self) -> List[StateMachine]:
         return sorted(self.state_machines, key=lambda x: x.creation_date)
 
     def describe_state_machine(self, arn: str) -> StateMachine:
@@ -552,10 +552,10 @@ class StepFunctionBackend(BaseBackend):
         state_machine = self._get_state_machine_for_execution(execution_arn)
         return state_machine.stop_execution(execution_arn)
 
-    @paginate(pagination_model=PAGINATION_MODEL)  # type: ignore[misc]
+    @paginate(pagination_model=PAGINATION_MODEL)
     def list_executions(
         self, state_machine_arn: str, status_filter: Optional[str] = None
-    ) -> Iterable[Execution]:
+    ) -> List[Execution]:
         """
         The status of every execution is set to 'RUNNING' by default.
         Set the following environment variable if you want to get a FAILED status back:
@@ -569,8 +569,7 @@ class StepFunctionBackend(BaseBackend):
         if status_filter:
             executions = list(filter(lambda e: e.status == status_filter, executions))
 
-        executions = sorted(executions, key=lambda x: x.start_date, reverse=True)
-        return executions
+        return sorted(executions, key=lambda x: x.start_date, reverse=True)
 
     def describe_execution(self, execution_arn: str) -> Execution:
         """
diff --git a/moto/utilities/paginator.py b/moto/utilities/paginator.py
index 2283d269d..078b51008 100644
--- a/moto/utilities/paginator.py
+++ b/moto/utilities/paginator.py
@@ -1,20 +1,34 @@
 import inspect
 from copy import deepcopy
 from functools import wraps
-from typing import Any, Dict, List, Optional, Tuple
+from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, TypeVar
 
 from botocore.paginate import TokenDecoder, TokenEncoder
 
 from moto.core.exceptions import InvalidToken
 
-# This should be typed using ParamSpec
-# https://stackoverflow.com/a/70591060/13245310
-# This currently does not work for our usecase
-# I believe this could be fixed after https://github.com/python/mypy/pull/14903 is accepted
+if TYPE_CHECKING:
+    from typing_extensions import ParamSpec, Protocol
 
+    P1 = ParamSpec("P1")
+    P2 = ParamSpec("P2")
+else:
+    Protocol = object
 
-def paginate(pagination_model: Dict[str, Any]) -> Any:
-    def pagination_decorator(func: Any) -> Any:
+T = TypeVar("T")
+
+
+class GenericFunction(Protocol):
+    def __call__(
+        self, func: "Callable[P1, List[T]]"
+    ) -> "Callable[P2, Tuple[List[T], Optional[str]]]":
+        ...
+
+
+def paginate(pagination_model: Dict[str, Any]) -> GenericFunction:
+    def pagination_decorator(
+        func: Callable[..., List[T]]
+    ) -> Callable[..., Tuple[List[T], Optional[str]]]:
         @wraps(func)
         def pagination_wrapper(*args: Any, **kwargs: Any) -> Any:  # type: ignore
             method = func.__name__


Test patch:
diff --git a/tests/test_autoscaling/test_autoscaling_cloudformation.py b/tests/test_autoscaling/test_autoscaling_cloudformation.py
index 0f211d23e..66694f4a3 100644
--- a/tests/test_autoscaling/test_autoscaling_cloudformation.py
+++ b/tests/test_autoscaling/test_autoscaling_cloudformation.py
@@ -305,6 +305,7 @@ def test_autoscaling_group_with_elb():
                     "MinSize": 5,
                     "Recurrence": "* * * * *",
                     "StartTime": "2022-07-01T00:00:00Z",
+                    "TimeZone": "Etc/UTC",
                 },
             },
             "my-launch-config": {
diff --git a/tests/test_autoscaling/test_autoscaling_scheduledactions.py b/tests/test_autoscaling/test_autoscaling_scheduledactions.py
index ddc94891b..486e747a3 100644
--- a/tests/test_autoscaling/test_autoscaling_scheduledactions.py
+++ b/tests/test_autoscaling/test_autoscaling_scheduledactions.py
@@ -1,6 +1,9 @@
+from datetime import datetime
 from unittest import TestCase
 
 import boto3
+from botocore.exceptions import ClientError
+from pytest import raises
 
 from moto import mock_aws
 
@@ -72,6 +75,121 @@ class TestAutoScalingScheduledActions(TestCase):
         actions = response["ScheduledUpdateGroupActions"]
         assert len(actions) == 1
 
+    def test_delete_nonexistent_action(self) -> None:
+        with raises(ClientError) as exc_info:
+            self.client.delete_scheduled_action(
+                AutoScalingGroupName=self.asg_name,
+                ScheduledActionName="nonexistent",
+            )
+        assert exc_info.value.response["Error"]["Code"] == "ValidationError"
+        assert (
+            exc_info.value.response["Error"]["Message"]
+            == "Scheduled action name not found"
+        )
+
+    def test_put_actions_content(self) -> None:
+        # actions with different combinations of properties return the correct content
+        self.client.batch_put_scheduled_update_group_action(
+            AutoScalingGroupName=self.asg_name,
+            ScheduledUpdateGroupActions=[
+                {
+                    "ScheduledActionName": "desired-capacity-with-start-time",
+                    "DesiredCapacity": 1,
+                    "StartTime": "2024-01-18T09:00:00Z",
+                },
+                {
+                    "ScheduledActionName": "min-size-with-recurrence",
+                    "MinSize": 3,
+                    "Recurrence": "* * * * *",
+                },
+                {
+                    "ScheduledActionName": "complete",
+                    "MinSize": 1,
+                    "DesiredCapacity": 2,
+                    "MaxSize": 3,
+                    "Recurrence": "* * * * *",
+                    "StartTime": "2024-01-19T09:00:00Z",
+                    "EndTime": "2024-01-20T09:00:00Z",
+                    "TimeZone": "America/New_York",
+                },
+            ],
+        )
+        response = self.client.describe_scheduled_actions(
+            AutoScalingGroupName=self.asg_name
+        )
+        actions = response["ScheduledUpdateGroupActions"]
+        assert len(actions) == 3
+
+        desired_capacity_action = next(
+            filter(
+                lambda action: action["ScheduledActionName"]
+                == "desired-capacity-with-start-time",
+                actions,
+            )
+        )
+        assert desired_capacity_action["DesiredCapacity"] == 1
+        assert desired_capacity_action["StartTime"] == datetime.fromisoformat(
+            "2024-01-18T09:00:00+00:00"
+        )
+        assert "MinSize" not in desired_capacity_action
+        assert "MaxSize" not in desired_capacity_action
+        assert "TimeZone" not in desired_capacity_action
+        assert "Recurrence" not in desired_capacity_action
+        assert "EndTime" not in desired_capacity_action
+
+        min_size_action = next(
+            filter(
+                lambda action: action["ScheduledActionName"]
+                == "min-size-with-recurrence",
+                actions,
+            )
+        )
+        assert min_size_action["MinSize"] == 3
+        assert min_size_action["Recurrence"] == "* * * * *"
+        assert "DesiredCapacity" not in min_size_action
+        assert "MaxSize" not in min_size_action
+        assert "TimeZone" not in min_size_action
+        # StartTime may be present
+        assert "EndTime" not in min_size_action
+
+        complete_action = next(
+            filter(lambda action: action["ScheduledActionName"] == "complete", actions)
+        )
+        assert complete_action["MinSize"] == 1
+        assert complete_action["DesiredCapacity"] == 2
+        assert complete_action["MaxSize"] == 3
+        assert complete_action["Recurrence"] == "* * * * *"
+        assert complete_action["StartTime"] == datetime.fromisoformat(
+            "2024-01-19T09:00:00+00:00"
+        )
+        assert complete_action["EndTime"] == datetime.fromisoformat(
+            "2024-01-20T09:00:00+00:00"
+        )
+        assert complete_action["TimeZone"] == "America/New_York"
+
+    def test_put_replaces_action_with_same_name(self) -> None:
+        self.client.put_scheduled_update_group_action(
+            AutoScalingGroupName=self.asg_name,
+            ScheduledActionName="my-action",
+            Recurrence="* * * * *",
+            MinSize=3,
+        )
+        self.client.put_scheduled_update_group_action(
+            AutoScalingGroupName=self.asg_name,
+            ScheduledActionName="my-action",
+            Recurrence="* * * * *",
+            DesiredCapacity=2,
+        )
+
+        response = self.client.describe_scheduled_actions(
+            AutoScalingGroupName=self.asg_name
+        )
+        actions = response["ScheduledUpdateGroupActions"]
+        assert len(actions) == 1
+
+        assert actions[0]["DesiredCapacity"] == 2
+        assert "MinSize" not in actions[0]
+
     def _create_scheduled_action(self, name, idx, asg_name=None):
         self.client.put_scheduled_update_group_action(
             AutoScalingGroupName=asg_name or self.asg_name,
@@ -82,4 +200,74 @@ class TestAutoScalingScheduledActions(TestCase):
             MinSize=idx + 1,
             MaxSize=idx + 5,
             DesiredCapacity=idx + 3,
+            TimeZone="Etc/UTC",
+        )
+
+    def test_batch_put_scheduled_group_action(self) -> None:
+        put_response = self.client.batch_put_scheduled_update_group_action(
+            AutoScalingGroupName=self.asg_name,
+            ScheduledUpdateGroupActions=[
+                {
+                    "ScheduledActionName": "StartAction",
+                    "Recurrence": "0 9 * * 1-5",
+                    "MinSize": 1,
+                    "MaxSize": 1,
+                    "DesiredCapacity": 1,
+                },
+                {
+                    "ScheduledActionName": "StopAction",
+                    "Recurrence": "0 17 * * 1-5",
+                    "MinSize": 0,
+                    "MaxSize": 0,
+                    "DesiredCapacity": 0,
+                },
+            ],
+        )
+        assert len(put_response["FailedScheduledUpdateGroupActions"]) == 0
+
+        response = self.client.describe_scheduled_actions(
+            AutoScalingGroupName=self.asg_name
+        )
+        actions = response["ScheduledUpdateGroupActions"]
+        assert len(actions) == 2
+
+    def test_batch_delete_scheduled_action(self) -> None:
+        action_names = [f"my-action-{i}" for i in range(10)]
+        for j, action_name in enumerate(action_names):
+            self._create_scheduled_action(action_name, j)
+        delete_response = self.client.batch_delete_scheduled_action(
+            AutoScalingGroupName=self.asg_name,
+            ScheduledActionNames=action_names[::2],  # delete even indices
+        )
+        assert len(delete_response["FailedScheduledActions"]) == 0
+
+        describe_response = self.client.describe_scheduled_actions(
+            AutoScalingGroupName=self.asg_name
+        )
+        actions = describe_response["ScheduledUpdateGroupActions"]
+        assert len(actions) == 5
+
+        delete_response = self.client.batch_delete_scheduled_action(
+            AutoScalingGroupName=self.asg_name,
+            ScheduledActionNames=action_names[1::2],  # delete odd indices
+        )
+        assert len(delete_response["FailedScheduledActions"]) == 0
+
+        describe_response = self.client.describe_scheduled_actions(
+            AutoScalingGroupName=self.asg_name
+        )
+        actions = describe_response["ScheduledUpdateGroupActions"]
+        assert len(actions) == 0
+
+    def test_batch_delete_nonexistent_action(self) -> None:
+        action_name = "nonexistent"
+        response = self.client.batch_delete_scheduled_action(
+            AutoScalingGroupName=self.asg_name,
+            ScheduledActionNames=[action_name],
         )
+        assert len(response["FailedScheduledActions"]) == 1
+        assert response["FailedScheduledActions"][0] == {
+            "ScheduledActionName": action_name,
+            "ErrorCode": "ValidationError",
+            "ErrorMessage": "Scheduled action name not found",
+        }
diff --git a/tests/test_cognitoidp/test_cognitoidp.py b/tests/test_cognitoidp/test_cognitoidp.py
index 45456b770..a9d42680f 100644
--- a/tests/test_cognitoidp/test_cognitoidp.py
+++ b/tests/test_cognitoidp/test_cognitoidp.py
@@ -19,6 +19,7 @@ import moto.cognitoidp.models
 from moto import mock_aws, settings
 from moto.cognitoidp.utils import create_id
 from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID
+from moto.core import set_initial_no_auth_action_count
 
 
 @mock_aws
@@ -2357,6 +2358,7 @@ def test_get_user():
 
 
 @mock_aws
+@set_initial_no_auth_action_count(0)
 def test_get_user_unknown_accesstoken():
     conn = boto3.client("cognito-idp", "us-west-2")
     with pytest.raises(ClientError) as ex:
@@ -3047,6 +3049,7 @@ def test_change_password__using_custom_user_agent_header():
 
 
 @mock_aws
+@set_initial_no_auth_action_count(2)
 def test_forgot_password():
     conn = boto3.client("cognito-idp", "us-west-2")
     user_pool_id = conn.create_user_pool(PoolName=str(uuid.uuid4()))["UserPool"]["Id"]
@@ -3913,15 +3916,22 @@ def test_confirm_sign_up():
     client_id = conn.create_user_pool_client(
         UserPoolId=user_pool_id, ClientName=str(uuid.uuid4()), GenerateSecret=True
     )["UserPoolClient"]["ClientId"]
+    _signup_and_confirm(client_id, conn, password, username)
+
+    result = conn.admin_get_user(UserPoolId=user_pool_id, Username=username)
+    assert result["UserStatus"] == "CONFIRMED"
+
+
+@set_initial_no_auth_action_count(0)
+def _signup_and_confirm(client_id, conn, password, username):
+    # Also verify Authentication works for these actions
+    # There are no IAM policies, but they should be public - accessible by anyone
     conn.sign_up(ClientId=client_id, Username=username, Password=password)
 
     conn.confirm_sign_up(
         ClientId=client_id, Username=username, ConfirmationCode="123456"
     )
 
-    result = conn.admin_get_user(UserPoolId=user_pool_id, Username=username)
-    assert result["UserStatus"] == "CONFIRMED"
-
 
 @mock_aws
 def test_confirm_sign_up_with_username_attributes():
@@ -4857,6 +4867,21 @@ def test_login_denied_if_account_disabled():
     assert ex.value.response["ResponseMetadata"]["HTTPStatusCode"] == 400
 
 
+@mock_aws
+# Also validate that we don't need IAM policies, as this operation should be publicly accessible
+@set_initial_no_auth_action_count(0)
+def test_initiate_auth_with_invalid_user_pool():
+    conn = boto3.client("cognito-idp", "us-west-2")
+    with pytest.raises(ClientError) as exc:
+        conn.initiate_auth(
+            ClientId="unknown",
+            AuthFlow="USER_PASSWORD_AUTH",
+            AuthParameters={"USERNAME": "user", "PASSWORD": "pass"},
+        )
+    err = exc.value.response["Error"]
+    assert err["Code"] == "ResourceNotFoundException"
+
+
 # Test will retrieve public key from cognito.amazonaws.com/.well-known/jwks.json,
 # which isnt mocked in ServerMode
 if not settings.TEST_SERVER_MODE:
diff --git a/tests/test_ec2/test_amis.py b/tests/test_ec2/test_amis.py
index e8bff43a0..87905a3ce 100644
--- a/tests/test_ec2/test_amis.py
+++ b/tests/test_ec2/test_amis.py
@@ -426,6 +426,9 @@ def test_ami_filters():
     ]
     imageB = boto3.resource("ec2", region_name="us-east-1").Image(imageB_id)
     imageB.modify_attribute(LaunchPermission={"Add": [{"Group": "all"}]})
+    assert imageB.describe_attribute(Attribute="launchPermission")[
+        "LaunchPermissions"
+    ] == [{"Group": "all"}]
 
     amis_by_architecture = ec2.describe_images(
         Filters=[{"Name": "architecture", "Values": ["x86_64"]}]
@@ -479,7 +482,6 @@ def test_ami_filters():
         Filters=[{"Name": "is-public", "Values": ["false"]}]
     )["Images"]
     assert imageA.id in [ami["ImageId"] for ami in amis_by_nonpublic]
-    assert len(amis_by_nonpublic) >= 2, "Should have at least 2 non-public images"
 
 
 @mock_aws
@@ -684,6 +686,53 @@ def test_ami_attribute_user_permissions():
     image.modify_attribute(**REMOVE_USERS_ARGS)
 
 
+@mock_aws
+def test_ami_attribute_organizations():
+    ec2 = boto3.client("ec2", region_name="us-east-1")
+    reservation = ec2.run_instances(ImageId=EXAMPLE_AMI_ID, MinCount=1, MaxCount=1)
+    instance_id = reservation["Instances"][0]["InstanceId"]
+    image_id = ec2.create_image(InstanceId=instance_id, Name="test-ami-A")["ImageId"]
+    image = boto3.resource("ec2", "us-east-1").Image(image_id)
+    arn = "someOrganizationArn"
+    image.modify_attribute(
+        Attribute="launchPermission",
+        OperationType="add",
+        OrganizationArns=[arn],
+    )
+    image.modify_attribute(
+        Attribute="launchPermission",
+        OperationType="add",
+        OrganizationalUnitArns=["ou1"],
+    )
+
+    ec2.modify_image_attribute(
+        Attribute="launchPermission",
+        ImageId=image_id,
+        LaunchPermission={
+            "Add": [
+                {"UserId": "111122223333"},
+                {"UserId": "555566667777"},
+                {"Group": "all"},
+                {"OrganizationArn": "orgarn"},
+                {"OrganizationalUnitArn": "ou2"},
+            ]
+        },
+    )
+
+    launch_permissions = image.describe_attribute(Attribute="launchPermission")[
+        "LaunchPermissions"
+    ]
+    assert launch_permissions == [
+        {"OrganizationArn": "someOrganizationArn"},
+        {"OrganizationalUnitArn": "ou1"},
+        {"UserId": "111122223333"},
+        {"UserId": "555566667777"},
+        {"Group": "all"},
+        {"OrganizationArn": "orgarn"},
+        {"OrganizationalUnitArn": "ou2"},
+    ]
+
+
 @mock_aws
 def test_ami_describe_executable_users():
     conn = boto3.client("ec2", region_name="us-east-1")
diff --git a/tests/test_glue/helpers.py b/tests/test_glue/helpers.py
index e900a5195..0ef161601 100644
--- a/tests/test_glue/helpers.py
+++ b/tests/test_glue/helpers.py
@@ -19,13 +19,17 @@ def create_database_input(database_name):
     return database_input
 
 
-def create_database(client, database_name, database_input=None, catalog_id=None):
+def create_database(
+    client, database_name, database_input=None, catalog_id=None, tags=None
+):
     if database_input is None:
         database_input = create_database_input(database_name)
 
     database_kwargs = {"DatabaseInput": database_input}
     if catalog_id is not None:
         database_kwargs["CatalogId"] = catalog_id
+    if tags is not None:
+        database_kwargs["Tags"] = tags
     return client.create_database(**database_kwargs)
 
 
diff --git a/tests/test_glue/test_datacatalog.py b/tests/test_glue/test_datacatalog.py
index 739e44513..c3efd2c98 100644
--- a/tests/test_glue/test_datacatalog.py
+++ b/tests/test_glue/test_datacatalog.py
@@ -40,6 +40,23 @@ def test_create_database():
     assert database.get("CatalogId") == database_catalog_id
 
 
+@mock_aws
+def test_create_database_with_tags():
+    client = boto3.client("glue", region_name="us-east-1")
+    database_name = "myspecialdatabase"
+    database_catalog_id = ACCOUNT_ID
+    database_input = helpers.create_database_input(database_name)
+    database_tags = {"key": "value"}
+    helpers.create_database(
+        client, database_name, database_input, database_catalog_id, tags=database_tags
+    )
+
+    response = client.get_tags(
+        ResourceArn=f"arn:aws:glue:us-east-1:{ACCOUNT_ID}:database/{database_name}"
+    )
+    assert response["Tags"] == database_tags
+
+
 @mock_aws
 def test_create_database_already_exists():
     client = boto3.client("glue", region_name="us-east-1")
diff --git a/tests/test_route53/test_route53.py b/tests/test_route53/test_route53.py
index 1ad8b90f7..2c692eb4a 100644
--- a/tests/test_route53/test_route53.py
+++ b/tests/test_route53/test_route53.py
@@ -602,6 +602,44 @@ def test_list_hosted_zones_by_dns_name():
     assert zones["HostedZones"][3]["CallerReference"] == str(hash("bar"))
 
 
+@mock_aws
+def test_list_hosted_zones_pagination():
+    conn = boto3.client("route53", region_name="us-east-1")
+
+    for idx in range(150):
+        conn.create_hosted_zone(
+            Name=f"test{idx}.com.", CallerReference=str(hash(f"h{idx}"))
+        )
+
+    page1 = conn.list_hosted_zones()
+    assert "Marker" not in page1
+    assert page1["IsTruncated"] is True
+    assert "NextMarker" in page1
+    assert "MaxItems" not in page1
+    assert len(page1["HostedZones"]) == 100
+
+    page2 = conn.list_hosted_zones(Marker=page1["NextMarker"])
+    assert page2["Marker"] == page1["NextMarker"]
+    assert page2["IsTruncated"] is False
+    assert "NextMarker" not in page2
+    assert "MaxItems" not in page2
+    assert len(page2["HostedZones"]) == 50
+
+    small_page = conn.list_hosted_zones(MaxItems="75")
+    assert "Marker" not in small_page
+    assert small_page["IsTruncated"] is True
+    assert "NextMarker" in small_page
+    assert small_page["MaxItems"] == "75"
+    assert len(small_page["HostedZones"]) == 75
+
+    remainer = conn.list_hosted_zones(Marker=small_page["NextMarker"])
+    assert remainer["Marker"] == small_page["NextMarker"]
+    assert remainer["IsTruncated"] is False
+    assert "NextMarker" not in remainer
+    assert "MaxItems" not in remainer
+    assert len(remainer["HostedZones"]) == 75
+
+
 @mock_aws
 def test_change_resource_record_sets_crud_valid():
     conn = boto3.client("route53", region_name="us-east-1")